Upgrade from 1.x
The breaking changes between Handoff 1.x and 2.x, and the steps to migrate a project.
Handoff 2 changes the runtime requirements, the declaration API, the Figma settings, and the output directories. Upgrades within 2.x are covered in each release note.
Before you start
- Make sure the project builds with its current Handoff version.
- Commit the project files, the package version, and the lockfile.
- Install the 2.x version you plan to use:
npm install handoff-app@^2.1.0. - Apply the sections below that affect your project.
| Change | Affects |
|---|---|
| Node.js 22 | All projects |
| Figma settings | Projects that set Figma values in config |
| Catalog items | Projects with component or pattern declarations |
| Output directories | Projects that deploy the static build, commit exported/, or use public-<projectId> |
Node.js 22
Handoff 2 requires Node.js 22 or newer and npm 10 or newer. Update Node.js, CI images, and hosting
runtimes first. Projects that init created in 1.x declare the old minimum. Change it:
"engines": {
"node": ">=22",
"npm": ">=10"
}Figma settings
The Figma file ID and access token moved to integrations.figma. Handoff ignores the old top-level
figmaProjectId, figma_project_id, devAccessToken, and dev_access_token. If you use
HANDOFF_FIGMA_PROJECT_ID and HANDOFF_DEV_ACCESS_TOKEN and set no Figma values in config, no
change is needed.
The access token accepts only an environment reference. A literal token throws
HandoffConfigError, in TypeScript, JavaScript, and JSON configs alike.
import { defineConfig, fromEnv } from 'handoff-app'
export default defineConfig({
integrations: {
figma: {
projectId: 'your-figma-file-id',
accessToken: fromEnv('HANDOFF_DEV_ACCESS_TOKEN'),
},
},
})In JSON, write "accessToken": { "$env": "HANDOFF_DEV_ACCESS_TOKEN" }.
Catalog items
Handoff 2 requires defineCatalogItem for components and compositions.
Register catalog directories
Move component and pattern directories from entries to catalog.include. Global entries.js and
entries.scss still work.
entries: { components: ['components'], patterns: ['patterns'], scss: 'styles/main.scss' }catalog: { include: ['components', 'patterns'] },
entries: { scss: 'styles/main.scss' }Rename declaration files
- Use
<name>.handoff.ts,.handoff.tsx,.handoff.js, or.handoff.cjs. - Replace legacy file names such as
button.jsonorbutton.js. - JSON declarations, including
.handoff.json, are no longer read.handoff.config.jsonstill works. - If
runtime.workspace.declarationFormatisjson, change it tots,js, orcjs.
Replace the helpers
| Removed | Replacement |
|---|---|
defineReactComponent(Component, config) | defineCatalogItem({ ...config, implementation: Component }) from handoff-app/react |
defineHandlebarsComponent(config) | defineCatalogItem({ ...config, implementation: './Button.hbs' }) from handoff-app/handlebars |
defineCsfComponent(config) | defineCatalogItem({ ...config, implementation: fromCSF('./Button.stories.tsx') }) from handoff-app/react |
defineComponent(config) | defineCatalogItem({ ...config, implementation: './Button.tsx' }) from the module for its renderer |
definePattern({ components }) | defineCatalogItem({ composition }) from handoff-app/pattern |
Remove the renderer, entries.component, entries.template, and entries.story fields; the
implementation supplies them. Keep entries.js, entries.scss, entries.schema, and
entries.templates if the item uses them.
| Old field | 2.x field |
|---|---|
title | name |
should_do | shouldDo |
should_not_do | shouldNotDo |
components (pattern) | composition |
member id (pattern) | ref |
Export previews
Remove the inline previews map. Export each preview next to the default export, rename preview
title to name, and rename values to args:
export const Primary = { name: 'Primary action', args: { children: 'Save' } }- Keep a preview key's spelling if it is a valid export name. Rename it otherwise, and update the composition references to it.
- Previews appear in source export order.
- In CommonJS, use
exports.defaultfor the item andexports.Primaryfor each preview.
Replace removed types
| Removed type | Replacement |
|---|---|
Component | CatalogItem |
DeclarationPreview | CatalogPreview or Preview<typeof item> |
PatternComponentRef | CompositionRef |
ReactDeclarationConfig, HandlebarsDeclarationConfig, CsfDeclarationConfig | The input type of the catalog helper |
GenericDeclarationConfig, GenericPatternDeclarationConfig | The input type of the catalog helper, or CatalogItemMeta |
Output directories
1.x named directories after the project id (the Figma file ID, or a name from the project path). 2.x uses fixed names:
| Content | 1.x | 2.x |
|---|---|---|
Static site from build | out/<projectId> | out/static |
Tokens and assets from fetch and checkout | exported/<projectId> | exported |
| Project-specific public files | public-<projectId> | Removed. Use public |
- Update deploy scripts and CI steps that read
out/<projectId>. WithsitesOutputDirectoryorHANDOFF_SITES_DIRset, the site is instaticunder that directory. - Move
exported/<projectId>up toexported, or runnpm run fetchagain. Commit the result. - Merge
public-<projectId>intopublic, then remove it.
A repository that builds several Figma files sets HANDOFF_SITES_DIR and HANDOFF_OUTPUT_DIR, or
the matching config values in a profile, per build.
Removed features
- The
make:templatecommand. - The legacy
/assetspages. The foundation pages show logos and icons.
Verify the upgrade
- Build the project.
- Check that every catalog item appears, with the expected preview order, args, and composition references.
- Run
npm run start, edit a declaration, and check that its previews update. - With a registry: publish the migrated items, check them out into a scratch workspace, and build that workspace.