handoff-app 2.0.0-5 → 2.0.0-7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. package/README.md +174 -41
  2. package/UPGRADE.md +198 -0
  3. package/config/config.template.js +1 -4
  4. package/config/config.template.ts +1 -2
  5. package/dist/app-builder/build.js +11 -6
  6. package/dist/app-builder/client-config.js +1 -0
  7. package/dist/app-builder/config-diff/strategies/pattern.js +3 -3
  8. package/dist/app-builder/watchers/component.js +5 -5
  9. package/dist/app-builder/watchers/utils.d.ts +2 -3
  10. package/dist/app-builder/watchers.d.ts +8 -15
  11. package/dist/app-builder/watchers.js +53 -54
  12. package/dist/cache/build-cache.js +18 -22
  13. package/dist/catalog/define.d.ts +6 -0
  14. package/dist/catalog/define.js +72 -0
  15. package/dist/catalog/implementation.d.ts +27 -0
  16. package/dist/catalog/implementation.js +133 -0
  17. package/dist/catalog/index.d.ts +6 -0
  18. package/dist/catalog/index.js +22 -0
  19. package/dist/catalog/normalize.d.ts +16 -0
  20. package/dist/catalog/normalize.js +115 -0
  21. package/dist/catalog/previews.d.ts +18 -0
  22. package/dist/catalog/previews.js +106 -0
  23. package/dist/catalog/renderers.d.ts +43 -0
  24. package/dist/catalog/renderers.js +62 -0
  25. package/dist/catalog/types.d.ts +63 -0
  26. package/dist/cli/make.d.ts +0 -5
  27. package/dist/cli/make.js +22 -67
  28. package/dist/cli/scaffold.js +25 -33
  29. package/dist/commands/checkout/index.d.ts +7 -4
  30. package/dist/commands/checkout/index.js +9 -6
  31. package/dist/commands/compat/push-args.d.ts +2 -2
  32. package/dist/commands/index.js +0 -2
  33. package/dist/commands/publish/index.d.ts +9 -6
  34. package/dist/commands/publish/index.js +11 -8
  35. package/dist/commands/pull/index.js +1 -1
  36. package/dist/commands/push/index.js +1 -1
  37. package/dist/commands/utils.d.ts +15 -4
  38. package/dist/commands/utils.js +19 -6
  39. package/dist/config/catalog-include.d.ts +43 -0
  40. package/dist/config/catalog-include.js +203 -0
  41. package/dist/config/defaults.d.ts +2 -0
  42. package/dist/config/defaults.js +4 -1
  43. package/dist/config/index.d.ts +3 -3
  44. package/dist/config/index.js +6 -5
  45. package/dist/config/loader.js +9 -1
  46. package/dist/config/runtime.d.ts +5 -6
  47. package/dist/config/runtime.js +154 -154
  48. package/dist/handlebars.d.ts +9 -0
  49. package/dist/handlebars.js +16 -0
  50. package/dist/index.d.ts +15 -4
  51. package/dist/index.js +29 -15
  52. package/dist/pattern.d.ts +7 -0
  53. package/dist/pattern.js +13 -0
  54. package/dist/react.d.ts +43 -0
  55. package/dist/react.js +11 -0
  56. package/dist/registry/asset-storage/define.d.ts +1 -1
  57. package/dist/registry/asset-storage/define.js +1 -1
  58. package/dist/registry/checkout/index.d.ts +9 -0
  59. package/dist/registry/checkout/index.js +335 -149
  60. package/dist/registry/db/schema.d.ts +34 -0
  61. package/dist/registry/db/schema.js +2 -0
  62. package/dist/registry/publish/index.d.ts +13 -2
  63. package/dist/registry/publish/index.js +109 -25
  64. package/dist/registry/publish/package.js +14 -1
  65. package/dist/registry/selection.d.ts +20 -0
  66. package/dist/registry/selection.js +37 -1
  67. package/dist/store/filesystem.d.ts +1 -1
  68. package/dist/store/filesystem.js +7 -7
  69. package/dist/store/registry.js +24 -3
  70. package/dist/transformers/plugins/csf-render.js +4 -2
  71. package/dist/transformers/plugins/ssr-render.js +14 -5
  72. package/dist/transformers/preview/component/api.d.ts +8 -0
  73. package/dist/transformers/preview/component/api.js +11 -15
  74. package/dist/transformers/preview/component/artifacts.d.ts +1 -1
  75. package/dist/transformers/preview/component/artifacts.js +11 -7
  76. package/dist/transformers/preview/component/builder.js +15 -18
  77. package/dist/transformers/preview/component/css.d.ts +2 -0
  78. package/dist/transformers/preview/component/css.js +8 -11
  79. package/dist/transformers/preview/component/html.js +18 -16
  80. package/dist/transformers/preview/component/javascript.js +2 -4
  81. package/dist/transformers/preview/pattern/builder.js +31 -18
  82. package/dist/transformers/preview/types.d.ts +15 -1
  83. package/dist/types/config.d.ts +36 -11
  84. package/docs/api.md +3 -0
  85. package/docs/cli.md +1 -1
  86. package/drizzle/0006_add_component_source_format.sql +3 -0
  87. package/drizzle/meta/0006_snapshot.json +1941 -0
  88. package/drizzle/meta/_journal.json +8 -1
  89. package/package.json +37 -2
  90. package/src/app/components/Component/Preview.tsx +36 -50
  91. package/src/app/components/CopyToClipboard.tsx +27 -26
  92. package/src/app/components/Layout/Header.tsx +4 -12
  93. package/src/app/components/Layout/RuntimeModeBadge.tsx +18 -0
  94. package/src/app/components/Markdown/CodeHighlight.tsx +73 -70
  95. package/src/app/components/McpIntegration/McpConfigDialog.tsx +349 -0
  96. package/src/app/components/McpIntegration/McpIcon.tsx +10 -0
  97. package/src/app/components/Navigation/MobileNav.tsx +3 -1
  98. package/src/app/components/context/PreviewContext.tsx +5 -5
  99. package/src/app/components/ui/dialog.tsx +73 -0
  100. package/src/app/components/util/index.ts +7 -15
  101. package/src/app/lib/docs-api/backend.ts +5 -1
  102. package/src/app/lib/docs-api/page-discovery.ts +1 -6
  103. package/src/app/lib/docs-api/page-rendering.ts +21 -1
  104. package/src/app/lib/docs-api/records.ts +25 -10
  105. package/src/app/lib/docs-api/registry.ts +4 -2
  106. package/src/app/lib/docs-api/runtime-config.ts +5 -0
  107. package/src/app/lib/docs-api/search.ts +7 -2
  108. package/src/app/lib/mcp/auth.ts +64 -0
  109. package/src/app/lib/mcp/index.ts +2 -0
  110. package/src/app/lib/mcp/server.ts +343 -0
  111. package/src/app/lib/mcp/shape.ts +596 -0
  112. package/src/app/lib/registry-api/transfer.ts +7 -5
  113. package/src/app/next.config.mjs +3 -0
  114. package/src/app/pages/[...slug]/index.tsx +16 -30
  115. package/src/app/pages/api/mcp.ts +56 -0
  116. package/src/app/pages/index.tsx +10 -9
  117. package/src/app-builder/build.ts +12 -8
  118. package/src/app-builder/client-config.ts +2 -1
  119. package/src/app-builder/config-diff/strategies/pattern.ts +1 -1
  120. package/src/app-builder/watchers/component.ts +3 -3
  121. package/src/app-builder/watchers/utils.ts +2 -3
  122. package/src/app-builder/watchers.ts +46 -50
  123. package/src/cache/build-cache.ts +11 -14
  124. package/src/catalog/README.md +92 -0
  125. package/src/catalog/define.ts +79 -0
  126. package/src/catalog/implementation.ts +145 -0
  127. package/src/catalog/index.ts +31 -0
  128. package/src/catalog/normalize.ts +143 -0
  129. package/src/catalog/previews.ts +111 -0
  130. package/src/catalog/renderers.ts +94 -0
  131. package/src/catalog/types.ts +74 -0
  132. package/src/cli/README.md +1 -1
  133. package/src/cli/make.ts +20 -72
  134. package/src/cli/scaffold.ts +25 -31
  135. package/src/commands/README.md +1 -1
  136. package/src/commands/checkout/index.ts +17 -7
  137. package/src/commands/compat/push-args.ts +2 -2
  138. package/src/commands/index.ts +0 -2
  139. package/src/commands/publish/index.ts +19 -9
  140. package/src/commands/pull/index.ts +2 -2
  141. package/src/commands/push/index.ts +2 -2
  142. package/src/commands/utils.ts +22 -7
  143. package/src/config/README.md +2 -1
  144. package/src/config/catalog-include.ts +227 -0
  145. package/src/config/defaults.ts +3 -0
  146. package/src/config/index.ts +4 -4
  147. package/src/config/loader.ts +12 -0
  148. package/src/config/runtime.ts +186 -191
  149. package/src/handlebars.ts +18 -0
  150. package/src/index.ts +39 -26
  151. package/src/pattern.ts +14 -0
  152. package/src/react.ts +52 -0
  153. package/src/registry/asset-storage/define.ts +1 -1
  154. package/src/registry/checkout/index.ts +416 -152
  155. package/src/registry/db/schema.ts +2 -0
  156. package/src/registry/publish/index.ts +123 -26
  157. package/src/registry/publish/package.ts +14 -1
  158. package/src/registry/selection.ts +44 -0
  159. package/src/store/filesystem.ts +4 -4
  160. package/src/store/registry.ts +13 -2
  161. package/src/transformers/plugins/csf-render.ts +4 -2
  162. package/src/transformers/plugins/ssr-render.ts +15 -5
  163. package/src/transformers/preview/component/api.ts +13 -1
  164. package/src/transformers/preview/component/artifacts.ts +8 -4
  165. package/src/transformers/preview/component/builder.ts +17 -4
  166. package/src/transformers/preview/component/css.ts +6 -10
  167. package/src/transformers/preview/component/html.ts +30 -19
  168. package/src/transformers/preview/component/javascript.ts +1 -3
  169. package/src/transformers/preview/pattern/builder.ts +33 -20
  170. package/src/transformers/preview/types.ts +15 -1
  171. package/src/types/config.ts +38 -11
  172. package/templates/default/README.md.tpl +10 -0
  173. package/templates/default/components/button/button.react.handoff.js.tpl +19 -23
  174. package/templates/default/components/button/button.react.handoff.ts.tpl +22 -22
  175. package/templates/default/handoff.config.js.tpl +11 -18
  176. package/templates/default/handoff.config.ts.tpl +11 -18
  177. package/templates/default/handoff.config.with-examples.js.tpl +12 -16
  178. package/templates/default/handoff.config.with-examples.ts.tpl +12 -16
  179. package/config/templates/component/template.json +0 -49
  180. package/config/templates/template.html +0 -7
  181. package/dist/commands/make/template.d.ts +0 -8
  182. package/dist/commands/make/template.js +0 -43
  183. package/dist/config/entries.d.ts +0 -47
  184. package/dist/config/entries.js +0 -162
  185. package/dist/config/normalizers/declaration.d.ts +0 -11
  186. package/dist/config/normalizers/declaration.js +0 -122
  187. package/dist/config/normalizers/pattern.d.ts +0 -8
  188. package/dist/config/normalizers/pattern.js +0 -41
  189. package/dist/declarations/helpers.d.ts +0 -6
  190. package/dist/declarations/helpers.js +0 -19
  191. package/dist/declarations/index.d.ts +0 -2
  192. package/dist/declarations/index.js +0 -9
  193. package/dist/declarations/types.d.ts +0 -60
  194. package/src/commands/make/template.ts +0 -46
  195. package/src/config/entries.ts +0 -175
  196. package/src/config/normalizers/declaration.ts +0 -179
  197. package/src/config/normalizers/pattern.ts +0 -59
  198. package/src/declarations/helpers.ts +0 -36
  199. package/src/declarations/index.ts +0 -19
  200. package/src/declarations/types.ts +0 -70
  201. /package/dist/{declarations → catalog}/types.js +0 -0
package/README.md CHANGED
@@ -66,11 +66,16 @@ my-handoff-project/
66
66
  └─ out/ # build output; gitignored
67
67
  ```
68
68
 
69
- ## Components and patterns
69
+ ## Catalog items
70
70
 
71
- An implementation and a `*.handoff.ts` declaration are required for a
72
- TypeScript React component. Stable identity, documentation metadata, source
73
- entries, and previews are supplied by the declaration.
71
+ Every documented UI entry is a catalog item. Each item declares either an implementation or a composition of other items.
72
+ An implementation names its renderer. `defineCatalogItem` comes from the module for that renderer.
73
+ The declaration supplies stable identity, documentation metadata, source entries, and previews.
74
+
75
+ ### React
76
+
77
+ Handoff finds the implementation file from its import, so you do not need to repeat the path.
78
+ Previews are named exports. `Preview<typeof item>` gives them the implementation's argument type.
74
79
 
75
80
  ```tsx
76
81
  // components/example/Component.tsx
@@ -83,61 +88,136 @@ export default function Component({ label }: ComponentProps) {
83
88
  }
84
89
  ```
85
90
 
86
- ```ts
87
- // components/example/component.handoff.ts
88
- import { defineReactComponent } from 'handoff-app';
91
+ ```tsx
92
+ // components/example/example.handoff.ts
93
+ import { defineCatalogItem, type Preview } from 'handoff-app/react';
89
94
  import Component from './Component';
90
95
 
91
- export default defineReactComponent(Component, {
96
+ const item = defineCatalogItem({
92
97
  id: 'component-id',
93
98
  name: 'Component name',
94
99
  description: 'Usage guidance for the component.',
95
100
  group: 'Group name',
96
- entries: {
97
- component: './Component.tsx',
98
- },
99
- previews: {
100
- default: {
101
- title: 'Default',
102
- args: { label: 'Example' },
103
- },
104
- },
101
+ implementation: Component,
102
+ });
103
+
104
+ export default item;
105
+
106
+ type ComponentPreview = Preview<typeof item>;
107
+
108
+ export const Default = {
109
+ args: { label: 'Example' },
110
+ } satisfies ComponentPreview;
111
+ ```
112
+
113
+ The export name is the preview name. Use `name` to set a different display title.
114
+
115
+ ### Handlebars
116
+
117
+ The implementation is the path to the template. Arguments are `Record<string, unknown>` by default;
118
+ an explicit argument type is supplied through `defineCatalogItem<BadgeArgs>`.
119
+
120
+ ```ts
121
+ // components/badge/badge.handoff.ts
122
+ import { defineCatalogItem, type Preview } from 'handoff-app/handlebars';
123
+
124
+ const item = defineCatalogItem({
125
+ id: 'badge',
126
+ name: 'Badge',
127
+ implementation: './Badge.hbs',
105
128
  });
129
+
130
+ export default item;
131
+
132
+ type BadgePreview = Preview<typeof item>;
133
+
134
+ export const Primary = {
135
+ args: { variant: 'primary', children: 'New' },
136
+ } satisfies BadgePreview;
137
+ ```
138
+
139
+ ### CSF
140
+
141
+ CSF is a source format. A story file uses the renderer from the module that declares it. A React item references an existing CSF file with `fromCSF`.
142
+ The CSF file stays unchanged and defines its previews through named exports. Each story's `args` merge with and override `meta.args`.
143
+ Handoff preserves story names, `argTypes`, and `render` functions.
144
+ It finds the React component through `meta.component`, then documents and publishes it with the story file.
145
+
146
+ ```ts
147
+ // components/card/card.handoff.ts
148
+ import { defineCatalogItem, fromCSF } from 'handoff-app/react';
149
+
150
+ export default defineCatalogItem({
151
+ id: 'card',
152
+ name: 'Card',
153
+ implementation: fromCSF('./Card.stories.tsx'),
154
+ });
155
+ ```
156
+
157
+ `Meta` and `StoryObj` are exported from `handoff-app/react`, so a story file type-checks without a
158
+ Storybook dependency. A project that already has Storybook keeps using its own types.
159
+
160
+ ```tsx
161
+ // components/card/Card.stories.tsx
162
+ import { type Meta, type StoryObj } from 'handoff-app/react';
163
+ import Card from './Card';
164
+
165
+ const meta = { component: Card } satisfies Meta<typeof Card>;
166
+ export default meta;
167
+
168
+ export const Primary: StoryObj<typeof meta> = {
169
+ args: { title: 'Example' },
170
+ };
106
171
  ```
107
172
 
108
- Components are referenced by stable ID in patterns. A named preview can be
109
- selected and its arguments can be overridden by each reference. Additional
110
- references can be added as required by the composition.
173
+ A CSF preview is rendered on the server and is not hydrated. A direct React implementation is
174
+ hydrated in the browser.
175
+
176
+ ### Compositions
177
+
178
+ A composition references other items by stable id. A named preview can be selected and its arguments
179
+ can be overridden by each reference. A composition needs no renderer, so it is declared with
180
+ `defineCatalogItem` from `handoff-app/pattern`.
111
181
 
112
182
  ```ts
113
- // patterns/example/pattern.handoff.ts
114
- import { definePattern } from 'handoff-app';
183
+ // patterns/example/example.handoff.ts
184
+ import { defineCatalogItem } from 'handoff-app/pattern';
115
185
 
116
- export default definePattern({
186
+ export default defineCatalogItem({
117
187
  id: 'pattern-id',
118
188
  name: 'Pattern name',
119
189
  description: 'Purpose and usage of the composition.',
120
190
  group: 'Group name',
121
- components: [
122
- {
123
- id: 'component-id',
124
- preview: 'default',
125
- args: { label: 'Pattern example' },
126
- },
191
+ composition: [
192
+ { ref: 'component-id', preview: 'Default' },
193
+ { ref: 'badge', args: { children: 'Send' } },
127
194
  ],
128
195
  });
129
196
  ```
130
197
 
131
- Component and pattern directories are registered in the project config:
198
+ An item declares `implementation` or `composition`, never both. Terms such as atom, element, and
199
+ block stay optional classification metadata on `type` and `categories`.
200
+
201
+ The `id` addresses the item everywhere: its artifacts, its documentation URL, and its publish and
202
+ checkout commands. Components and patterns share one namespace, so each id must be unique across
203
+ both.
204
+
205
+ If two declarations claim one id, Handoff keeps the first and skips the second. The warning names
206
+ both files. `publish` refuses to run until each id is unique.
207
+
208
+
209
+ ### Registration
210
+
211
+ Catalog directories are registered in one place. A path is either an item directory or a collection
212
+ directory whose subdirectories are each treated as an item.
132
213
 
133
214
  ```ts
134
215
  // handoff.config.ts
135
216
  import { defineConfig } from 'handoff-app';
136
217
 
137
218
  export default defineConfig({
138
- entries: {
139
- components: ['components/example'],
140
- patterns: ['patterns'],
219
+ catalog: {
220
+ include: ['components', 'patterns'],
141
221
  },
142
222
  runtime: {
143
223
  workspace: {
@@ -147,6 +227,11 @@ export default defineConfig({
147
227
  });
148
228
  ```
149
229
 
230
+ ### Upgrade from version 1.x.x to version 2.x.x
231
+
232
+ See the [version 1.x.x to version 2.x.x migration guide](UPGRADE.md#version-1xx-to-version-2xx)
233
+ for breaking changes, migration steps, and upgrade verification.
234
+
150
235
  Custom documentation pages are Markdown files under `pages/`. Their relative
151
236
  paths become their routes and registry IDs.
152
237
 
@@ -340,20 +425,23 @@ npm run publish -- all
340
425
  A single kind is published on its own:
341
426
 
342
427
  ```bash
343
- npm run publish -- components
344
- npm run publish -- patterns
428
+ npm run publish -- catalog
345
429
  npm run publish -- pages
346
430
  npm run publish -- tokens
347
431
  npm run publish -- assets
348
432
  ```
349
433
 
434
+ `catalog` covers every catalog item. An item is stored as a component or as a
435
+ pattern, and its declaration decides which. The command names items and never a
436
+ kind.
437
+
350
438
  Publishing tokens or assets runs the Figma data pipeline before upload, so the
351
439
  documented Figma credentials must be available.
352
440
 
353
441
  One or more IDs can be appended to narrow a publish to those entities:
354
442
 
355
443
  ```bash
356
- npm run publish -- components component-id another-id
444
+ npm run publish -- catalog item-id another-id
357
445
  ```
358
446
 
359
447
  `--dry-run` reports what would be uploaded and contacts no registry at all, so
@@ -363,16 +451,16 @@ the existing output, and the two combine to leave the workspace untouched:
363
451
 
364
452
  ```bash
365
453
  npm run publish -- all --dry-run
366
- npm run publish -- components --no-build
454
+ npm run publish -- catalog --no-build
367
455
  ```
368
456
 
369
457
  `checkout` takes the same `all`, multi-ID, and `--dry-run` forms. A dry-run
370
458
  checkout reads from the registry, lists the files it would create or overwrite,
371
459
  and writes nothing.
372
460
 
373
- After the registry is reloaded, the published components, patterns, and
374
- foundations should be visible. Published database records are read by registry
375
- pages; the local workspace is never read directly.
461
+ After the registry is reloaded, the published catalog items and foundations are
462
+ visible. Published database records are read by registry pages; the local
463
+ workspace is never read directly.
376
464
 
377
465
  For CI, a registry connection and user-issued token are configured:
378
466
 
@@ -385,6 +473,51 @@ runtime: {
385
473
  },
386
474
  ```
387
475
 
476
+ ## MCP
477
+
478
+ The documentation app serves a Model Context Protocol endpoint at `/api/mcp/`, so
479
+ coding agents can look up the pages, components and tokens that already exist instead
480
+ of inventing markup and values. It is stateless Streamable HTTP over `POST`;
481
+ there is nothing to start and no extra port. Keep the trailing slash: the app
482
+ sets `trailingSlash: true`, so `/api/mcp` answers with a 308 redirect.
483
+
484
+ | Target | Endpoint | Credential |
485
+ | --- | --- | --- |
486
+ | Workspace (`dev` / `start`) | `http://localhost:3000/api/mcp/` | none, as with `/api/docs/*` |
487
+ | Registry (standalone or Vercel) | `<registry url>/api/mcp/` | an access token, read-only is enough |
488
+ | Static export | not available, a static site has no API routes | n/a |
489
+
490
+ Registry tokens come from `handoff-app login` or Account settings in the
491
+ registry itself, and are the same credentials the rest of the registry API takes.
492
+
493
+ The endpoint is served unless a project turns it off:
494
+
495
+ ```ts
496
+ runtime: {
497
+ mcp: false,
498
+ },
499
+ ```
500
+
501
+ Like `runtime.mode`, this is config-only and baked at build time, so changing it
502
+ in a deployed registry means a rebuild. A disabled build answers 404 on the route,
503
+ drops the MCP SDK from the packaged bundle, and shows no connect affordance at all
504
+ rather than one that fails.
505
+
506
+ The documentation app carries its own endpoint and a ready-made client config
507
+ for Claude Code, Cursor and VS Code behind the plug icon in the header. By hand:
508
+
509
+ ```json
510
+ {
511
+ "mcpServers": {
512
+ "handoff": {
513
+ "type": "http",
514
+ "url": "https://registry.example.com/api/mcp/",
515
+ "headers": { "Authorization": "Bearer hnd_..." }
516
+ }
517
+ }
518
+ }
519
+ ```
520
+
388
521
  ## Configuration
389
522
 
390
523
  Configuration is read from `handoff.config.ts`, `.js`, `.cjs`, or `.json`, in
@@ -428,7 +561,7 @@ both.
428
561
  | `npm run build -- [--target static\|registry]` | The static site or standalone registry bundle is built |
429
562
  | `npm run db:migrate` | Registry database migrations are applied |
430
563
  | `npm run validate` | Configured components are validated |
431
- | `npm run publish -- <kind\|all> [id...]` | Components, patterns, pages, tokens, or assets are published |
564
+ | `npm run publish -- <kind\|all> [id...]` | Catalog items, pages, tokens, or assets are published |
432
565
  | `npm run checkout -- <kind\|all> [id...]` | Published content is pulled into a workspace |
433
566
  | `npm run login -- --url <url>` | The CLI is authorized through the registry device flow |
434
567
  | `npm run logout -- [--url <url>]` | A saved CLI credential is revoked and removed |
package/UPGRADE.md ADDED
@@ -0,0 +1,198 @@
1
+ # Upgrade Handoff
2
+
3
+ This document contains migration steps for each major version upgrade.
4
+
5
+ ## Version 1.x.x to version 2.x.x
6
+
7
+ This guide describes how to upgrade a Handoff project from version 1.x.x to version 2.x.x.
8
+ Each feature section identifies the affected projects and the required changes.
9
+
10
+ ### Before you start
11
+
12
+ 1. Verify that the project builds with its current Handoff version.
13
+ 2. Save the project files, package version, and lockfile in version control.
14
+ 3. Install the version 2.x.x package that you plan to use.
15
+ 4. Apply the migration sections that affect your project.
16
+
17
+ ### Breaking changes
18
+
19
+ | Change | Affected projects |
20
+ | --- | --- |
21
+ | [Catalog items](#catalog-items) | Projects with component or pattern declarations |
22
+
23
+ <!-- Add future breaking changes as feature sections before "Verify the upgrade". Add each section to this table. -->
24
+
25
+ ### Catalog items
26
+
27
+ #### Affected projects
28
+
29
+ This change affects projects that use the old component or pattern registration API.
30
+ It also affects projects that use the deprecated API from an earlier version 2.x.x prerelease.
31
+ Version 2.x.x requires `defineCatalogItem` for implementations and compositions.
32
+
33
+ #### Register catalog directories
34
+
35
+ Move component and pattern directories from `entries` to `catalog.include`.
36
+
37
+ Version 1.x.x:
38
+
39
+ ```js
40
+ entries: { components: ['components'], patterns: ['patterns'], scss: 'styles/main.scss' }
41
+ ```
42
+
43
+ Version 2.x.x:
44
+
45
+ ```js
46
+ catalog: { include: ['components', 'patterns'] },
47
+ entries: { scss: 'styles/main.scss' }
48
+ ```
49
+
50
+ Each path can name one item directory or a collection of item directories. Global `entries.js` and `entries.scss` still work.
51
+
52
+ #### Convert declaration files
53
+
54
+ Use `<name>.handoff.ts`, `<name>.handoff.js`, or `<name>.handoff.cjs`.
55
+ Replace legacy filenames such as `button.json` or `button.js`.
56
+
57
+ Version 2.x.x does not support JSON item declarations, including `.handoff.json`.
58
+ JSON project config (`handoff.config.json`) remains supported.
59
+
60
+ If `runtime.workspace.declarationFormat` is `json`, change it to `ts`, `js`, or `cjs`. The default remains `ts`.
61
+
62
+ #### Replace declaration helpers
63
+
64
+ | Removed API | Replacement |
65
+ | --- | --- |
66
+ | `defineReactComponent(Component, config)` | `defineCatalogItem({ ...config, implementation: Component })` from `handoff-app/react` |
67
+ | `defineHandlebarsComponent(config)` | `defineCatalogItem({ ...config, implementation: './Button.hbs' })` from `handoff-app/handlebars` |
68
+ | `defineCsfComponent(config)` | `defineCatalogItem({ ...config, implementation: fromCSF('./Button.stories.tsx') })` from `handoff-app/react` |
69
+ | `defineComponent(config)` | `defineCatalogItem({ ...config, implementation: './Button.tsx' })` from the module for its renderer |
70
+ | `definePattern({ components })` | `defineCatalogItem({ composition })` from `handoff-app/pattern` |
71
+
72
+ Remove the old `renderer`, `entries.component`, `entries.template`, and `entries.story` fields.
73
+ The implementation supplies the renderer and source file.
74
+ Keep the supporting `entries.js`, `entries.scss`, `entries.schema`, and `entries.templates` paths that your item uses.
75
+ Rename the metadata fields:
76
+
77
+ | Old field | Version 2.x.x field |
78
+ | --- | --- |
79
+ | `title` | `name` |
80
+ | `should_do` | `shouldDo` |
81
+ | `should_not_do` | `shouldNotDo` |
82
+
83
+ ##### React
84
+
85
+ ```ts
86
+ import { defineCatalogItem, type Preview } from 'handoff-app/react';
87
+ import Button from './Button';
88
+
89
+ const item = defineCatalogItem({
90
+ name: 'Button',
91
+ implementation: Button,
92
+ entries: { scss: './button.scss' },
93
+ });
94
+ export default item;
95
+ export const Primary = { args: { children: 'Save' } } satisfies Preview<typeof item>;
96
+ ```
97
+
98
+ Imported components retain their prop types. A file path also works: `implementation: './Button.tsx'`.
99
+ Keep implementation files inside the item directory to publish them.
100
+
101
+ ##### Handlebars and CommonJS
102
+
103
+ ```js
104
+ const { defineCatalogItem } = require('handoff-app/handlebars');
105
+
106
+ exports.default = defineCatalogItem({
107
+ name: 'Badge',
108
+ implementation: './Badge.hbs',
109
+ });
110
+ exports.Primary = { args: { children: 'New' } };
111
+ ```
112
+
113
+ Use this form in `.handoff.js` or `.handoff.cjs`. JavaScript declarations can also use `import` and `export`.
114
+
115
+ ##### CSF
116
+
117
+ ```ts
118
+ import { defineCatalogItem, fromCSF } from 'handoff-app/react';
119
+
120
+ export default defineCatalogItem({
121
+ name: 'Button',
122
+ implementation: fromCSF('./Button.stories.tsx'),
123
+ });
124
+ ```
125
+
126
+ CSF is a React source format. The story file supplies the previews.
127
+ The filename does not select CSF automatically.
128
+
129
+ ##### Compositions
130
+
131
+ ```ts
132
+ import { defineCatalogItem } from 'handoff-app/pattern';
133
+
134
+ export default defineCatalogItem({
135
+ name: 'Toolbar',
136
+ composition: [{ ref: 'button', preview: 'Primary', args: { children: 'Save' } }],
137
+ });
138
+ ```
139
+
140
+ Rename `components` to `composition`.
141
+ Rename each member's `id` to `ref`.
142
+ Keep item IDs stable.
143
+
144
+ #### Export previews
145
+
146
+ Remove the inline `previews` map.
147
+ Export each preview beside the default catalog item.
148
+ Rename preview `title` to `name`.
149
+ Rename `values` to `args`:
150
+
151
+ ```ts
152
+ export const Primary = { name: 'Primary action', args: { children: 'Save' } };
153
+ ```
154
+
155
+ If a preview key is a valid export name, preserve its spelling and capitalization.
156
+ If a key is not a valid export name, rename it.
157
+ Update composition references to each renamed preview.
158
+
159
+ Previews appear in source export order.
160
+
161
+ For CommonJS declarations, use `exports.default` for the item.
162
+ Use a named export such as `exports.Primary` for each preview.
163
+
164
+ #### Replace declaration types
165
+
166
+ Replace imports of the removed types:
167
+
168
+ | Removed type | Replacement |
169
+ | --- | --- |
170
+ | `Component` | `CatalogItem` |
171
+ | `DeclarationPreview` | `CatalogPreview` or `Preview<typeof item>` |
172
+ | `PatternComponentRef` | `CompositionRef` |
173
+ | `ReactDeclarationConfig`, `HandlebarsDeclarationConfig`, `CsfDeclarationConfig` | The catalog helper's input type |
174
+ | `GenericDeclarationConfig`, `GenericPatternDeclarationConfig` | The catalog helper's input type, or `CatalogItemMeta` for shared metadata |
175
+
176
+ `RendererKind` comes from the catalog API and contains `react` and `handlebars`.
177
+ CSF uses `renderer: 'react'` with `format: 'csf'` inside `implementation`.
178
+
179
+ #### Registry projects
180
+
181
+ Checkout writes only catalog declarations.
182
+ Registry records require explicit renderer and source-format metadata where applicable.
183
+
184
+ ### Verify the upgrade
185
+
186
+ After you apply the required migrations, verify the project:
187
+
188
+ 1. Build the project with version 2.x.x.
189
+ 2. Verify that all expected catalog items appear in the documentation.
190
+ 3. Verify the preview order, arguments, and composition references.
191
+ 4. Start the workspace with file watchers.
192
+ 5. Edit a declaration to verify that its previews update.
193
+
194
+ If the project uses a registry, verify the published items:
195
+
196
+ 1. Publish the migrated items.
197
+ 2. Verify that checkout produces valid declarations.
198
+ 3. Rebuild the checked-out items.
@@ -54,13 +54,10 @@ const config = {
54
54
  ports: { app: 3000, websocket: 3001 },
55
55
  },
56
56
  // These are the entry points used to compile the target application
57
+ catalog: { include: ['./components'] },
57
58
  entries: {
58
59
  scss: './sass/main.scss',
59
60
  js: './js/main.js',
60
- components: ['./components'],
61
- // Uncomment to enable pattern documentation pages.
62
- // Patterns compose component previews into single-page views.
63
- // patterns: ['./patterns'],
64
61
  },
65
62
  // These are the asset zip file download links
66
63
  assetsZipLinks: {
@@ -34,11 +34,10 @@ export default defineConfig({
34
34
  },
35
35
  ports: { app: 3000, websocket: 3001 },
36
36
  },
37
+ catalog: { include: ['./components'] },
37
38
  entries: {
38
39
  scss: './sass/main.scss',
39
40
  js: './js/main.js',
40
- components: ['./components'],
41
- // patterns: ['./patterns'],
42
41
  },
43
42
  assetsZipLinks: {
44
43
  icons: null,
@@ -40,6 +40,7 @@ const p = __importStar(require("@clack/prompts"));
40
40
  const cross_spawn_1 = __importDefault(require("cross-spawn"));
41
41
  const fs_extra_1 = __importDefault(require("fs-extra"));
42
42
  const path_1 = __importDefault(require("path"));
43
+ const config_1 = require("../config");
43
44
  const components_1 = require("../pipeline/components");
44
45
  const patterns_1 = require("../pipeline/patterns");
45
46
  const resolve_1 = require("../registry/asset-storage/resolve");
@@ -216,6 +217,7 @@ const initializeProjectApp = (handoff_1, ...args_1) => __awaiter(void 0, [handof
216
217
  // Resolved runtime mode + registry connection inputs (names only) baked into the Next bundle so the
217
218
  // deployed registry app resolves its mode/DB env-var name without any build-machine filesystem.
218
219
  const escapedRuntimeMode = escapeForSingleQuotedJsString(runtimeMode);
220
+ const escapedMcpEnabled = escapeForSingleQuotedJsString(String((0, config_1.isMcpEnabled)(handoff.config)));
219
221
  const escapedRegistryDriver = escapeForSingleQuotedJsString((0, driver_1.resolveRegistryDriver)(handoff.config));
220
222
  const escapedDatabaseUrlEnv = escapeForSingleQuotedJsString((0, driver_1.resolveDatabaseUrlEnv)(handoff.config));
221
223
  // Asset storage selection baked (provider + module + env-var names + non-secret options JSON).
@@ -233,6 +235,7 @@ const initializeProjectApp = (handoff_1, ...args_1) => __awaiter(void 0, [handof
233
235
  '%HANDOFF_EXPORT_PATH%': escapedExportPath,
234
236
  '%HANDOFF_WEBSOCKET_PORT%': escapedWebsocketPort,
235
237
  '%HANDOFF_RUNTIME_MODE%': escapedRuntimeMode,
238
+ '%HANDOFF_MCP_ENABLED%': escapedMcpEnabled,
236
239
  '%HANDOFF_REGISTRY_DRIVER%': escapedRegistryDriver,
237
240
  '%HANDOFF_REGISTRY_DATABASE_URL_ENV%': escapedDatabaseUrlEnv,
238
241
  '%HANDOFF_ASSET_STORAGE_ADAPTER%': escapedAssetStorageAdapter,
@@ -426,12 +429,16 @@ containers, custom Node servers, and other non-Vercel hosts.
426
429
  /**
427
430
  * Resolve the set of npm packages the packaged registry app must be able to `require`/`import` at
428
431
  * runtime, given the configured database driver. `next`/`react`/`react-dom` run the server and React
429
- * runtime; `drizzle-orm` backs both the request-time DB client and the migration runner; the driver
430
- * package is driver-specific (the Neon serverless driver also needs `ws` for its Node WebSocket transport).
432
+ * runtime; `drizzle-orm` backs both the request-time DB client and the migration runner; the MCP SDK
433
+ * (and its `zod` peer) is only needed when `/api/mcp/` is enabled; the driver package is
434
+ * driver-specific (the Neon serverless driver also needs `ws` for its Node WebSocket transport).
431
435
  */
432
436
  const getRequiredRegistryRuntimeModules = (handoff) => {
433
437
  var _a;
434
438
  const base = ['next', 'next-auth', 'react', 'react-dom', 'drizzle-orm'];
439
+ if ((0, config_1.isMcpEnabled)(handoff.config)) {
440
+ base.push('@modelcontextprotocol/sdk', 'zod');
441
+ }
435
442
  const driver = (0, driver_1.resolveRegistryDriver)(handoff.config);
436
443
  const driverModules = driver === 'neon' ? ['@neondatabase/serverless', 'ws'] : ['pg'];
437
444
  // Asset-storage SDKs the deployed registry must be able to load at request time. The pre-packaged
@@ -688,14 +695,12 @@ const watchApp = (handoff) => __awaiter(void 0, void 0, void 0, function* () {
688
695
  pendingHandlers: new Map(),
689
696
  runtimeComponentsWatcher: null,
690
697
  runtimeConfigurationWatcher: null,
691
- componentDirectoriesWatcher: null,
692
- patternDirectoriesWatcher: null,
698
+ catalogDirectoriesWatcher: null,
693
699
  };
694
700
  (0, watchers_1.watchPublicDirectory)(handoff, wss, state, chokidarConfig);
695
701
  (0, watchers_1.watchRuntimeComponents)(handoff, state, (0, watchers_1.getRuntimeComponentsPathsToWatch)(handoff));
696
702
  (0, watchers_1.watchRuntimeConfiguration)(handoff, state);
697
- (0, watchers_1.watchComponentDirectories)(handoff, state, chokidarConfig);
698
- (0, watchers_1.watchPatternDirectories)(handoff, state, chokidarConfig);
703
+ (0, watchers_1.watchCatalogDirectories)(handoff, state, chokidarConfig);
699
704
  (0, watchers_1.watchGlobalEntries)(handoff, state, chokidarConfig);
700
705
  (0, watchers_1.watchPages)(handoff, chokidarConfig);
701
706
  });
@@ -65,6 +65,7 @@ const buildServerRuntimeConfig = (config, modeOverride) => {
65
65
  const assetStorage = (0, resolve_1.resolveAssetStorageFromConfig)(config);
66
66
  return {
67
67
  mode: (_b = modeOverride !== null && modeOverride !== void 0 ? modeOverride : (_a = config === null || config === void 0 ? void 0 : config.runtime) === null || _a === void 0 ? void 0 : _a.mode) !== null && _b !== void 0 ? _b : 'workspace',
68
+ mcp: (0, config_1.isMcpEnabled)(config),
68
69
  registry: {
69
70
  driver: (0, driver_1.resolveRegistryDriver)(config),
70
71
  databaseUrlEnv: (0, driver_1.resolveDatabaseUrlEnv)(config),
@@ -43,12 +43,12 @@ const snapshot_1 = require("../snapshot");
43
43
  * components. Outer key is component id, inner key is synthetic preview key.
44
44
  */
45
45
  const snapshotPatternSynthetics = (handoff, patternId) => {
46
- var _a, _b, _c, _d;
47
- const components = (_c = (_b = (_a = handoff.runtimeConfig) === null || _a === void 0 ? void 0 : _a.entries) === null || _b === void 0 ? void 0 : _b.components) !== null && _c !== void 0 ? _c : {};
46
+ var _a, _b, _c;
47
+ const components = (_b = (_a = handoff.runtimeConfig) === null || _a === void 0 ? void 0 : _a.entities.components) !== null && _b !== void 0 ? _b : {};
48
48
  const prefix = `__pattern_${patternId}_`;
49
49
  const result = new Map();
50
50
  for (const [componentId, component] of Object.entries(components)) {
51
- const previews = (_d = component.internalPatternPreviews) !== null && _d !== void 0 ? _d : {};
51
+ const previews = (_c = component.internalPatternPreviews) !== null && _c !== void 0 ? _c : {};
52
52
  const entries = new Map();
53
53
  for (const [key, preview] of Object.entries(previews)) {
54
54
  if (key.startsWith(prefix)) {
@@ -23,11 +23,11 @@ const mapEntryTypeToSegment = (type) => {
23
23
  };
24
24
  exports.mapEntryTypeToSegment = mapEntryTypeToSegment;
25
25
  const resolveComponentIdForChangedFile = (handoff, changedFilePath) => {
26
- var _a, _b, _c, _d;
26
+ var _a, _b, _c;
27
27
  const normalizedChangedPath = (0, path_2.normalizePathForCompare)(changedFilePath);
28
- const runtimeComponents = (_c = (_b = (_a = handoff.runtimeConfig) === null || _a === void 0 ? void 0 : _a.entries) === null || _b === void 0 ? void 0 : _b.components) !== null && _c !== void 0 ? _c : {};
28
+ const runtimeComponents = (_b = (_a = handoff.runtimeConfig) === null || _a === void 0 ? void 0 : _a.entities.components) !== null && _b !== void 0 ? _b : {};
29
29
  for (const [componentId, componentDef] of Object.entries(runtimeComponents)) {
30
- const entries = (_d = componentDef.entries) !== null && _d !== void 0 ? _d : {};
30
+ const entries = (_c = componentDef.entries) !== null && _c !== void 0 ? _c : {};
31
31
  for (const entryPath of Object.values(entries)) {
32
32
  if (!entryPath)
33
33
  continue;
@@ -49,8 +49,8 @@ exports.resolveComponentIdForChangedFile = resolveComponentIdForChangedFile;
49
49
  const getRuntimeComponentsPathsToWatch = (handoff) => {
50
50
  var _a, _b, _c;
51
51
  const result = new Map();
52
- for (const runtimeComponentId of Object.keys((_b = (_a = handoff.runtimeConfig) === null || _a === void 0 ? void 0 : _a.entries.components) !== null && _b !== void 0 ? _b : {})) {
53
- const runtimeComponent = handoff.runtimeConfig.entries.components[runtimeComponentId];
52
+ for (const runtimeComponentId of Object.keys((_b = (_a = handoff.runtimeConfig) === null || _a === void 0 ? void 0 : _a.entities.components) !== null && _b !== void 0 ? _b : {})) {
53
+ const runtimeComponent = handoff.runtimeConfig.entities.components[runtimeComponentId];
54
54
  for (const [runtimeComponentEntryType, runtimeComponentEntryPath] of Object.entries((_c = runtimeComponent.entries) !== null && _c !== void 0 ? _c : {})) {
55
55
  const normalizedComponentEntryPath = runtimeComponentEntryPath;
56
56
  if (fs_extra_1.default.existsSync(normalizedComponentEntryPath)) {
@@ -4,9 +4,8 @@ export interface WatcherState {
4
4
  pendingHandlers: Map<string, () => Promise<void>>;
5
5
  runtimeComponentsWatcher: chokidar.FSWatcher | null;
6
6
  runtimeConfigurationWatcher: chokidar.FSWatcher | null;
7
- componentDirectoriesWatcher: chokidar.FSWatcher | null;
8
- /** Watches entries.patterns parent directories for newly created pattern subdirectories. */
9
- patternDirectoriesWatcher: chokidar.FSWatcher | null;
7
+ /** Watches every registered catalog directory for newly created item subdirectories. */
8
+ catalogDirectoriesWatcher: chokidar.FSWatcher | null;
10
9
  }
11
10
  /**
12
11
  * Schedules a handler for execution with mutual exclusion and per-source