handoff-app 2.0.0-6 → 2.0.0-8

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 (233) hide show
  1. package/README.md +240 -52
  2. package/UPGRADE.md +218 -0
  3. package/config/config.template.js +1 -4
  4. package/config/config.template.ts +1 -2
  5. package/dist/app-builder/build.js +12 -6
  6. package/dist/app-builder/client-config.js +3 -1
  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 +70 -61
  12. package/dist/cache/build-cache.js +23 -23
  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/auth/device.d.ts +6 -1
  27. package/dist/cli/auth/device.js +5 -2
  28. package/dist/cli/auth/store.d.ts +15 -6
  29. package/dist/cli/auth/store.js +72 -19
  30. package/dist/cli/make.d.ts +0 -5
  31. package/dist/cli/make.js +22 -67
  32. package/dist/cli/scaffold.js +25 -33
  33. package/dist/commands/checkout/index.d.ts +7 -4
  34. package/dist/commands/checkout/index.js +9 -6
  35. package/dist/commands/compat/push-args.d.ts +2 -2
  36. package/dist/commands/index.js +0 -2
  37. package/dist/commands/login/index.d.ts +5 -0
  38. package/dist/commands/login/index.js +18 -6
  39. package/dist/commands/logout/index.d.ts +5 -0
  40. package/dist/commands/logout/index.js +54 -15
  41. package/dist/commands/publish/index.d.ts +9 -6
  42. package/dist/commands/publish/index.js +11 -8
  43. package/dist/commands/pull/index.js +1 -1
  44. package/dist/commands/push/index.js +1 -1
  45. package/dist/commands/types.d.ts +2 -0
  46. package/dist/commands/utils.d.ts +36 -11
  47. package/dist/commands/utils.js +30 -16
  48. package/dist/config/catalog-include.d.ts +43 -0
  49. package/dist/config/catalog-include.js +203 -0
  50. package/dist/config/defaults.d.ts +3 -3
  51. package/dist/config/defaults.js +54 -48
  52. package/dist/config/env.d.ts +9 -0
  53. package/dist/config/env.js +43 -0
  54. package/dist/config/errors.d.ts +4 -0
  55. package/dist/config/errors.js +11 -0
  56. package/dist/config/from-env.d.ts +24 -0
  57. package/dist/config/from-env.js +7 -0
  58. package/dist/config/helpers.d.ts +2 -0
  59. package/dist/config/helpers.js +3 -1
  60. package/dist/config/index.d.ts +4 -3
  61. package/dist/config/index.js +10 -5
  62. package/dist/config/loader.d.ts +29 -11
  63. package/dist/config/loader.js +126 -45
  64. package/dist/config/resolve-env.d.ts +5 -0
  65. package/dist/config/resolve-env.js +102 -0
  66. package/dist/config/runtime.d.ts +7 -8
  67. package/dist/config/runtime.js +154 -154
  68. package/dist/config/validator.d.ts +2 -2
  69. package/dist/config/validator.js +2 -4
  70. package/dist/handlebars.d.ts +9 -0
  71. package/dist/handlebars.js +16 -0
  72. package/dist/index.d.ts +48 -8
  73. package/dist/index.js +88 -30
  74. package/dist/pattern.d.ts +7 -0
  75. package/dist/pattern.js +13 -0
  76. package/dist/pipeline/index.d.ts +1 -1
  77. package/dist/pipeline/index.js +1 -1
  78. package/dist/react.d.ts +43 -0
  79. package/dist/react.js +11 -0
  80. package/dist/registry/asset-storage/adapters/vercel-blob.js +1 -1
  81. package/dist/registry/asset-storage/define.d.ts +1 -1
  82. package/dist/registry/asset-storage/define.js +1 -1
  83. package/dist/registry/asset-storage/resolve.d.ts +2 -2
  84. package/dist/registry/asset-storage/resolve.js +4 -1
  85. package/dist/registry/checkout/index.d.ts +9 -0
  86. package/dist/registry/checkout/index.js +339 -153
  87. package/dist/registry/connection.d.ts +28 -24
  88. package/dist/registry/connection.js +53 -67
  89. package/dist/registry/db/driver.d.ts +4 -11
  90. package/dist/registry/db/driver.js +4 -16
  91. package/dist/registry/db/index.d.ts +1 -1
  92. package/dist/registry/db/index.js +1 -2
  93. package/dist/registry/db/schema.d.ts +34 -0
  94. package/dist/registry/db/schema.js +2 -0
  95. package/dist/registry/publish/index.d.ts +13 -2
  96. package/dist/registry/publish/index.js +113 -29
  97. package/dist/registry/publish/package.js +14 -1
  98. package/dist/registry/selection.d.ts +20 -0
  99. package/dist/registry/selection.js +37 -1
  100. package/dist/store/filesystem.d.ts +3 -3
  101. package/dist/store/filesystem.js +7 -7
  102. package/dist/store/registry.js +24 -3
  103. package/dist/transformers/plugins/csf-render.js +4 -2
  104. package/dist/transformers/plugins/ssr-render.js +14 -5
  105. package/dist/transformers/preview/component/api.d.ts +8 -0
  106. package/dist/transformers/preview/component/api.js +11 -15
  107. package/dist/transformers/preview/component/artifacts.d.ts +1 -1
  108. package/dist/transformers/preview/component/artifacts.js +11 -7
  109. package/dist/transformers/preview/component/builder.js +15 -18
  110. package/dist/transformers/preview/component/css.d.ts +2 -0
  111. package/dist/transformers/preview/component/css.js +8 -11
  112. package/dist/transformers/preview/component/html.js +18 -16
  113. package/dist/transformers/preview/component/javascript.js +2 -4
  114. package/dist/transformers/preview/pattern/builder.js +31 -18
  115. package/dist/transformers/preview/types.d.ts +15 -1
  116. package/dist/types/config.d.ts +77 -40
  117. package/docs/api.md +34 -0
  118. package/docs/cli.md +3 -3
  119. package/drizzle/0006_add_component_source_format.sql +3 -0
  120. package/drizzle/meta/0006_snapshot.json +1941 -0
  121. package/drizzle/meta/_journal.json +8 -1
  122. package/package.json +34 -1
  123. package/src/app/components/Component/Preview.tsx +36 -50
  124. package/src/app/components/Markdown/CodeHighlight.tsx +73 -70
  125. package/src/app/components/context/PreviewContext.tsx +5 -5
  126. package/src/app/components/util/index.ts +7 -15
  127. package/src/app/lib/mcp/shape.ts +6 -5
  128. package/src/app/lib/registry-api/transfer.ts +7 -5
  129. package/src/app/pages/account/tokens.tsx +3 -1
  130. package/src/app-builder/build.ts +13 -8
  131. package/src/app-builder/client-config.ts +5 -3
  132. package/src/app-builder/config-diff/strategies/pattern.ts +1 -1
  133. package/src/app-builder/watchers/component.ts +3 -3
  134. package/src/app-builder/watchers/utils.ts +2 -3
  135. package/src/app-builder/watchers.ts +63 -57
  136. package/src/cache/build-cache.ts +16 -15
  137. package/src/catalog/README.md +92 -0
  138. package/src/catalog/define.ts +79 -0
  139. package/src/catalog/implementation.ts +145 -0
  140. package/src/catalog/index.ts +31 -0
  141. package/src/catalog/normalize.ts +143 -0
  142. package/src/catalog/previews.ts +111 -0
  143. package/src/catalog/renderers.ts +94 -0
  144. package/src/catalog/types.ts +74 -0
  145. package/src/cli/README.md +1 -1
  146. package/src/cli/auth/device.ts +8 -3
  147. package/src/cli/auth/store.ts +75 -18
  148. package/src/cli/make.ts +20 -72
  149. package/src/cli/scaffold.ts +25 -31
  150. package/src/commands/README.md +1 -1
  151. package/src/commands/checkout/index.ts +17 -7
  152. package/src/commands/compat/push-args.ts +2 -2
  153. package/src/commands/index.ts +0 -2
  154. package/src/commands/login/index.ts +20 -6
  155. package/src/commands/logout/index.ts +74 -20
  156. package/src/commands/publish/index.ts +19 -9
  157. package/src/commands/pull/index.ts +2 -2
  158. package/src/commands/push/index.ts +2 -2
  159. package/src/commands/types.ts +2 -0
  160. package/src/commands/utils.ts +36 -12
  161. package/src/config/README.md +63 -2
  162. package/src/config/catalog-include.ts +227 -0
  163. package/src/config/defaults.ts +24 -14
  164. package/src/config/env.ts +41 -0
  165. package/src/config/errors.ts +7 -0
  166. package/src/config/from-env.ts +26 -0
  167. package/src/config/helpers.ts +4 -2
  168. package/src/config/index.ts +7 -4
  169. package/src/config/loader.ts +168 -50
  170. package/src/config/resolve-env.ts +94 -0
  171. package/src/config/runtime.ts +188 -193
  172. package/src/config/validator.ts +4 -6
  173. package/src/handlebars.ts +18 -0
  174. package/src/index.d.ts +2 -2
  175. package/src/index.ts +124 -34
  176. package/src/pattern.ts +14 -0
  177. package/src/pipeline/index.ts +1 -1
  178. package/src/react.ts +52 -0
  179. package/src/registry/asset-storage/adapters/vercel-blob.ts +1 -1
  180. package/src/registry/asset-storage/define.ts +1 -1
  181. package/src/registry/asset-storage/resolve.ts +6 -3
  182. package/src/registry/checkout/index.ts +421 -157
  183. package/src/registry/connection.ts +72 -71
  184. package/src/registry/db/driver.ts +6 -17
  185. package/src/registry/db/index.ts +0 -1
  186. package/src/registry/db/schema.ts +2 -0
  187. package/src/registry/publish/index.ts +128 -31
  188. package/src/registry/publish/package.ts +14 -1
  189. package/src/registry/selection.ts +44 -0
  190. package/src/store/filesystem.ts +6 -6
  191. package/src/store/registry.ts +13 -2
  192. package/src/transformers/plugins/csf-render.ts +4 -2
  193. package/src/transformers/plugins/ssr-render.ts +15 -5
  194. package/src/transformers/preview/component/api.ts +13 -1
  195. package/src/transformers/preview/component/artifacts.ts +8 -4
  196. package/src/transformers/preview/component/builder.ts +17 -4
  197. package/src/transformers/preview/component/css.ts +6 -10
  198. package/src/transformers/preview/component/html.ts +30 -19
  199. package/src/transformers/preview/component/javascript.ts +1 -3
  200. package/src/transformers/preview/pattern/builder.ts +33 -20
  201. package/src/transformers/preview/types.ts +15 -1
  202. package/src/types/config.ts +76 -40
  203. package/templates/default/components/button/button.react.handoff.js.tpl +19 -23
  204. package/templates/default/components/button/button.react.handoff.ts.tpl +22 -22
  205. package/templates/default/env.tpl +10 -0
  206. package/templates/default/gitignore.tpl +7 -0
  207. package/templates/default/handoff.config.js.tpl +21 -182
  208. package/templates/default/handoff.config.ts.tpl +21 -166
  209. package/templates/default/handoff.config.with-examples.js.tpl +22 -180
  210. package/templates/default/handoff.config.with-examples.ts.tpl +22 -164
  211. package/config/templates/component/template.json +0 -49
  212. package/config/templates/template.html +0 -7
  213. package/dist/commands/make/template.d.ts +0 -8
  214. package/dist/commands/make/template.js +0 -43
  215. package/dist/config/entries.d.ts +0 -47
  216. package/dist/config/entries.js +0 -162
  217. package/dist/config/normalizers/declaration.d.ts +0 -11
  218. package/dist/config/normalizers/declaration.js +0 -122
  219. package/dist/config/normalizers/pattern.d.ts +0 -8
  220. package/dist/config/normalizers/pattern.js +0 -41
  221. package/dist/declarations/helpers.d.ts +0 -6
  222. package/dist/declarations/helpers.js +0 -19
  223. package/dist/declarations/index.d.ts +0 -2
  224. package/dist/declarations/index.js +0 -9
  225. package/dist/declarations/types.d.ts +0 -60
  226. package/src/commands/make/template.ts +0 -46
  227. package/src/config/entries.ts +0 -175
  228. package/src/config/normalizers/declaration.ts +0 -179
  229. package/src/config/normalizers/pattern.ts +0 -59
  230. package/src/declarations/helpers.ts +0 -36
  231. package/src/declarations/index.ts +0 -19
  232. package/src/declarations/types.ts +0 -70
  233. /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
 
@@ -274,12 +359,12 @@ runtime variables listed above are supplied through the deployment environment.
274
359
 
275
360
  #### Neon PostgreSQL
276
361
 
277
- The Neon connection driver is selected in `handoff.config.ts`:
362
+ Import `fromEnv` from `handoff-app`. Select the Neon connection driver in `handoff.config.ts`:
278
363
 
279
364
  ```ts
280
365
  runtime: {
281
366
  registry: {
282
- databaseUrlEnv: 'DATABASE_URL',
367
+ databaseUrl: fromEnv('DATABASE_URL'),
283
368
  database: {
284
369
  driver: 'neon',
285
370
  },
@@ -319,6 +404,12 @@ should not be left unattended before installation is complete.
319
404
 
320
405
  ### 4. Workspace authorization
321
406
 
407
+ A workspace reaches a registry with a registry URL and an access token. There
408
+ are two ways to supply them. Both use the same kind of token, and both are
409
+ listed and revoked under Account → Access tokens in the registry.
410
+
411
+ #### Device login
412
+
322
413
  CLI authorization is started from the source workspace with:
323
414
 
324
415
  ```bash
@@ -326,9 +417,48 @@ npm run login -- --url http://localhost:4000
326
417
  ```
327
418
 
328
419
  Registry sign-in, entry of the displayed device code, and CLI approval are
329
- completed in the browser. The revocable credential is saved in
420
+ completed in the browser. The issued credential is saved in
330
421
  `.handoff/cli-auth.json` for that exact registry URL.
331
422
 
423
+ One credential is saved per profile, so a workspace can stay signed in to
424
+ several registries at the same time:
425
+
426
+ ```bash
427
+ npm run login -- --profile staging --url https://staging.example.com
428
+ npm run publish -- all --profile staging
429
+ ```
430
+
431
+ A login profile name is free. It needs no `handoff.config.<name>.*` file, and
432
+ `login` is the only command that accepts a new name. A profile without its own
433
+ login falls back to the default login, so one `npm run login` is still enough
434
+ for a workspace with one registry.
435
+
436
+ `npm run logout` revokes and removes the login of the selected profile. The
437
+ `--all` option removes every saved login.
438
+
439
+ #### Environment variables
440
+
441
+ A token is created in the registry under Account → Access tokens. Read and
442
+ write access is required to publish; read access is enough to check out. The
443
+ token and the registry URL are then set in `.env`:
444
+
445
+ ```dotenv
446
+ HANDOFF_REGISTRY_URL=http://localhost:4000
447
+ HANDOFF_REGISTRY_ACCESS_TOKEN=hnd_...
448
+ ```
449
+
450
+ No configuration file entry is needed, because these two variables are the
451
+ defaults for `runtime.registryConnection`. A selected profile reads
452
+ `.env.<profile>` on top of `.env`, so one workspace can address a different
453
+ registry per profile. In CI, the job environment supplies the same two
454
+ variables instead of a file.
455
+
456
+ Environment values win over a saved device login, so a CI job stays
457
+ deterministic on a machine where a developer is signed in. A token is only used
458
+ for the registry URL it was issued for. If the environment names a different
459
+ registry than the login of the selected profile, publish and checkout report
460
+ which login was skipped and why.
461
+
332
462
  ### 5. Content publishing
333
463
 
334
464
  Every kind in the workspace is published in dependency order with:
@@ -340,20 +470,23 @@ npm run publish -- all
340
470
  A single kind is published on its own:
341
471
 
342
472
  ```bash
343
- npm run publish -- components
344
- npm run publish -- patterns
473
+ npm run publish -- catalog
345
474
  npm run publish -- pages
346
475
  npm run publish -- tokens
347
476
  npm run publish -- assets
348
477
  ```
349
478
 
479
+ `catalog` covers every catalog item. An item is stored as a component or as a
480
+ pattern, and its declaration decides which. The command names items and never a
481
+ kind.
482
+
350
483
  Publishing tokens or assets runs the Figma data pipeline before upload, so the
351
484
  documented Figma credentials must be available.
352
485
 
353
486
  One or more IDs can be appended to narrow a publish to those entities:
354
487
 
355
488
  ```bash
356
- npm run publish -- components component-id another-id
489
+ npm run publish -- catalog item-id another-id
357
490
  ```
358
491
 
359
492
  `--dry-run` reports what would be uploaded and contacts no registry at all, so
@@ -363,24 +496,29 @@ the existing output, and the two combine to leave the workspace untouched:
363
496
 
364
497
  ```bash
365
498
  npm run publish -- all --dry-run
366
- npm run publish -- components --no-build
499
+ npm run publish -- catalog --no-build
367
500
  ```
368
501
 
369
502
  `checkout` takes the same `all`, multi-ID, and `--dry-run` forms. A dry-run
370
503
  checkout reads from the registry, lists the files it would create or overwrite,
371
504
  and writes nothing.
372
505
 
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.
506
+ After the registry is reloaded, the published catalog items and foundations are
507
+ visible. Published database records are read by registry pages; the local
508
+ workspace is never read directly.
376
509
 
377
- For CI, a registry connection and user-issued token are configured:
510
+ For CI, supply `HANDOFF_REGISTRY_URL` and `HANDOFF_REGISTRY_ACCESS_TOKEN`
511
+ through the job environment, as described under
512
+ [Workspace authorization](#4-workspace-authorization). A connection block in
513
+ `handoff.config.ts` is needed only to pin the URL in the repository, or to read
514
+ the values from differently named variables (`fromEnv` is imported from
515
+ `handoff-app`):
378
516
 
379
517
  ```ts
380
518
  runtime: {
381
519
  registryConnection: {
382
520
  url: 'https://registry.example.com',
383
- accessTokenEnv: 'HANDOFF_REGISTRY_ACCESS_TOKEN',
521
+ accessToken: fromEnv('HANDOFF_REGISTRY_ACCESS_TOKEN'),
384
522
  },
385
523
  },
386
524
  ```
@@ -433,17 +571,19 @@ for Claude Code, Cursor and VS Code behind the plug icon in the header. By hand:
433
571
  ## Configuration
434
572
 
435
573
  Configuration is read from `handoff.config.ts`, `.js`, `.cjs`, or `.json`, in
436
- that order. `defineConfig` provides typed authoring. A partial `runtime` block
437
- is deep-merged with defaults.
574
+ that order. `defineConfig` provides typed authoring. Values merge onto the
575
+ defaults: plain objects merge recursively, and arrays, scalars, `null`, and
576
+ functions replace.
438
577
 
439
578
  Useful environment variables:
440
579
 
441
580
  | Variable | Purpose |
442
581
  | --- | --- |
582
+ | `HANDOFF_PROFILE` | Config profile merged onto the base config |
443
583
  | `HANDOFF_FIGMA_PROJECT_ID` | Figma file ID used by `fetch` |
444
584
  | `HANDOFF_DEV_ACCESS_TOKEN` | Figma personal access token used by `fetch` |
445
585
  | `HANDOFF_REGISTRY_URL` | Connected workspace registry URL |
446
- | `HANDOFF_REGISTRY_ACCESS_TOKEN` | User-issued CI token |
586
+ | `HANDOFF_REGISTRY_ACCESS_TOKEN` | Registry access token used by a connected workspace |
447
587
  | `HANDOFF_SYNC_SECRET` | Optional deployment-wide registry credential |
448
588
  | `DATABASE_URL` | Registry PostgreSQL connection string |
449
589
  | `AUTH_SECRET` | Registry session-signing secret, at least 32 characters |
@@ -462,6 +602,52 @@ Vercel Blob or a custom adapter can be selected through
462
602
  selected through `runtime.registry.database.driver`; PostgreSQL is used by
463
603
  both.
464
604
 
605
+ ## Profiles
606
+
607
+ A profile is a sidecar config file that merges onto the base config. It holds
608
+ only what changes between environments, so a project keeps one shared config
609
+ instead of a second complete copy of it.
610
+
611
+ - The base config is `handoff.config.ts`, `.js`, `.cjs`, or `.json`. It is the
612
+ only config file loaded when no profile is selected.
613
+ - A profile file is `handoff.config.<profile>.*`, with the same four
614
+ extensions.
615
+ - Select a profile with `--profile <name>` or with `HANDOFF_PROFILE`.
616
+ `--profile` takes precedence over `HANDOFF_PROFILE`.
617
+ - Profile names are not predefined. A name can contain lowercase letters,
618
+ numbers, and hyphens.
619
+ - A selected profile must exist. If no `handoff.config.<name>.*` file is found,
620
+ the command stops with an error.
621
+ - `login`, `logout`, `publish`, and `checkout` also accept a profile that has
622
+ only a saved registry login, because there the profile selects the registry.
623
+ Every other command still needs the config file.
624
+ - Layers resolve as defaults, base config, profile, then programmatic config,
625
+ with the merge rules described under [Configuration](#configuration).
626
+ - A selected profile also reads `.env.<profile>` on top of `.env`, when that
627
+ file exists. Both are read from the directory the command runs in, and a
628
+ variable already set in the environment beats both files.
629
+
630
+ ```bash
631
+ npx handoff-app build --target registry --profile registry
632
+ ```
633
+
634
+ `build --target registry` always packages a registry-mode application, so a
635
+ profile does not need to set `runtime.mode`. This makes a profile a good place
636
+ for the registry build settings: the database driver, the database
637
+ environment-variable name, the asset storage, and whether MCP is enabled. A
638
+ hosting provider or a CI job selects the profile through `HANDOFF_PROFILE`.
639
+
640
+ `defineConfig` types a profile as well as a base config. The generated
641
+ `.gitignore` lists `handoff.config.local.*`, which makes `local` the usual name
642
+ for a profile that stays on one machine.
643
+
644
+ Arrays replace, so a profile that declares `catalog.include` also decides what
645
+ `make` and `checkout` register.
646
+
647
+ `.env.<profile>` is read once at startup rather than watched.
648
+ `HANDOFF_WORKING_PATH` cannot be set from it, because the working path is
649
+ resolved before any profile is known.
650
+
465
651
  ## CLI reference
466
652
 
467
653
  | Command | Description |
@@ -473,12 +659,14 @@ both.
473
659
  | `npm run build -- [--target static\|registry]` | The static site or standalone registry bundle is built |
474
660
  | `npm run db:migrate` | Registry database migrations are applied |
475
661
  | `npm run validate` | Configured components are validated |
476
- | `npm run publish -- <kind\|all> [id...]` | Components, patterns, pages, tokens, or assets are published |
662
+ | `npm run publish -- <kind\|all> [id...]` | Catalog items, pages, tokens, or assets are published |
477
663
  | `npm run checkout -- <kind\|all> [id...]` | Published content is pulled into a workspace |
478
- | `npm run login -- --url <url>` | The CLI is authorized through the registry device flow |
479
- | `npm run logout -- [--url <url>]` | A saved CLI credential is revoked and removed |
664
+ | `npm run login -- [--profile <name>] --url <url>` | The CLI is authorized through the registry device flow |
665
+ | `npm run logout -- [--profile <name>] [--all]` | Saved CLI credentials are revoked and removed |
480
666
 
481
- `publish` and `checkout` additionally accept `--dry-run`, and `publish` accepts `--no-build`.
667
+ Every command except `init` accepts `-c, --config`, `--profile`, `-d, --debug`,
668
+ and `-f, --force`. `publish` and `checkout` additionally accept `--dry-run`, and
669
+ `publish` accepts `--no-build`.
482
670
 
483
671
  Arguments after `--` are forwarded to the local CLI. Exact options can be shown
484
672
  by adding `--help` after the separator. Advanced configuration and hooks are
package/UPGRADE.md ADDED
@@ -0,0 +1,218 @@
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
+ | [Figma token environment references](#figma-token-environment-references) | Projects that put a Figma token directly in config |
22
+ | [Catalog items](#catalog-items) | Projects with component or pattern declarations |
23
+
24
+ <!-- Add future breaking changes as feature sections before "Verify the upgrade". Add each section to this table. -->
25
+
26
+ ### Figma token environment references
27
+
28
+ `devAccessToken` and its legacy spelling `dev_access_token` now accept only an environment reference.
29
+ A literal token throws `HandoffConfigError`, including in JavaScript and JSON configs.
30
+ Move the token to the environment and replace the literal:
31
+
32
+ ```ts
33
+ import { defineConfig, fromEnv } from 'handoff-app';
34
+
35
+ export default defineConfig({
36
+ devAccessToken: fromEnv('HANDOFF_DEV_ACCESS_TOKEN'),
37
+ });
38
+ ```
39
+
40
+ For JSON, use `"devAccessToken": { "$env": "HANDOFF_DEV_ACCESS_TOKEN" }`.
41
+ If you already use `HANDOFF_DEV_ACCESS_TOKEN` and omit the config property, no change is required.
42
+ Environment references resolve after profile merging. Higher-layer literals still override default
43
+ environment seeds for properties that allow literals, such as output directories.
44
+
45
+ ### Catalog items
46
+
47
+ #### Affected projects
48
+
49
+ This change affects projects that use the old component or pattern registration API.
50
+ It also affects projects that use the deprecated API from an earlier version 2.x.x prerelease.
51
+ Version 2.x.x requires `defineCatalogItem` for implementations and compositions.
52
+
53
+ #### Register catalog directories
54
+
55
+ Move component and pattern directories from `entries` to `catalog.include`.
56
+
57
+ Version 1.x.x:
58
+
59
+ ```js
60
+ entries: { components: ['components'], patterns: ['patterns'], scss: 'styles/main.scss' }
61
+ ```
62
+
63
+ Version 2.x.x:
64
+
65
+ ```js
66
+ catalog: { include: ['components', 'patterns'] },
67
+ entries: { scss: 'styles/main.scss' }
68
+ ```
69
+
70
+ Each path can name one item directory or a collection of item directories. Global `entries.js` and `entries.scss` still work.
71
+
72
+ #### Convert declaration files
73
+
74
+ Use `<name>.handoff.ts`, `<name>.handoff.js`, or `<name>.handoff.cjs`.
75
+ Replace legacy filenames such as `button.json` or `button.js`.
76
+
77
+ Version 2.x.x does not support JSON item declarations, including `.handoff.json`.
78
+ JSON project config (`handoff.config.json`) remains supported.
79
+
80
+ If `runtime.workspace.declarationFormat` is `json`, change it to `ts`, `js`, or `cjs`. The default remains `ts`.
81
+
82
+ #### Replace declaration helpers
83
+
84
+ | Removed API | Replacement |
85
+ | --- | --- |
86
+ | `defineReactComponent(Component, config)` | `defineCatalogItem({ ...config, implementation: Component })` from `handoff-app/react` |
87
+ | `defineHandlebarsComponent(config)` | `defineCatalogItem({ ...config, implementation: './Button.hbs' })` from `handoff-app/handlebars` |
88
+ | `defineCsfComponent(config)` | `defineCatalogItem({ ...config, implementation: fromCSF('./Button.stories.tsx') })` from `handoff-app/react` |
89
+ | `defineComponent(config)` | `defineCatalogItem({ ...config, implementation: './Button.tsx' })` from the module for its renderer |
90
+ | `definePattern({ components })` | `defineCatalogItem({ composition })` from `handoff-app/pattern` |
91
+
92
+ Remove the old `renderer`, `entries.component`, `entries.template`, and `entries.story` fields.
93
+ The implementation supplies the renderer and source file.
94
+ Keep the supporting `entries.js`, `entries.scss`, `entries.schema`, and `entries.templates` paths that your item uses.
95
+ Rename the metadata fields:
96
+
97
+ | Old field | Version 2.x.x field |
98
+ | --- | --- |
99
+ | `title` | `name` |
100
+ | `should_do` | `shouldDo` |
101
+ | `should_not_do` | `shouldNotDo` |
102
+
103
+ ##### React
104
+
105
+ ```ts
106
+ import { defineCatalogItem, type Preview } from 'handoff-app/react';
107
+ import Button from './Button';
108
+
109
+ const item = defineCatalogItem({
110
+ name: 'Button',
111
+ implementation: Button,
112
+ entries: { scss: './button.scss' },
113
+ });
114
+ export default item;
115
+ export const Primary = { args: { children: 'Save' } } satisfies Preview<typeof item>;
116
+ ```
117
+
118
+ Imported components retain their prop types. A file path also works: `implementation: './Button.tsx'`.
119
+ Keep implementation files inside the item directory to publish them.
120
+
121
+ ##### Handlebars and CommonJS
122
+
123
+ ```js
124
+ const { defineCatalogItem } = require('handoff-app/handlebars');
125
+
126
+ exports.default = defineCatalogItem({
127
+ name: 'Badge',
128
+ implementation: './Badge.hbs',
129
+ });
130
+ exports.Primary = { args: { children: 'New' } };
131
+ ```
132
+
133
+ Use this form in `.handoff.js` or `.handoff.cjs`. JavaScript declarations can also use `import` and `export`.
134
+
135
+ ##### CSF
136
+
137
+ ```ts
138
+ import { defineCatalogItem, fromCSF } from 'handoff-app/react';
139
+
140
+ export default defineCatalogItem({
141
+ name: 'Button',
142
+ implementation: fromCSF('./Button.stories.tsx'),
143
+ });
144
+ ```
145
+
146
+ CSF is a React source format. The story file supplies the previews.
147
+ The filename does not select CSF automatically.
148
+
149
+ ##### Compositions
150
+
151
+ ```ts
152
+ import { defineCatalogItem } from 'handoff-app/pattern';
153
+
154
+ export default defineCatalogItem({
155
+ name: 'Toolbar',
156
+ composition: [{ ref: 'button', preview: 'Primary', args: { children: 'Save' } }],
157
+ });
158
+ ```
159
+
160
+ Rename `components` to `composition`.
161
+ Rename each member's `id` to `ref`.
162
+ Keep item IDs stable.
163
+
164
+ #### Export previews
165
+
166
+ Remove the inline `previews` map.
167
+ Export each preview beside the default catalog item.
168
+ Rename preview `title` to `name`.
169
+ Rename `values` to `args`:
170
+
171
+ ```ts
172
+ export const Primary = { name: 'Primary action', args: { children: 'Save' } };
173
+ ```
174
+
175
+ If a preview key is a valid export name, preserve its spelling and capitalization.
176
+ If a key is not a valid export name, rename it.
177
+ Update composition references to each renamed preview.
178
+
179
+ Previews appear in source export order.
180
+
181
+ For CommonJS declarations, use `exports.default` for the item.
182
+ Use a named export such as `exports.Primary` for each preview.
183
+
184
+ #### Replace declaration types
185
+
186
+ Replace imports of the removed types:
187
+
188
+ | Removed type | Replacement |
189
+ | --- | --- |
190
+ | `Component` | `CatalogItem` |
191
+ | `DeclarationPreview` | `CatalogPreview` or `Preview<typeof item>` |
192
+ | `PatternComponentRef` | `CompositionRef` |
193
+ | `ReactDeclarationConfig`, `HandlebarsDeclarationConfig`, `CsfDeclarationConfig` | The catalog helper's input type |
194
+ | `GenericDeclarationConfig`, `GenericPatternDeclarationConfig` | The catalog helper's input type, or `CatalogItemMeta` for shared metadata |
195
+
196
+ `RendererKind` comes from the catalog API and contains `react` and `handlebars`.
197
+ CSF uses `renderer: 'react'` with `format: 'csf'` inside `implementation`.
198
+
199
+ #### Registry projects
200
+
201
+ Checkout writes only catalog declarations.
202
+ Registry records require explicit renderer and source-format metadata where applicable.
203
+
204
+ ### Verify the upgrade
205
+
206
+ After you apply the required migrations, verify the project:
207
+
208
+ 1. Build the project with version 2.x.x.
209
+ 2. Verify that all expected catalog items appear in the documentation.
210
+ 3. Verify the preview order, arguments, and composition references.
211
+ 4. Start the workspace with file watchers.
212
+ 5. Edit a declaration to verify that its previews update.
213
+
214
+ If the project uses a registry, verify the published items:
215
+
216
+ 1. Publish the migrated items.
217
+ 2. Verify that checkout produces valid declarations.
218
+ 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,