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
@@ -17,11 +17,21 @@ const fs_extra_1 = __importDefault(require("fs-extra"));
17
17
  const path_1 = __importDefault(require("path"));
18
18
  const url_1 = require("../../../artifacts/url");
19
19
  const previews_1 = require("./previews");
20
+ /**
21
+ * Strip the fields that must not reach the written artifact.
22
+ *
23
+ * The docs API serves this artifact publicly, and publish uploads it verbatim. An absolute path in
24
+ * the artifact exposes the filesystem layout of the build machine, so this drops the workspace
25
+ * locations `path` and `entries`. The build takes those locations from the runtime config, and
26
+ * publish remaps `entries` onto the registry record.
27
+ */
20
28
  const sanitizeComponentApiData = (component) => {
21
29
  if (!component)
22
30
  return component;
23
31
  const sanitized = Object.assign(Object.assign({}, component), { previews: (0, previews_1.getDocumentedPreviews)(component.previews) });
24
32
  delete sanitized.internalPatternPreviews;
33
+ delete sanitized.entries;
34
+ delete sanitized.path;
25
35
  return sanitized;
26
36
  };
27
37
  exports.sanitizeComponentApiData = sanitizeComponentApiData;
@@ -126,21 +136,7 @@ const readComponentMetadataApi = (handoff, id) => __awaiter(void 0, void 0, void
126
136
  return null;
127
137
  }
128
138
  // Construct the summary from the full component data
129
- return {
130
- id,
131
- title: componentData.title,
132
- description: componentData.description,
133
- type: componentData.type,
134
- group: componentData.group,
135
- image: componentData.image ? componentData.image : '',
136
- figma: componentData.figma ? componentData.figma : '',
137
- figmaComponentId: componentData.figmaComponentId,
138
- categories: componentData.categories ? componentData.categories : [],
139
- tags: componentData.tags ? componentData.tags : [],
140
- properties: componentData.properties,
141
- previews: (0, previews_1.getDocumentedPreviews)(componentData.previews),
142
- path: (0, url_1.buildComponentDetailUrl)(id, (_a = process.env.HANDOFF_APP_BASE_PATH) !== null && _a !== void 0 ? _a : ''),
143
- };
139
+ return Object.assign(Object.assign({ id, title: componentData.title, description: componentData.description, type: componentData.type, group: componentData.group, image: componentData.image ? componentData.image : '', figma: componentData.figma ? componentData.figma : '', figmaComponentId: componentData.figmaComponentId, categories: componentData.categories ? componentData.categories : [], tags: componentData.tags ? componentData.tags : [], properties: componentData.properties, previews: (0, previews_1.getDocumentedPreviews)(componentData.previews), path: (0, url_1.buildComponentDetailUrl)(id, (_a = process.env.HANDOFF_APP_BASE_PATH) !== null && _a !== void 0 ? _a : '') }, (componentData.renderer ? { renderer: componentData.renderer } : {})), (componentData.sourceFormat ? { sourceFormat: componentData.sourceFormat } : {}));
144
140
  });
145
141
  exports.readComponentMetadataApi = readComponentMetadataApi;
146
142
  exports.default = exports.writeComponentApi;
@@ -1,4 +1,4 @@
1
1
  import Handoff from '../../../index';
2
2
  export declare const getComponentApiPath: (handoff: Handoff) => string;
3
3
  export declare const removeComponentApi: (handoff: Handoff, id: string) => Promise<void>;
4
- export declare const syncComponentArtifacts: (handoff: Handoff) => Promise<void>;
4
+ export declare const syncComponentArtifacts: (handoff: Handoff, removedIds?: readonly string[]) => Promise<void>;
@@ -26,8 +26,8 @@ const CLIENT_ARTIFACT_SUFFIX = '.client.js';
26
26
  const getComponentApiPath = (handoff) => path_1.default.resolve((0, api_1.getAPIPath)(handoff), 'component');
27
27
  exports.getComponentApiPath = getComponentApiPath;
28
28
  const getComponentPreviewKeys = (handoff, componentId) => __awaiter(void 0, void 0, void 0, function* () {
29
- var _a, _b, _c;
30
- const runtimeComponent = (_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 ? void 0 : _c[componentId];
29
+ var _a;
30
+ const runtimeComponent = (_a = handoff.runtimeConfig) === null || _a === void 0 ? void 0 : _a.entities.components[componentId];
31
31
  const previewKeys = new Set();
32
32
  const outputFilePath = path_1.default.resolve((0, exports.getComponentApiPath)(handoff), `${componentId}.json`);
33
33
  if (fs_extra_1.default.existsSync(outputFilePath)) {
@@ -40,7 +40,7 @@ const getComponentPreviewKeys = (handoff, componentId) => __awaiter(void 0, void
40
40
  }
41
41
  }
42
42
  }
43
- catch (_d) {
43
+ catch (_b) {
44
44
  // Fall back to runtime config below.
45
45
  }
46
46
  }
@@ -71,14 +71,18 @@ const removeComponentApi = (handoff, id) => __awaiter(void 0, void 0, void 0, fu
71
71
  }
72
72
  });
73
73
  exports.removeComponentApi = removeComponentApi;
74
- const syncComponentArtifacts = (handoff) => __awaiter(void 0, void 0, void 0, function* () {
75
- var _a, _b, _c;
74
+ const syncComponentArtifacts = (handoff_1, ...args_1) => __awaiter(void 0, [handoff_1, ...args_1], void 0, function* (handoff, removedIds = []) {
75
+ var _a, _b;
76
76
  const componentPath = (0, exports.getComponentApiPath)(handoff);
77
77
  yield fs_extra_1.default.ensureDir(componentPath);
78
- 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 : {};
78
+ const runtimeComponents = (_b = (_a = handoff.runtimeConfig) === null || _a === void 0 ? void 0 : _a.entities.components) !== null && _b !== void 0 ? _b : {};
79
79
  const runtimeIds = Object.keys(runtimeComponents);
80
80
  const entries = yield fs_extra_1.default.readdir(componentPath);
81
- const discoveredArtifactIds = new Set(runtimeIds);
81
+ // `getArtifactComponentId` prunes a preview file only when it can name the owner. A deleted
82
+ // component is gone from the runtime config, and its `.json` is already removed, so nothing can
83
+ // match it. Naming it here restores the match without making its files valid. Every artifact it
84
+ // owns is then pruned, including the `__pattern_*` previews that the `.json` does not list.
85
+ const discoveredArtifactIds = new Set([...runtimeIds, ...removedIds]);
82
86
  for (const entry of entries) {
83
87
  const parsed = path_1.default.parse(entry);
84
88
  if (parsed.ext === '.json' || parsed.ext === '.js' || parsed.ext === '.css') {
@@ -71,9 +71,17 @@ var ComponentSegment;
71
71
  ComponentSegment["Previews"] = "previews";
72
72
  ComponentSegment["Validation"] = "validation";
73
73
  })(ComponentSegment || (exports.ComponentSegment = ComponentSegment = {}));
74
+ /**
75
+ * A source format reads its previews out of the source file. A placeholder there is a preview
76
+ * nothing can render: the docs would offer a "Default" entry with no artifact. Only a renderer
77
+ * that renders whatever previews it is given needs the placeholder.
78
+ */
79
+ const ownsItsPreviews = (data) => !!(data === null || data === void 0 ? void 0 : data.sourceFormat);
74
80
  const ensureDefaultPreview = (data) => {
75
81
  if (!data)
76
82
  return;
83
+ if (ownsItsPreviews(data))
84
+ return;
77
85
  if (!data.previews || Object.keys(data.previews).length === 0) {
78
86
  data.previews = {
79
87
  default: {
@@ -155,7 +163,7 @@ function processComponents(handoff, id, segmentToProcess, options) {
155
163
  const documentationObject = yield handoff.getDocumentationObject();
156
164
  const components = (_a = documentationObject === null || documentationObject === void 0 ? void 0 : documentationObject.components) !== null && _a !== void 0 ? _a : {};
157
165
  // Resolve the component set through the storage-agnostic store (v2). The filesystem store is a
158
- // read view over `runtimeConfig.entries.components`, so these are the same records the build has
166
+ // read view over `runtimeConfig.entities.components`, so these are the same records the build has
159
167
  // always used — rebuilt into the id-keyed map the build logic below expects.
160
168
  const runtimeComponents = {};
161
169
  for (const component of yield handoff.store.components.list()) {
@@ -166,7 +174,7 @@ function processComponents(handoff, id, segmentToProcess, options) {
166
174
  if (id && !runtimeComponents[id]) {
167
175
  yield (0, artifacts_1.removeComponentApi)(handoff, id);
168
176
  yield (0, summary_1.removeComponentFromSummaryApi)(handoff, id);
169
- yield (0, artifacts_1.syncComponentArtifacts)(handoff);
177
+ yield (0, artifacts_1.syncComponentArtifacts)(handoff, [id]);
170
178
  return [];
171
179
  }
172
180
  // Determine which components need building based on cache (when enabled)
@@ -268,7 +276,7 @@ function processComponents(handoff, id, segmentToProcess, options) {
268
276
  // If this is NOT a figma component, add the default generic preview.
269
277
  // We add it here (before merge) so that if the user explicitly provided previews in 'restMetadata',
270
278
  // those will override this default (standard "config overrides defaults" behavior).
271
- if (!restMetadata.figmaComponentId) {
279
+ if (!restMetadata.figmaComponentId && !ownsItsPreviews(restMetadata)) {
272
280
  componentDefaults.previews = {
273
281
  default: {
274
282
  title: 'Default',
@@ -296,12 +304,14 @@ function processComponents(handoff, id, segmentToProcess, options) {
296
304
  data.css = existingData.css;
297
305
  data.sass = existingData.sass;
298
306
  }
299
- // If we're not building previews, preserve pre-existing HTML, code snippet, usage, and previews.
307
+ // If we're not building previews, preserve pre-existing HTML, code snippet, usage, previews,
308
+ // and the code language they were rendered in.
300
309
  if (!buildPlan.previews) {
301
310
  data.html = existingData.html;
302
311
  data.code = existingData.code;
303
312
  data.usage = existingData.usage;
304
313
  data.previews = existingData.previews;
314
+ data.format = existingData.format;
305
315
  }
306
316
  /**
307
317
  * Always keep validation results from the previous data,
@@ -396,19 +406,6 @@ function processComponents(handoff, id, segmentToProcess, options) {
396
406
  */
397
407
  const buildComponentSummary = (id, data) => {
398
408
  var _a;
399
- return {
400
- id,
401
- title: data.title,
402
- description: data.description,
403
- type: data.type,
404
- group: data.group,
405
- image: data.image ? data.image : '',
406
- figma: data.figma ? data.figma : '',
407
- categories: data.categories ? data.categories : [],
408
- tags: data.tags ? data.tags : [],
409
- properties: data.properties,
410
- previews: (0, previews_1.getDocumentedPreviews)(data.previews),
411
- path: (0, url_1.buildComponentDetailUrl)(id, (_a = process.env.HANDOFF_APP_BASE_PATH) !== null && _a !== void 0 ? _a : ''),
412
- };
409
+ return Object.assign(Object.assign({ id, title: data.title, description: data.description, type: data.type, group: data.group, image: data.image ? data.image : '', figma: data.figma ? data.figma : '', categories: data.categories ? data.categories : [], tags: data.tags ? data.tags : [], properties: data.properties, previews: (0, previews_1.getDocumentedPreviews)(data.previews), path: (0, url_1.buildComponentDetailUrl)(id, (_a = process.env.HANDOFF_APP_BASE_PATH) !== null && _a !== void 0 ? _a : '') }, (data.renderer ? { renderer: data.renderer } : {})), (data.sourceFormat ? { sourceFormat: data.sourceFormat } : {}));
413
410
  };
414
411
  exports.default = processComponents;
@@ -44,6 +44,8 @@ declare const buildComponentCss: (data: TransformComponentTokensResult, handoff:
44
44
  templates?: string;
45
45
  };
46
46
  renderer?: import("../../../index").RendererKind;
47
+ sourceFormat?: import("../../../index").SourceFormat;
48
+ componentExport?: string;
47
49
  options?: {
48
50
  preview?: {
49
51
  groupBy?: string;
@@ -16,7 +16,6 @@ exports.buildMainCss = exports.SHARED_COMPONENT_CSS_FILE = exports.MAIN_COMPONEN
16
16
  const fs_extra_1 = __importDefault(require("fs-extra"));
17
17
  const path_1 = __importDefault(require("path"));
18
18
  const vite_1 = require("vite");
19
- const config_1 = require("../../../config");
20
19
  const duration_1 = require("../../../utils/duration");
21
20
  const logger_1 = require("../../../utils/logger");
22
21
  const vite_config_1 = __importDefault(require("../../vite-config"));
@@ -163,14 +162,14 @@ const buildComponentCss = (data, handoff) => __awaiter(void 0, void 0, void 0, f
163
162
  * Build the main CSS file using Vite
164
163
  */
165
164
  const buildMainCss = (handoff) => __awaiter(void 0, void 0, void 0, function* () {
166
- var _a, _b, _c;
165
+ var _a, _b;
167
166
  const outputPath = (0, component_1.getComponentOutputPath)(handoff);
168
167
  const mainCssPath = path_1.default.resolve(outputPath, exports.MAIN_COMPONENT_CSS_FILE);
169
- const runtimeConfig = (0, config_1.initRuntimeConfig)(handoff)[0];
170
- if (((_a = runtimeConfig === null || runtimeConfig === void 0 ? void 0 : runtimeConfig.entries) === null || _a === void 0 ? void 0 : _a.scss) && fs_extra_1.default.existsSync(runtimeConfig.entries.scss)) {
171
- const stat = yield fs_extra_1.default.stat(runtimeConfig.entries.scss);
172
- const entryPath = stat.isDirectory() ? path_1.default.resolve(runtimeConfig.entries.scss, 'main.scss') : runtimeConfig.entries.scss;
173
- if (entryPath === runtimeConfig.entries.scss || fs_extra_1.default.existsSync(entryPath)) {
168
+ const scssEntry = (_b = (_a = handoff.runtimeConfig) === null || _a === void 0 ? void 0 : _a.entries) === null || _b === void 0 ? void 0 : _b.scss;
169
+ if (scssEntry && fs_extra_1.default.existsSync(scssEntry)) {
170
+ const stat = yield fs_extra_1.default.stat(scssEntry);
171
+ const entryPath = stat.isDirectory() ? path_1.default.resolve(scssEntry, 'main.scss') : scssEntry;
172
+ if (entryPath === scssEntry || fs_extra_1.default.existsSync(entryPath)) {
174
173
  logger_1.Logger.info(`Building styles for global entry (${exports.MAIN_COMPONENT_CSS_FILE})…`);
175
174
  const startedAt = Date.now();
176
175
  // Drop the previous artifact so a failed/empty rebuild cannot preserve stale global CSS that
@@ -178,13 +177,11 @@ const buildMainCss = (handoff) => __awaiter(void 0, void 0, void 0, function* ()
178
177
  yield fs_extra_1.default.remove(mainCssPath);
179
178
  try {
180
179
  const loadPaths = [
180
+ path_1.default.dirname(scssEntry),
181
181
  path_1.default.resolve(handoff.workingPath),
182
182
  path_1.default.resolve(handoff.workingPath, handoff.exportsDirectory, handoff.getProjectId()),
183
183
  path_1.default.resolve(handoff.workingPath, 'node_modules'),
184
184
  ];
185
- if ((_c = (_b = handoff.runtimeConfig) === null || _b === void 0 ? void 0 : _b.entries) === null || _c === void 0 ? void 0 : _c.scss) {
186
- loadPaths.unshift(path_1.default.dirname(handoff.runtimeConfig.entries.scss));
187
- }
188
185
  yield buildCssBundle({
189
186
  entry: entryPath,
190
187
  outputPath,
@@ -194,7 +191,7 @@ const buildMainCss = (handoff) => __awaiter(void 0, void 0, void 0, function* ()
194
191
  });
195
192
  logger_1.Logger.info(`Finished building styles for global entry (${exports.MAIN_COMPONENT_CSS_FILE}) in ${(0, duration_1.formatDurationMs)(Date.now() - startedAt)}`);
196
193
  }
197
- catch (_d) {
194
+ catch (_c) {
198
195
  // buildCssBundle already logs failure for the entry path; ensure no stale artifact remains.
199
196
  yield fs_extra_1.default.remove(mainCssPath);
200
197
  }
@@ -19,15 +19,17 @@ const logger_1 = require("../../../utils/logger");
19
19
  const plugins_1 = require("../../plugins");
20
20
  const vite_config_1 = __importDefault(require("../../vite-config"));
21
21
  const component_1 = require("../component");
22
- const resolveRenderer = (data) => {
23
- var _a, _b, _c;
24
- const templatePath = ((_a = data.entries) === null || _a === void 0 ? void 0 : _a.template) || '';
25
- return (data.renderer ||
26
- (((_b = data.entries) === null || _b === void 0 ? void 0 : _b.story) || templatePath.match(/\.stories\.(jsx|tsx|js|ts)$/) ? 'csf' : undefined) ||
27
- (((_c = data.entries) === null || _c === void 0 ? void 0 : _c.component) || (templatePath.includes('.tsx') && !templatePath.match(/\.stories\.(jsx|tsx|js|ts)$/))
28
- ? 'react'
29
- : undefined) ||
30
- (templatePath.includes('.hbs') ? 'handlebars' : undefined));
22
+ /**
23
+ * Exactly one entry builds an item. A source format claims it before its renderer, because a CSF
24
+ * item is also a React item and both plugins would write their own previews. The maps are
25
+ * exhaustive, so a new renderer fails to compile until its previews are built.
26
+ */
27
+ const FORMAT_PLUGINS = {
28
+ csf: (data, components, handoff) => [(0, plugins_1.csfRenderPlugin)(data, components, handoff)],
29
+ };
30
+ const RENDERER_PLUGINS = {
31
+ handlebars: (data, components, handoff) => [(0, plugins_1.handlebarsPreviewsPlugin)(data, components, handoff)],
32
+ react: (data, components, handoff) => [(0, plugin_react_1.default)(), (0, plugins_1.ssrRenderPlugin)(data, components, handoff)],
31
33
  };
32
34
  /**
33
35
  * Builds previews for components using Vite and Handlebars.
@@ -47,13 +49,13 @@ const buildPreviews = (data, handoff, components) => __awaiter(void 0, void 0, v
47
49
  var _a, _b, _c;
48
50
  if (!((_a = data.entries) === null || _a === void 0 ? void 0 : _a.template))
49
51
  return data;
50
- const resolvedRenderer = resolveRenderer(data);
51
- const plugins = [
52
- ...(vite_config_1.default.plugins || []),
53
- ...(resolvedRenderer === 'handlebars' ? [(0, plugins_1.handlebarsPreviewsPlugin)(data, components, handoff)] : []),
54
- ...(resolvedRenderer === 'csf' ? [(0, plugins_1.csfRenderPlugin)(data, components, handoff)] : []),
55
- ...(resolvedRenderer === 'react' ? [(0, plugin_react_1.default)(), (0, plugins_1.ssrRenderPlugin)(data, components, handoff)] : []),
56
- ];
52
+ const { renderer, sourceFormat } = data;
53
+ const previewPlugins = sourceFormat ? FORMAT_PLUGINS[sourceFormat] : RENDERER_PLUGINS[renderer];
54
+ if (!previewPlugins) {
55
+ logger_1.Logger.error(`No preview support for renderer "${renderer}"${sourceFormat ? ` and source format "${sourceFormat}"` : ''}: ${data.entries.template}`);
56
+ return data;
57
+ }
58
+ const plugins = [...(vite_config_1.default.plugins || []), ...previewPlugins(data, components, handoff)];
57
59
  // Store the current NODE_ENV value before vite build
58
60
  // This is necessary because viteBuild forcibly sets NODE_ENV to 'production'
59
61
  // which can cause issues with subsequent Next.js operations that rely on
@@ -16,7 +16,6 @@ exports.buildMainJS = exports.buildComponentJs = exports.MAIN_COMPONENT_JS_FILE
16
16
  const fs_extra_1 = __importDefault(require("fs-extra"));
17
17
  const path_1 = __importDefault(require("path"));
18
18
  const vite_1 = require("vite");
19
- const config_1 = require("../../../config");
20
19
  const duration_1 = require("../../../utils/duration");
21
20
  const logger_1 = require("../../../utils/logger");
22
21
  const vite_config_1 = __importDefault(require("../../vite-config"));
@@ -135,11 +134,10 @@ exports.buildComponentJs = buildComponentJs;
135
134
  * @returns A Promise that resolves when the build process is complete
136
135
  */
137
136
  const buildMainJS = (handoff) => __awaiter(void 0, void 0, void 0, function* () {
138
- var _a;
137
+ var _a, _b;
139
138
  const outputPath = (0, component_1.getComponentOutputPath)(handoff);
140
139
  const mainJsPath = path_1.default.resolve(outputPath, exports.MAIN_COMPONENT_JS_FILE);
141
- const runtimeConfig = (0, config_1.initRuntimeConfig)(handoff)[0];
142
- const entry = (_a = runtimeConfig === null || runtimeConfig === void 0 ? void 0 : runtimeConfig.entries) === null || _a === void 0 ? void 0 : _a.js;
140
+ const entry = (_b = (_a = handoff.runtimeConfig) === null || _a === void 0 ? void 0 : _a.entries) === null || _b === void 0 ? void 0 : _b.js;
143
141
  // No usable global JS entry: drop any stale artifact and emit nothing to reference.
144
142
  if (!entry || !fs_extra_1.default.existsSync(path_1.default.resolve(entry))) {
145
143
  if (entry) {
@@ -25,54 +25,70 @@ const html_1 = require("./html");
25
25
  * Returns pattern ids whose declarations reference any of the given component ids.
26
26
  */
27
27
  const getPatternIdsReferencingComponents = (handoff, componentIds) => {
28
- var _a, _b, _c, _d;
28
+ var _a, _b, _c;
29
29
  if (componentIds.length === 0) {
30
30
  return [];
31
31
  }
32
32
  const idSet = new Set(componentIds);
33
- const runtimePatterns = (_c = (_b = (_a = handoff.runtimeConfig) === null || _a === void 0 ? void 0 : _a.entries) === null || _b === void 0 ? void 0 : _b.patterns) !== null && _c !== void 0 ? _c : {};
33
+ const runtimePatterns = (_b = (_a = handoff.runtimeConfig) === null || _a === void 0 ? void 0 : _a.entities.patterns) !== null && _b !== void 0 ? _b : {};
34
34
  const result = [];
35
35
  for (const patternId of Object.keys(runtimePatterns)) {
36
36
  const pattern = runtimePatterns[patternId];
37
- if ((_d = pattern.components) === null || _d === void 0 ? void 0 : _d.some((ref) => idSet.has(ref.id))) {
37
+ if ((_c = pattern.components) === null || _c === void 0 ? void 0 : _c.some((ref) => idSet.has(ref.id))) {
38
38
  result.push(patternId);
39
39
  }
40
40
  }
41
41
  return result;
42
42
  };
43
43
  exports.getPatternIdsReferencingComponents = getPatternIdsReferencingComponents;
44
+ /** Collapses repeats into one item with a count, so one cause reads as one problem. */
45
+ const summarizeReasons = (reasons) => {
46
+ var _a;
47
+ const counts = new Map();
48
+ for (const reason of reasons) {
49
+ counts.set(reason, ((_a = counts.get(reason)) !== null && _a !== void 0 ? _a : 0) + 1);
50
+ }
51
+ return Array.from(counts, ([reason, count]) => (count > 1 ? `${reason} (x${count})` : reason)).join('; ');
52
+ };
44
53
  function buildPattern(handoff, patternId, pattern, componentOutputDir, basePath, sharedArtifacts) {
45
54
  return __awaiter(this, void 0, void 0, function* () {
55
+ var _a, _b, _c;
46
56
  const fragments = [];
47
- let hasErrors = false;
48
- for (let i = 0; i < pattern.components.length; i++) {
49
- const ref = pattern.components[i];
57
+ // Composition is what drops a ref, so it is also what reports why. Discovery recorded its reason
58
+ // without logging it, and that reason is more precise than the one this loop can build.
59
+ const skipped = [];
60
+ const composedWithFallback = [];
61
+ for (const ref of pattern.components) {
50
62
  if (ref.resolved === false && !ref.resolvedPreview) {
51
- hasErrors = true;
63
+ skipped.push((_a = ref.unresolvedReason) !== null && _a !== void 0 ? _a : `component "${ref.id}" did not resolve`);
52
64
  continue;
53
65
  }
54
66
  const previewKey = ref.resolvedPreview || ref.preview;
55
67
  if (!previewKey) {
56
- const error = `Pattern "${patternId}" component[${i}] ("${ref.id}") has no resolved preview key. Skipping.`;
57
- logger_1.Logger.warn(error);
68
+ skipped.push((_b = ref.unresolvedReason) !== null && _b !== void 0 ? _b : `component "${ref.id}" has no preview to render`);
58
69
  ref.resolved = false;
59
- hasErrors = true;
60
70
  continue;
61
71
  }
62
72
  const htmlFileName = `${ref.id}-${previewKey}.html`;
63
73
  const htmlFilePath = path_1.default.resolve(componentOutputDir, htmlFileName);
64
74
  if (!fs_extra_1.default.existsSync(htmlFilePath)) {
65
- const error = `Pattern "${patternId}" component[${i}] ("${ref.id}") preview file not found: ${htmlFileName}. ` +
66
- `Ensure the component has a preview named "${previewKey}".`;
67
- logger_1.Logger.warn(error);
75
+ skipped.push((_c = ref.unresolvedReason) !== null && _c !== void 0 ? _c : `preview "${previewKey}" of component "${ref.id}" was not built (${htmlFileName})`);
68
76
  ref.resolved = false;
69
- hasErrors = true;
70
77
  continue;
71
78
  }
72
79
  ref.resolved = true;
80
+ if (ref.unresolvedReason) {
81
+ composedWithFallback.push(ref.unresolvedReason);
82
+ }
73
83
  const html = yield fs_extra_1.default.readFile(htmlFilePath, 'utf8');
74
84
  fragments.push({ componentId: ref.id, html });
75
85
  }
86
+ if (skipped.length > 0) {
87
+ logger_1.Logger.warn(`Pattern "${patternId}" skipped ${skipped.length} of ${pattern.components.length} fragment(s): ${summarizeReasons(skipped)}.`);
88
+ }
89
+ if (composedWithFallback.length > 0) {
90
+ logger_1.Logger.warn(`Pattern "${patternId}" composed ${composedWithFallback.length} fragment(s) from a fallback: ${summarizeReasons(composedWithFallback)}.`);
91
+ }
76
92
  // Resolve component-owned artifact presence for each composed component so the pattern references
77
93
  // only the stylesheets that actually exist (no dangling references for style-less components).
78
94
  const componentArtifacts = new Map();
@@ -85,9 +101,6 @@ function buildPattern(handoff, patternId, pattern, componentOutputDir, basePath,
85
101
  logger_1.Logger.warn(`Pattern "${patternId}" produced no fragments. Skipping.`);
86
102
  return null;
87
103
  }
88
- if (hasErrors) {
89
- logger_1.Logger.warn(`Pattern "${patternId}" has missing components but will be composed from available fragments.`);
90
- }
91
104
  const composedHtml = (0, html_1.composePatternHtml)(patternId, pattern.title, fragments, basePath, sharedArtifacts, componentArtifacts);
92
105
  const patternUrl = `${patternId}.html`;
93
106
  const patternData = Object.assign(Object.assign({}, pattern), { id: patternId, url: patternUrl, path: (0, url_1.buildPatternDetailUrl)(patternId, basePath) });
@@ -106,7 +119,7 @@ function processPatterns(handoff, options) {
106
119
  return __awaiter(this, void 0, void 0, function* () {
107
120
  var _a;
108
121
  // Resolve the pattern set through the storage-agnostic store (v2). The filesystem store is a read
109
- // view over `runtimeConfig.entries.patterns`, so these are the same records the build has always
122
+ // view over `runtimeConfig.entities.patterns`, so these are the same records the build has always
110
123
  // used — rebuilt into the id-keyed map the build logic below expects.
111
124
  const runtimePatterns = {};
112
125
  for (const pattern of yield handoff.store.patterns.list()) {
@@ -1,6 +1,6 @@
1
1
  import type { GeneratedDocs } from 'handoff-docgen';
2
2
  import { Card } from '../../app/components/Component/Cards';
3
- import type { RendererKind } from '../../declarations/types';
3
+ import type { RendererKind, SourceFormat } from '../../catalog/renderers';
4
4
  import { ValidationResult } from '../../types/preview';
5
5
  import { Filter } from '../../utils/filter';
6
6
  import { SlotMetadata } from './component';
@@ -105,6 +105,13 @@ export type ComponentObject = {
105
105
  };
106
106
  /** Optional explicit renderer id */
107
107
  renderer?: RendererKind;
108
+ /**
109
+ * Implementation source format, such as CSF. The separate `format` field describes the preview code language.
110
+ * These names must differ because the build spreads this record over the preview.
111
+ */
112
+ sourceFormat?: SourceFormat;
113
+ /** Export of `entries.component` that holds the implementation. Defaults to the default export. */
114
+ componentExport?: string;
108
115
  /** Schema describing the expected properties (props/slots) for the component */
109
116
  properties: {
110
117
  [key: string]: SlotMetadata;
@@ -113,6 +120,8 @@ export type ComponentObject = {
113
120
  previews: {
114
121
  [key: string]: OptionalPreviewRender;
115
122
  };
123
+ /** Source export order retained when publishing previews through jsonb storage. */
124
+ previewOrder?: string[];
116
125
  /** Internal previews generated only for pattern composition, not public docs */
117
126
  internalPatternPreviews?: {
118
127
  [key: string]: OptionalPreviewRender;
@@ -180,6 +189,9 @@ export type TransformComponentTokensResult = {
180
189
  templates?: string;
181
190
  };
182
191
  renderer?: RendererKind;
192
+ /** See `ComponentObject.sourceFormat`. Distinct from `format`, the preview's code language. */
193
+ sourceFormat?: SourceFormat;
194
+ componentExport?: string;
183
195
  options?: {
184
196
  preview?: {
185
197
  groupBy?: string;
@@ -213,6 +225,8 @@ export type PatternComponentEntry = {
213
225
  resolvedPreview?: string;
214
226
  /** Whether the pattern entry resolved to a usable component preview */
215
227
  resolved?: boolean;
228
+ /** Why discovery could not resolve this entry as authored. The pattern build reports it. */
229
+ unresolvedReason?: string;
216
230
  };
217
231
  export type PatternObject = {
218
232
  title: string;
@@ -98,22 +98,20 @@ export interface ConfigEntries {
98
98
  * @example "scripts/main.js"
99
99
  */
100
100
  js?: string;
101
+ }
102
+ /** Catalog item registration. */
103
+ export interface ConfigCatalog {
101
104
  /**
102
- * Array of component paths to be included in the build
103
- * @example ["components/button", "components/input"]
104
- */
105
- components?: string[];
106
- /**
107
- * Array of pattern paths to be included in the build.
108
- * Patterns compose multiple component previews into single-page views.
109
- * @example ["patterns/hero-section", "patterns"]
105
+ * Directories to search for catalog items. A path is either an item directory or a collection
106
+ * directory whose subdirectories are each treated as an item.
107
+ * @example ["components", "patterns"]
110
108
  */
111
- patterns?: string[];
109
+ include?: string[];
112
110
  }
113
111
  /** Runtime mode. Resolved solely from `runtime.mode`; never inferred from env vars or connection settings. */
114
112
  export type RuntimeMode = 'workspace' | 'registry';
115
113
  /** Format used when synthesizing local workspace declarations. */
116
- export type DeclarationFormat = 'ts' | 'js' | 'cjs' | 'json';
114
+ export type DeclarationFormat = 'ts' | 'js' | 'cjs';
117
115
  /**
118
116
  * User-facing `runtime` configuration block. A single optional block that selects the runtime
119
117
  * mode and carries mode-specific host/connection settings.
@@ -127,6 +125,13 @@ export interface HandoffRuntimeConfig {
127
125
  * Mode is never inferred from environment variables, database settings, or token presence.
128
126
  */
129
127
  mode?: RuntimeMode;
128
+ /**
129
+ * Whether the app serves the `/api/mcp/` Model Context Protocol endpoint. Set to `false` to leave
130
+ * the endpoint and the header affordance out of the build; the route then answers 404. Like
131
+ * `mode`, this is config-only and baked at build time, so changing it needs a rebuild.
132
+ * @default true
133
+ */
134
+ mcp?: boolean;
130
135
  /** Workspace-mode settings. */
131
136
  workspace?: {
132
137
  entries?: ConfigEntries;
@@ -220,6 +225,10 @@ export interface Config {
220
225
  * Configuration for entry points to assets and components that will be built
221
226
  */
222
227
  entries?: ConfigEntries;
228
+ /**
229
+ * Where catalog items are registered.
230
+ */
231
+ catalog?: ConfigCatalog;
223
232
  /**
224
233
  * Override URLs for the asset zip download links. When unset, each link defaults to the
225
234
  * basePath-aware asset route `{basePath}/api/docs/assets/{collection}/{collection}.zip`, served by
@@ -397,11 +406,22 @@ export interface ConfigFileEntry {
397
406
  kind: string;
398
407
  entityId: string;
399
408
  }
409
+ /** A catalog declaration skipped because another declaration already took its id. */
410
+ export interface DuplicateCatalogId {
411
+ id: string;
412
+ /** Absolute path of the declaration that owns the id. */
413
+ kept: string;
414
+ /** Absolute path of the declaration that was skipped. */
415
+ skipped: string;
416
+ }
400
417
  export interface RuntimeConfig {
418
+ /** Resolved absolute paths of the project-wide bundle entry points, from `config.entries`. */
401
419
  entries?: {
402
420
  scss?: string;
403
421
  js?: string;
404
- templates?: string;
422
+ };
423
+ /** Entities discovered from the workspace, keyed by id. Always present, possibly empty. */
424
+ entities: {
405
425
  components: {
406
426
  [id: string]: ComponentListObject;
407
427
  };
@@ -412,6 +432,11 @@ export interface RuntimeConfig {
412
432
  [id: string]: PageListObject;
413
433
  };
414
434
  };
435
+ /**
436
+ * Catalog declarations skipped because their id was already taken. Always present, possibly empty.
437
+ * Discovery is the only pass that sees both declarations, so publish reads the duplicates from here.
438
+ */
439
+ duplicateCatalogIds: DuplicateCatalogId[];
415
440
  options: {
416
441
  [key: string]: RuntimeConfigComponentOptions;
417
442
  };
package/docs/api.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # Handoff API
2
2
 
3
+ Register implementations and compositions with `defineCatalogItem`. See the
4
+ [catalog examples](../README.md#catalog-items) and [version 1.x.x to version 2.x.x migration guide](../UPGRADE.md#version-1xx-to-version-2xx).
5
+
3
6
  Handoff exposes two APIs - A Javascript API for interacting with the pipeline
4
7
  Handoff exposes a Javascript API allowing you to easily integrate Handoff into
5
8
  existing Node based applications, CI/CD, and command line tools. This API also
package/docs/cli.md CHANGED
@@ -63,7 +63,7 @@ Commands:
63
63
  logout [--url <registry-url>] - Revokes the saved token when possible, then removes the local credential.
64
64
 
65
65
  make
66
- make:template <component> <state> [opts] - Creates a new template
66
+ make:component <name> [opts] - Creates a new catalog item with a declaration
67
67
  make:page <component> <state> [opts] - Creates a new page
68
68
 
69
69
  eject - Ejects the default entire configuration to the current directory
@@ -0,0 +1,3 @@
1
+ ALTER TABLE "components" ADD COLUMN "source_format" text;--> statement-breakpoint
2
+ -- Backfill from the authoritative record so components published before this migration get a value too.
3
+ UPDATE "components" SET "source_format" = "record"->>'sourceFormat' WHERE "record"->>'sourceFormat' IS NOT NULL;