handoff-app 1.1.3 → 1.2.0-1

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 (160) hide show
  1. package/config/config.template.js +21 -14
  2. package/config/config.template.ts +54 -0
  3. package/config/docs/system.md +2 -0
  4. package/dist/app-builder/build.js +31 -12
  5. package/dist/app-builder/config-diff/index.d.ts +4 -0
  6. package/dist/app-builder/config-diff/index.js +12 -0
  7. package/dist/app-builder/config-diff/registry.d.ts +9 -0
  8. package/dist/app-builder/config-diff/registry.js +30 -0
  9. package/dist/app-builder/config-diff/snapshot.d.ts +13 -0
  10. package/dist/app-builder/config-diff/snapshot.js +43 -0
  11. package/dist/app-builder/config-diff/strategies/pattern.d.ts +2 -0
  12. package/dist/app-builder/config-diff/strategies/pattern.js +106 -0
  13. package/dist/app-builder/config-diff/types.d.ts +33 -0
  14. package/dist/app-builder/config-diff/types.js +2 -0
  15. package/dist/app-builder/watchers/component.d.ts +15 -0
  16. package/dist/app-builder/watchers/component.js +69 -0
  17. package/dist/app-builder/watchers/utils.d.ts +14 -0
  18. package/dist/app-builder/watchers/utils.js +44 -0
  19. package/dist/app-builder/watchers.d.ts +10 -19
  20. package/dist/app-builder/watchers.js +65 -79
  21. package/dist/app-builder/websocket.js +10 -4
  22. package/dist/cache/build-cache.js +14 -5
  23. package/dist/cli/create.js +67 -23
  24. package/dist/cli/eject.js +48 -7
  25. package/dist/cli/make.js +29 -4
  26. package/dist/cli/scaffold.js +60 -19
  27. package/dist/cli.js +1 -1
  28. package/dist/config/helpers.d.ts +7 -0
  29. package/dist/config/helpers.js +31 -0
  30. package/dist/config/index.d.ts +2 -1
  31. package/dist/config/index.js +5 -1
  32. package/dist/config/loader.d.ts +10 -1
  33. package/dist/config/loader.js +92 -28
  34. package/dist/config/normalizers/declaration.d.ts +11 -0
  35. package/dist/config/normalizers/declaration.js +119 -0
  36. package/dist/config/normalizers/pattern.d.ts +8 -0
  37. package/dist/config/normalizers/pattern.js +35 -0
  38. package/dist/config/runtime.d.ts +9 -3
  39. package/dist/config/runtime.js +260 -33
  40. package/dist/declarations/helpers.d.ts +6 -0
  41. package/dist/declarations/helpers.js +19 -0
  42. package/dist/declarations/index.d.ts +2 -0
  43. package/dist/declarations/index.js +9 -0
  44. package/dist/declarations/types.d.ts +60 -0
  45. package/dist/declarations/types.js +2 -0
  46. package/dist/index.d.ts +12 -1
  47. package/dist/index.js +34 -5
  48. package/dist/pipeline/index.d.ts +1 -0
  49. package/dist/pipeline/index.js +3 -1
  50. package/dist/pipeline/patterns.d.ts +8 -0
  51. package/dist/pipeline/patterns.js +21 -0
  52. package/dist/transformers/docgen/index.js +12 -2
  53. package/dist/transformers/plugins/csf-render.js +170 -34
  54. package/dist/transformers/plugins/ssr-render.js +18 -2
  55. package/dist/transformers/preview/component/api.js +16 -7
  56. package/dist/transformers/preview/component/builder.js +61 -8
  57. package/dist/transformers/preview/component/css.d.ts +8 -0
  58. package/dist/transformers/preview/component/css.js +6 -4
  59. package/dist/transformers/preview/component/html.js +14 -5
  60. package/dist/transformers/preview/component/javascript.d.ts +0 -1
  61. package/dist/transformers/preview/component/javascript.js +25 -14
  62. package/dist/transformers/preview/component/previews.d.ts +9 -0
  63. package/dist/transformers/preview/component/previews.js +10 -0
  64. package/dist/transformers/preview/pattern/api.d.ts +11 -0
  65. package/dist/transformers/preview/pattern/api.js +79 -0
  66. package/dist/transformers/preview/pattern/builder.d.ts +17 -0
  67. package/dist/transformers/preview/pattern/builder.js +167 -0
  68. package/dist/transformers/preview/pattern/html.d.ts +19 -0
  69. package/dist/transformers/preview/pattern/html.js +94 -0
  70. package/dist/transformers/preview/types.d.ts +54 -0
  71. package/dist/transformers/utils/csf.d.ts +16 -0
  72. package/dist/transformers/utils/csf.js +30 -0
  73. package/dist/transformers/utils/index.d.ts +1 -0
  74. package/dist/transformers/utils/index.js +2 -0
  75. package/dist/transformers/utils/module.d.ts +5 -0
  76. package/dist/transformers/utils/module.js +26 -13
  77. package/dist/transformers/utils/usage.d.ts +23 -0
  78. package/dist/transformers/utils/usage.js +294 -0
  79. package/dist/transformers/utils/vite-logger.js +4 -4
  80. package/dist/transformers/vite-config.js +2 -0
  81. package/dist/types/config.d.ts +32 -7
  82. package/dist/types/preview.d.ts +2 -0
  83. package/dist/utils/duration.d.ts +2 -0
  84. package/dist/utils/duration.js +6 -0
  85. package/dist/utils/logger.d.ts +42 -5
  86. package/dist/utils/logger.js +135 -15
  87. package/docs/cli.md +3 -3
  88. package/package.json +3 -1
  89. package/src/app/components/Component/PageSliceResolver.tsx +6 -6
  90. package/src/app/components/Component/Preview.tsx +17 -12
  91. package/src/app/components/Component/slices/ComponentDisplaySlice.tsx +16 -6
  92. package/src/app/components/Markdown/CodeHighlight.tsx +60 -11
  93. package/src/app/components/util/index.ts +71 -2
  94. package/src/app/pages/system/pattern/[pattern]/index.tsx +346 -0
  95. package/src/app/pages/system/pattern/index.tsx +14 -0
  96. package/src/app-builder/README.md +37 -9
  97. package/src/app-builder/build.ts +34 -12
  98. package/src/app-builder/config-diff/index.ts +4 -0
  99. package/src/app-builder/config-diff/registry.ts +23 -0
  100. package/src/app-builder/config-diff/snapshot.ts +51 -0
  101. package/src/app-builder/config-diff/strategies/pattern.ts +80 -0
  102. package/src/app-builder/config-diff/types.ts +36 -0
  103. package/src/app-builder/watchers/component.ts +72 -0
  104. package/src/app-builder/watchers/utils.ts +44 -0
  105. package/src/app-builder/watchers.ts +74 -80
  106. package/src/app-builder/websocket.ts +16 -5
  107. package/src/cache/build-cache.ts +14 -5
  108. package/src/cli/create.ts +86 -25
  109. package/src/cli/eject.ts +29 -7
  110. package/src/cli/make.ts +32 -5
  111. package/src/cli/scaffold.ts +68 -21
  112. package/src/cli.ts +1 -1
  113. package/src/config/README.md +10 -1
  114. package/src/config/helpers.ts +43 -0
  115. package/src/config/index.ts +4 -1
  116. package/src/config/loader.ts +110 -31
  117. package/src/config/normalizers/declaration.ts +176 -0
  118. package/src/config/normalizers/pattern.ts +56 -0
  119. package/src/config/runtime.ts +317 -35
  120. package/src/declarations/helpers.ts +36 -0
  121. package/src/declarations/index.ts +19 -0
  122. package/src/declarations/types.ts +70 -0
  123. package/src/index.ts +47 -6
  124. package/src/pipeline/index.ts +1 -0
  125. package/src/pipeline/patterns.ts +12 -0
  126. package/src/transformers/README.md +2 -2
  127. package/src/transformers/docgen/index.ts +13 -2
  128. package/src/transformers/plugins/csf-render.ts +216 -49
  129. package/src/transformers/plugins/ssr-render.ts +23 -7
  130. package/src/transformers/preview/component/api.ts +27 -7
  131. package/src/transformers/preview/component/builder.ts +82 -9
  132. package/src/transformers/preview/component/css.ts +6 -5
  133. package/src/transformers/preview/component/html.ts +18 -5
  134. package/src/transformers/preview/component/javascript.ts +29 -19
  135. package/src/transformers/preview/component/previews.ts +12 -0
  136. package/src/transformers/preview/pattern/api.ts +83 -0
  137. package/src/transformers/preview/pattern/builder.ts +192 -0
  138. package/src/transformers/preview/pattern/html.ts +107 -0
  139. package/src/transformers/preview/types.ts +58 -1
  140. package/src/transformers/utils/csf.ts +48 -0
  141. package/src/transformers/utils/index.ts +3 -0
  142. package/src/transformers/utils/module.ts +33 -18
  143. package/src/transformers/utils/usage.ts +319 -0
  144. package/src/transformers/utils/vite-logger.ts +4 -4
  145. package/src/transformers/vite-config.ts +2 -0
  146. package/src/types/README.md +1 -1
  147. package/src/types/config.ts +33 -7
  148. package/src/types/preview.ts +2 -0
  149. package/src/utils/duration.ts +3 -0
  150. package/src/utils/logger.ts +143 -16
  151. package/templates/default/components/button/Button.jsx.tpl +64 -0
  152. package/templates/default/components/button/button.react.handoff.js.tpl +31 -0
  153. package/templates/default/components/button/button.react.handoff.ts.tpl +31 -0
  154. package/templates/default/handoff.config.js.tpl +29 -16
  155. package/templates/default/handoff.config.ts.tpl +196 -0
  156. package/templates/default/handoff.config.with-examples.js.tpl +28 -17
  157. package/templates/default/handoff.config.with-examples.ts.tpl +195 -0
  158. package/templates/default/package.json.tpl +1 -3
  159. package/handoff.config.js +0 -90
  160. package/templates/default/components/button/button.js.tpl +0 -62
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.buildPatterns = void 0;
13
+ const builder_1 = require("../transformers/preview/pattern/builder");
14
+ /**
15
+ * Builds pattern previews by composing pre-built component preview HTML.
16
+ * Must be called AFTER buildComponents.
17
+ */
18
+ const buildPatterns = (handoff, options) => __awaiter(void 0, void 0, void 0, function* () {
19
+ yield (0, builder_1.processPatterns)(handoff, options);
20
+ });
21
+ exports.buildPatterns = buildPatterns;
@@ -225,8 +225,18 @@ exports.enrichPropertiesWithDocgen = enrichPropertiesWithDocgen;
225
225
  const generateDocsArtifact = (entry, handoff) => __awaiter(void 0, void 0, void 0, function* () {
226
226
  try {
227
227
  const tsconfigPath = path_1.default.resolve(handoff.workingPath, 'tsconfig.json');
228
- if (!fs_extra_1.default.existsSync(tsconfigPath)) {
229
- logger_1.Logger.warn(`TypeScript config not found at ${tsconfigPath}, using default resolution`);
228
+ const hasTsconfig = fs_extra_1.default.existsSync(tsconfigPath);
229
+ const entryExtension = path_1.default.extname(entry).toLowerCase();
230
+ const isJavaScriptEntry = ['.js', '.jsx', '.cjs', '.mjs'].includes(entryExtension);
231
+ if (!hasTsconfig) {
232
+ // For JS-only projects, handoff-docgen currently expects a tsconfig and will fail noisily.
233
+ // Skip docgen gracefully in this case.
234
+ if (isJavaScriptEntry) {
235
+ logger_1.Logger.debug(`Skipping handoff-docgen for ${entry}: no tsconfig.json found in ${handoff.workingPath}`);
236
+ return null;
237
+ }
238
+ logger_1.Logger.warn(`TypeScript config not found at ${tsconfigPath}; skipping docs generation for ${entry}.`);
239
+ return null;
230
240
  }
231
241
  const componentDirectory = path_1.default.dirname(path_1.default.resolve(entry));
232
242
  const { maxDepth, excludeDirectories } = getDocgenConfig(handoff);
@@ -18,13 +18,16 @@ const lodash_1 = require("lodash");
18
18
  const path_1 = __importDefault(require("path"));
19
19
  const react_1 = __importDefault(require("react"));
20
20
  const server_1 = __importDefault(require("react-dom/server"));
21
+ const react_element_to_jsx_string_1 = __importDefault(require("react-element-to-jsx-string"));
21
22
  const logger_1 = require("../../utils/logger");
23
+ const docgen_1 = require("../docgen");
22
24
  const component_1 = require("../preview/component");
23
25
  const html_1 = require("../utils/html");
24
26
  const module_1 = require("../utils/module");
25
27
  const schema_1 = require("../utils/schema");
28
+ const usage_1 = require("../utils/usage");
29
+ const csf_1 = require("../utils/csf");
26
30
  const vite_logger_1 = require("../utils/vite-logger");
27
- const docgen_1 = require("../docgen");
28
31
  const PLUGIN_CONSTANTS = {
29
32
  PLUGIN_NAME: 'vite-plugin-csf-render',
30
33
  SCRIPT_ID: 'script',
@@ -75,17 +78,6 @@ function createSlotMetadata(key, argType, value) {
75
78
  },
76
79
  };
77
80
  }
78
- function getStoryEntries(moduleExports) {
79
- return Object.entries(moduleExports).filter(([key, value]) => {
80
- if (key === 'default' || key === '__esModule')
81
- return false;
82
- if (typeof value === 'function')
83
- return true;
84
- if (value && typeof value === 'object')
85
- return true;
86
- return false;
87
- });
88
- }
89
81
  function toReactElement(renderResult, meta, args) {
90
82
  if (react_1.default.isValidElement(renderResult)) {
91
83
  return renderResult;
@@ -101,23 +93,147 @@ function toReactElement(renderResult, meta, args) {
101
93
  }
102
94
  return react_1.default.createElement('pre', null, JSON.stringify(args, null, 2));
103
95
  }
96
+ /**
97
+ * Produces the React element for a story, mirroring Storybook's resolution order:
98
+ * story render > meta render > meta.component > JSON fallback.
99
+ */
100
+ function getStoryElement(meta, story, args) {
101
+ const storyRender = typeof story === 'function' ? story : story === null || story === void 0 ? void 0 : story.render;
102
+ const render = storyRender || meta.render;
103
+ if (render) {
104
+ return toReactElement(render(args), meta, args);
105
+ }
106
+ if (meta.component) {
107
+ return react_1.default.createElement(meta.component, args);
108
+ }
109
+ return react_1.default.createElement('pre', null, JSON.stringify(args, null, 2));
110
+ }
104
111
  function safeRenderToHtml(meta, story, args) {
105
112
  try {
106
- const storyRender = typeof story === 'function' ? story : story.render;
107
- const render = storyRender || meta.render;
108
- if (render) {
109
- return server_1.default.renderToString(toReactElement(render(args), meta, args));
110
- }
111
- if (meta.component) {
112
- return server_1.default.renderToString(react_1.default.createElement(meta.component, args));
113
- }
114
- return server_1.default.renderToString(react_1.default.createElement('pre', null, JSON.stringify(args, null, 2)));
113
+ return server_1.default.renderToString(getStoryElement(meta, story, args));
115
114
  }
116
115
  catch (error) {
117
116
  logger_1.Logger.warn(`SSR render failed for story, falling back to static placeholder: ${error}`);
118
117
  return `<div style="padding:1rem;color:#b91c1c;font-family:monospace;font-size:13px">Render error: ${String(error)}</div>`;
119
118
  }
120
119
  }
120
+ /**
121
+ * Converts a story's React element into a JSX source string using the same
122
+ * approach as Storybook's Docs addon.
123
+ */
124
+ function reactElementToUsage(element) {
125
+ try {
126
+ return (0, react_element_to_jsx_string_1.default)(element, {
127
+ sortProps: false,
128
+ useBooleanShorthandSyntax: true,
129
+ useFragmentShortSyntax: true,
130
+ maxInlineAttributesLineLength: 80,
131
+ });
132
+ }
133
+ catch (error) {
134
+ logger_1.Logger.warn(`Failed to convert React element to JSX string: ${error}`);
135
+ return null;
136
+ }
137
+ }
138
+ function findMatchingBrace(source, openIndex) {
139
+ let depth = 0;
140
+ for (let i = openIndex; i < source.length; i += 1) {
141
+ const char = source[i];
142
+ if (char === '{')
143
+ depth += 1;
144
+ if (char === '}') {
145
+ depth -= 1;
146
+ if (depth === 0) {
147
+ return i;
148
+ }
149
+ }
150
+ }
151
+ return -1;
152
+ }
153
+ function extractMetaObjectBody(sourceCode) {
154
+ // TODO: Regex-based CSF meta parsing can miss complex inline `satisfies` cases.
155
+ // Move this to a TS-AST resolver in handoff-docgen and consume the normalized import info here.
156
+ const inlineMetaMatch = sourceCode.match(/export\s+default\s+({[\s\S]*?})\s*(?:satisfies\s+[^{;\n]+|as\s+[^{;\n]+)?\s*;/m);
157
+ if (inlineMetaMatch === null || inlineMetaMatch === void 0 ? void 0 : inlineMetaMatch[1]) {
158
+ return inlineMetaMatch[1];
159
+ }
160
+ const exportedMetaRef = sourceCode.match(/export\s+default\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*;/m);
161
+ if (!(exportedMetaRef === null || exportedMetaRef === void 0 ? void 0 : exportedMetaRef[1])) {
162
+ return null;
163
+ }
164
+ const metaVarName = exportedMetaRef[1];
165
+ const declarationPatterns = [
166
+ new RegExp(`(?:const|let|var)\\s+${metaVarName}\\s*=\\s*`, 'm'),
167
+ new RegExp(`(?:const|let|var)\\s+${metaVarName}\\s*=\\s*[^\\n;]*?\\b(?:satisfies|as)\\b[^\\n;]*?`, 'm'),
168
+ ];
169
+ for (const pattern of declarationPatterns) {
170
+ const match = pattern.exec(sourceCode);
171
+ if (!match)
172
+ continue;
173
+ const openIndex = sourceCode.indexOf('{', match.index);
174
+ if (openIndex === -1)
175
+ continue;
176
+ const closeIndex = findMatchingBrace(sourceCode, openIndex);
177
+ if (closeIndex === -1)
178
+ continue;
179
+ return sourceCode.slice(openIndex, closeIndex + 1);
180
+ }
181
+ return null;
182
+ }
183
+ function resolveComponentImportInfo(sourceCode) {
184
+ const metaBody = extractMetaObjectBody(sourceCode);
185
+ if (!metaBody) {
186
+ return null;
187
+ }
188
+ const componentMatch = metaBody.match(/\bcomponent\s*:\s*([A-Za-z_$][A-Za-z0-9_$]*)\b/m);
189
+ if (!(componentMatch === null || componentMatch === void 0 ? void 0 : componentMatch[1])) {
190
+ return null;
191
+ }
192
+ const localName = componentMatch[1];
193
+ const defaultImportMatch = sourceCode.match(new RegExp(`import\\s+${localName}\\s+from\\s+['"]([^'"]+)['"]`, 'm'));
194
+ if (defaultImportMatch === null || defaultImportMatch === void 0 ? void 0 : defaultImportMatch[1]) {
195
+ return {
196
+ componentName: localName,
197
+ importStatement: `import ${localName} from '${defaultImportMatch[1]}';`,
198
+ };
199
+ }
200
+ const namedImportRegex = /import\s*{\s*([^}]+)\s*}\s*from\s*['"]([^'"]+)['"]/gm;
201
+ for (const match of sourceCode.matchAll(namedImportRegex)) {
202
+ const specifiers = match[1]
203
+ .split(',')
204
+ .map((specifier) => specifier.trim())
205
+ .filter(Boolean);
206
+ const moduleSpecifier = match[2];
207
+ for (const specifier of specifiers) {
208
+ const aliasMatch = specifier.match(/^([A-Za-z_$][A-Za-z0-9_$]*)\s+as\s+([A-Za-z_$][A-Za-z0-9_$]*)$/);
209
+ if (aliasMatch && aliasMatch[2] === localName) {
210
+ return {
211
+ componentName: localName,
212
+ importStatement: `import { ${aliasMatch[1]} as ${localName} } from '${moduleSpecifier}';`,
213
+ };
214
+ }
215
+ if (specifier === localName) {
216
+ return {
217
+ componentName: localName,
218
+ importStatement: `import { ${localName} } from '${moduleSpecifier}';`,
219
+ };
220
+ }
221
+ }
222
+ }
223
+ return null;
224
+ }
225
+ function buildUsageSnippet(componentName, importStatement, element, properties, previewValues) {
226
+ const usageBody = element ? reactElementToUsage(element) : null;
227
+ if (usageBody) {
228
+ return `${importStatement}\n\n${usageBody}`;
229
+ }
230
+ return (0, usage_1.generateUsageSnippet)({
231
+ componentName,
232
+ properties,
233
+ previewValues,
234
+ importStatement,
235
+ });
236
+ }
121
237
  function createHtmlDocument(componentId, previewTitle, renderedHtml) {
122
238
  var _a, _b, _c;
123
239
  return `<!DOCTYPE html>
@@ -172,24 +288,22 @@ function csfRenderPlugin(componentData, _documentationComponents, handoff) {
172
288
  return;
173
289
  }
174
290
  const meta = (moduleExports.default || {});
175
- const stories = getStoryEntries(moduleExports);
291
+ const stories = (0, csf_1.getCsfStoryEntries)(moduleExports);
292
+ const storyMap = Object.fromEntries(stories);
176
293
  if (stories.length === 0) {
177
294
  logger_1.Logger.warn(`No named stories found in CSF file for ${componentId}: ${templatePath}`);
178
295
  return;
179
296
  }
180
297
  const explicitProperties = componentData.properties || {};
181
298
  const generatedProperties = Object.assign({}, explicitProperties);
182
- // Include all named stories as previews and infer properties from args/argTypes
183
- componentData.previews = {};
299
+ const incomingPreviews = Object.assign({}, (componentData.previews || {}));
300
+ const patternPreviews = Object.fromEntries(Object.entries(incomingPreviews).filter(([key]) => key.startsWith('__pattern_')));
301
+ // Include all named stories as previews and preserve injected pattern previews.
302
+ componentData.previews = Object.assign(Object.assign({}, (0, csf_1.createCsfStoryPreviews)(moduleExports)), patternPreviews);
184
303
  for (const [storyKey, storyValue] of stories) {
185
304
  const storyObj = (typeof storyValue === 'function' ? {} : storyValue);
186
305
  const mergedArgs = Object.assign(Object.assign({}, (meta.args || {})), (storyObj.args || {}));
187
306
  const mergedArgTypes = Object.assign(Object.assign({}, (meta.argTypes || {})), (storyObj.argTypes || {}));
188
- componentData.previews[storyKey] = {
189
- title: storyObj.name || (0, lodash_1.startCase)(storyKey),
190
- values: mergedArgs,
191
- url: '',
192
- };
193
307
  for (const [argKey, argValue] of Object.entries(mergedArgs)) {
194
308
  if (!generatedProperties[argKey]) {
195
309
  generatedProperties[argKey] = createSlotMetadata(argKey, mergedArgTypes[argKey], argValue);
@@ -204,6 +318,9 @@ function csfRenderPlugin(componentData, _documentationComponents, handoff) {
204
318
  // Enrich with docgen: descend into array/object types (e.g. cards[].image, title, link)
205
319
  const generatedDocs = yield (0, docgen_1.generateDocsArtifact)(templatePath, handoff);
206
320
  const componentName = ((_a = meta.component) === null || _a === void 0 ? void 0 : _a.displayName) || ((_b = meta.component) === null || _b === void 0 ? void 0 : _b.name);
321
+ const componentImportInfo = resolveComponentImportInfo(sourceCode);
322
+ const usageComponentName = (componentImportInfo === null || componentImportInfo === void 0 ? void 0 : componentImportInfo.componentName) || componentName || (0, usage_1.extractComponentName)(templatePath);
323
+ const usageImportStatement = (componentImportInfo === null || componentImportInfo === void 0 ? void 0 : componentImportInfo.importStatement) || `import ${usageComponentName} from './${usageComponentName}';`;
207
324
  if (generatedDocs && componentName) {
208
325
  const docgenProperties = (0, docgen_1.getPropertiesForComponentFromDocs)(generatedDocs, componentName);
209
326
  if (docgenProperties) {
@@ -242,11 +359,15 @@ function csfRenderPlugin(componentData, _documentationComponents, handoff) {
242
359
  }
243
360
  componentData.properties = (0, schema_1.ensureIds)(generatedProperties);
244
361
  let lastHtml = '';
245
- for (const [storyKey, storyValue] of stories) {
246
- const preview = componentData.previews[storyKey];
247
- const rendered = safeRenderToHtml(meta, storyValue, preview.values || {});
362
+ for (const [previewKey, preview] of Object.entries(componentData.previews)) {
363
+ const storyKey = preview.sourcePreview && storyMap[preview.sourcePreview]
364
+ ? preview.sourcePreview
365
+ : (storyMap[previewKey] ? previewKey : undefined);
366
+ const storyValue = storyKey ? storyMap[storyKey] : undefined;
367
+ const storyArgs = preview.values || {};
368
+ const rendered = safeRenderToHtml(meta, storyValue, storyArgs);
248
369
  const html = yield (0, html_1.formatHtml)(createHtmlDocument(componentId, preview.title, rendered));
249
- const fileName = `${componentId}-${storyKey}.html`;
370
+ const fileName = `${componentId}-${previewKey}.html`;
250
371
  this.emitFile({
251
372
  type: 'asset',
252
373
  fileName,
@@ -254,9 +375,18 @@ function csfRenderPlugin(componentData, _documentationComponents, handoff) {
254
375
  });
255
376
  this.emitFile({
256
377
  type: 'asset',
257
- fileName: `${componentId}-${storyKey}${PLUGIN_CONSTANTS.INSPECT_SUFFIX}.html`,
378
+ fileName: `${componentId}-${previewKey}${PLUGIN_CONSTANTS.INSPECT_SUFFIX}.html`,
258
379
  source: html,
259
380
  });
381
+ // Capture JSX usage for each story so docs can switch usage by selected preview.
382
+ let storyElement = null;
383
+ try {
384
+ storyElement = getStoryElement(meta, storyValue, storyArgs);
385
+ }
386
+ catch (_f) {
387
+ // Fall back to schema-based generation below.
388
+ }
389
+ preview.usage = buildUsageSnippet(usageComponentName, usageImportStatement, storyElement, componentData.properties || {}, storyArgs);
260
390
  preview.url = fileName;
261
391
  lastHtml = html;
262
392
  }
@@ -264,6 +394,12 @@ function csfRenderPlugin(componentData, _documentationComponents, handoff) {
264
394
  componentData.preview = '';
265
395
  componentData.code = (0, html_1.trimPreview)(sourceCode);
266
396
  componentData.html = (0, html_1.trimPreview)(lastHtml);
397
+ // Keep top-level usage for backwards compatibility (first preview usage).
398
+ const storyKeys = Object.keys(componentData.previews);
399
+ const firstStoryKey = storyKeys[0];
400
+ if (firstStoryKey) {
401
+ componentData.usage = componentData.previews[firstStoryKey].usage || '';
402
+ }
267
403
  });
268
404
  },
269
405
  };
@@ -26,6 +26,7 @@ const html_1 = require("../utils/html");
26
26
  const module_1 = require("../utils/module");
27
27
  const schema_loader_1 = require("../utils/schema-loader");
28
28
  const string_1 = require("../utils/string");
29
+ const usage_1 = require("../utils/usage");
29
30
  const vite_logger_1 = require("../utils/vite-logger");
30
31
  /**
31
32
  * Constants for the SSR render plugin
@@ -187,7 +188,6 @@ function ssrRenderPlugin(componentData, documentationComponents, handoff) {
187
188
  if (!documentationComponents) {
188
189
  documentationComponents = {};
189
190
  }
190
- const generatedPreviews = {};
191
191
  // Process component instances from documentation
192
192
  // Use figmaComponentId if provided, otherwise skip implicit matching
193
193
  if (componentData.figmaComponentId) {
@@ -200,6 +200,7 @@ function ssrRenderPlugin(componentData, documentationComponents, handoff) {
200
200
  title: variationId,
201
201
  url: '',
202
202
  values: instanceValues,
203
+ usage: '',
203
204
  };
204
205
  }
205
206
  }
@@ -254,8 +255,13 @@ function ssrRenderPlugin(componentData, documentationComponents, handoff) {
254
255
  fileName: `${componentId}-${previewKey}${PLUGIN_CONSTANTS.INSPECT_SUFFIX}.html`,
255
256
  source: finalHtml,
256
257
  });
257
- generatedPreviews[previewKey] = finalHtml;
258
258
  componentData.previews[previewKey].url = `${componentId}-${previewKey}.html`;
259
+ componentData.previews[previewKey].usage = (0, usage_1.generateUsageSnippet)({
260
+ componentName: (0, usage_1.extractComponentName)(componentPath),
261
+ properties: componentData.properties || {},
262
+ previewValues: previewProps || {},
263
+ templateFileName: path_1.default.basename(componentPath),
264
+ });
259
265
  }
260
266
  // Format final HTML and update component data
261
267
  finalHtml = yield (0, html_1.formatHtml)(finalHtml);
@@ -263,6 +269,16 @@ function ssrRenderPlugin(componentData, documentationComponents, handoff) {
263
269
  componentData.preview = '';
264
270
  componentData.code = (0, html_1.trimPreview)(componentSourceCode);
265
271
  componentData.html = (0, html_1.trimPreview)(finalHtml);
272
+ // Generate usage snippet from the first preview's values
273
+ const previewKeys = Object.keys(componentData.previews);
274
+ const firstPreviewValues = previewKeys.length > 0 ? componentData.previews[previewKeys[0]].values : {};
275
+ const componentName = (0, usage_1.extractComponentName)(componentPath);
276
+ componentData.usage = (0, usage_1.generateUsageSnippet)({
277
+ componentName,
278
+ properties: componentData.properties || {},
279
+ previewValues: firstPreviewValues,
280
+ templateFileName: path_1.default.basename(componentPath),
281
+ });
266
282
  });
267
283
  },
268
284
  };
@@ -15,6 +15,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.readComponentMetadataApi = exports.readComponentApi = exports.updateComponentSummaryApi = exports.writeComponentApi = exports.getAPIPath = void 0;
16
16
  const fs_extra_1 = __importDefault(require("fs-extra"));
17
17
  const path_1 = __importDefault(require("path"));
18
+ const previews_1 = require("./previews");
19
+ const sanitizeComponentApiData = (component) => {
20
+ if (!component)
21
+ return component;
22
+ const sanitized = Object.assign(Object.assign({}, component), { previews: (0, previews_1.getDocumentedPreviews)(component.previews) });
23
+ delete sanitized.internalPatternPreviews;
24
+ return sanitized;
25
+ };
18
26
  /**
19
27
  * Merges values from a source object into a target object, returning a new object.
20
28
  * For each key present in either object:
@@ -66,15 +74,16 @@ const writeComponentSummaryAPI = (handoff, componentData) => __awaiter(void 0, v
66
74
  const writeComponentApi = (id_1, component_1, handoff_1, ...args_1) => __awaiter(void 0, [id_1, component_1, handoff_1, ...args_1], void 0, function* (id, component, handoff, preserveKeys = []) {
67
75
  const outputDirPath = path_1.default.resolve((0, exports.getAPIPath)(handoff), 'component');
68
76
  const outputFilePath = path_1.default.resolve(outputDirPath, `${id}.json`);
77
+ const sanitizedComponent = sanitizeComponentApiData(component);
69
78
  if (fs_extra_1.default.existsSync(outputFilePath)) {
70
79
  const existingJson = yield fs_extra_1.default.readFile(outputFilePath, 'utf8');
71
80
  if (existingJson) {
72
81
  try {
73
- const existingData = JSON.parse(existingJson);
82
+ const existingData = sanitizeComponentApiData(JSON.parse(existingJson));
74
83
  // Special case: always allow page to be cleared when undefined
75
84
  // This handles the case where page slices are removed
76
- const finalPreserveKeys = component.page === undefined ? preserveKeys.filter((key) => key !== 'page') : preserveKeys;
77
- const mergedData = updateObject(existingData, component, finalPreserveKeys);
85
+ const finalPreserveKeys = sanitizedComponent.page === undefined ? preserveKeys.filter((key) => key !== 'page') : preserveKeys;
86
+ const mergedData = sanitizeComponentApiData(updateObject(existingData, sanitizedComponent, finalPreserveKeys));
78
87
  yield fs_extra_1.default.writeFile(outputFilePath, JSON.stringify(mergedData, null, 2));
79
88
  return;
80
89
  }
@@ -86,7 +95,7 @@ const writeComponentApi = (id_1, component_1, handoff_1, ...args_1) => __awaiter
86
95
  if (!fs_extra_1.default.existsSync(outputDirPath)) {
87
96
  fs_extra_1.default.mkdirSync(outputDirPath, { recursive: true });
88
97
  }
89
- yield fs_extra_1.default.writeFile(outputFilePath, JSON.stringify(component, null, 2));
98
+ yield fs_extra_1.default.writeFile(outputFilePath, JSON.stringify(sanitizedComponent, null, 2));
90
99
  });
91
100
  exports.writeComponentApi = writeComponentApi;
92
101
  /**
@@ -106,7 +115,7 @@ const updateComponentSummaryApi = (handoff_1, componentData_1, ...args_1) => __a
106
115
  if (fs_extra_1.default.existsSync(apiPath)) {
107
116
  try {
108
117
  const existing = yield fs_extra_1.default.readFile(apiPath, 'utf8');
109
- existingData = JSON.parse(existing);
118
+ existingData = JSON.parse(existing).map((component) => (Object.assign(Object.assign({}, component), { previews: (0, previews_1.getDocumentedPreviews)(component.previews) })));
110
119
  }
111
120
  catch (_a) {
112
121
  // Corrupt or missing JSON — treat as empty
@@ -132,7 +141,7 @@ const readComponentApi = (handoff, id) => __awaiter(void 0, void 0, void 0, func
132
141
  try {
133
142
  const existingJson = yield fs_extra_1.default.readFile(outputFilePath, 'utf8');
134
143
  if (existingJson) {
135
- return JSON.parse(existingJson);
144
+ return sanitizeComponentApiData(JSON.parse(existingJson));
136
145
  }
137
146
  }
138
147
  catch (_) {
@@ -166,7 +175,7 @@ const readComponentMetadataApi = (handoff, id) => __awaiter(void 0, void 0, void
166
175
  categories: componentData.categories ? componentData.categories : [],
167
176
  tags: componentData.tags ? componentData.tags : [],
168
177
  properties: componentData.properties,
169
- previews: componentData.previews,
178
+ previews: (0, previews_1.getDocumentedPreviews)(componentData.previews),
170
179
  path: `/api/component/${id}.json`,
171
180
  };
172
181
  });
@@ -27,10 +27,12 @@ exports.ComponentSegment = void 0;
27
27
  exports.processComponents = processComponents;
28
28
  const cloneDeep_1 = __importDefault(require("lodash/cloneDeep"));
29
29
  const cache_1 = require("../../../cache");
30
+ const duration_1 = require("../../../utils/duration");
30
31
  const logger_1 = require("../../../utils/logger");
31
32
  const schema_1 = require("../../utils/schema");
32
33
  const types_1 = require("../types");
33
34
  const api_1 = require("./api");
35
+ const previews_1 = require("./previews");
34
36
  const css_1 = __importDefault(require("./css"));
35
37
  const html_1 = __importDefault(require("./html"));
36
38
  const javascript_1 = __importDefault(require("./javascript"));
@@ -79,6 +81,40 @@ const ensureDefaultPreview = (data) => {
79
81
  };
80
82
  }
81
83
  };
84
+ const getBuildPreviews = (data) => {
85
+ return Object.assign(Object.assign({}, ((data === null || data === void 0 ? void 0 : data.previews) || {})), ((data === null || data === void 0 ? void 0 : data.internalPatternPreviews) || {}));
86
+ };
87
+ /** Vite-backed segments in execution order; extend here if new steps are added. */
88
+ const VITE_BUILD_SEGMENTS = [
89
+ { key: 'js', label: 'script' },
90
+ { key: 'css', label: 'styles' },
91
+ { key: 'previews', label: 'previews' },
92
+ ];
93
+ const isFullViteComponentBuild = (plan) => plan.js && plan.css && plan.previews;
94
+ const getActiveViteBuildLabels = (plan) => VITE_BUILD_SEGMENTS.filter(({ key }) => plan[key]).map(({ label }) => label);
95
+ /** e.g. "a", "a and b", "a, b, and c" — common CLI copy style. */
96
+ const formatEnglishList = (items) => {
97
+ if (items.length === 0)
98
+ return '';
99
+ if (items.length === 1)
100
+ return items[0];
101
+ if (items.length === 2)
102
+ return `${items[0]} and ${items[1]}`;
103
+ return `${items.slice(0, -1).join(', ')}, and ${items[items.length - 1]}`;
104
+ };
105
+ const viteBuildStartMessage = (componentId, plan) => {
106
+ if (isFullViteComponentBuild(plan)) {
107
+ return `Building component "${componentId}"…`;
108
+ }
109
+ return `Building ${formatEnglishList(getActiveViteBuildLabels(plan))} for component "${componentId}"…`;
110
+ };
111
+ const viteBuildFinishMessage = (componentId, plan, elapsedMs) => {
112
+ const duration = (0, duration_1.formatDurationMs)(elapsedMs);
113
+ if (isFullViteComponentBuild(plan)) {
114
+ return `Finished building component "${componentId}" in ${duration}`;
115
+ }
116
+ return `Finished building ${formatEnglishList(getActiveViteBuildLabels(plan))} for component "${componentId}" in ${duration}`;
117
+ };
82
118
  /**
83
119
  * Returns a normalized build plan describing which component segments need rebuilding.
84
120
  *
@@ -166,12 +202,17 @@ function processComponents(handoff, id, segmentToProcess, options) {
166
202
  if (cache) {
167
203
  (0, cache_1.pruneRemovedComponents)(cache, allComponentIds);
168
204
  }
169
- const skippedCount = allComponentIds.length - componentsToBuild.size;
170
- if (skippedCount > 0) {
171
- logger_1.Logger.info(`Building ${componentsToBuild.size} of ${allComponentIds.length} components (${skippedCount} unchanged)`);
205
+ const total = allComponentIds.length;
206
+ const toBuildCount = componentsToBuild.size;
207
+ const skippedCount = total - toBuildCount;
208
+ if (total > 0 && toBuildCount === 0) {
209
+ logger_1.Logger.info(`All ${total} components unchanged; skipping build`);
210
+ }
211
+ else if (skippedCount > 0 && toBuildCount > 0) {
212
+ logger_1.Logger.info(`Building ${toBuildCount} of ${total} components (${skippedCount} unchanged)`);
172
213
  }
173
- else if (componentsToBuild.size > 0) {
174
- logger_1.Logger.info(`Building all ${componentsToBuild.size} components`);
214
+ else if (toBuildCount > 0) {
215
+ logger_1.Logger.info(`Building all ${toBuildCount} components`);
175
216
  }
176
217
  else {
177
218
  logger_1.Logger.info('All components up to date, nothing to build');
@@ -239,10 +280,11 @@ function processComponents(handoff, id, segmentToProcess, options) {
239
280
  data.css = existingData.css;
240
281
  data.sass = existingData.sass;
241
282
  }
242
- // If we're not building previews, preserve pre-existing HTML, code snippet, and previews.
283
+ // If we're not building previews, preserve pre-existing HTML, code snippet, usage, and previews.
243
284
  if (!buildPlan.previews) {
244
285
  data.html = existingData.html;
245
286
  data.code = existingData.code;
287
+ data.usage = existingData.usage;
246
288
  data.previews = existingData.previews;
247
289
  }
248
290
  /**
@@ -261,6 +303,12 @@ function processComponents(handoff, id, segmentToProcess, options) {
261
303
  }
262
304
  // Components should always have at least one preview variation.
263
305
  ensureDefaultPreview(data);
306
+ const runsViteBuildSteps = buildPlan.js || buildPlan.css || buildPlan.previews;
307
+ let viteBuildStartedAtMs = 0;
308
+ if (runsViteBuildSteps) {
309
+ viteBuildStartedAtMs = Date.now();
310
+ logger_1.Logger.info(viteBuildStartMessage(runtimeComponentId, buildPlan));
311
+ }
264
312
  // Build JS if needed (new build, validation missing, or explicit segment request).
265
313
  if (buildPlan.js) {
266
314
  data = yield (0, javascript_1.default)(data, handoff);
@@ -271,7 +319,11 @@ function processComponents(handoff, id, segmentToProcess, options) {
271
319
  }
272
320
  // Build previews (HTML, snapshots, etc) if needed.
273
321
  if (buildPlan.previews) {
274
- data = yield (0, html_1.default)(data, handoff, components);
322
+ data = yield (0, html_1.default)(Object.assign(Object.assign({}, data), { previews: getBuildPreviews(data) }), handoff, components);
323
+ }
324
+ if (runsViteBuildSteps) {
325
+ const elapsedMs = Date.now() - viteBuildStartedAtMs;
326
+ logger_1.Logger.info(viteBuildFinishMessage(runtimeComponentId, buildPlan, elapsedMs));
275
327
  }
276
328
  /**
277
329
  * Run validation if explicitly requested and a hook is configured.
@@ -284,6 +336,7 @@ function processComponents(handoff, id, segmentToProcess, options) {
284
336
  // Ensure that every property within the properties array/object contains an 'id' field.
285
337
  // This guarantees unique identification for property entries, which is useful for updates and API consumers.
286
338
  data.properties = (0, schema_1.ensureIds)(data.properties);
339
+ data.previews = (0, previews_1.getDocumentedPreviews)(data.previews);
287
340
  // Write the updated component data to the API file for external access and caching.
288
341
  //console.log('data', data);
289
342
  yield (0, api_1.writeComponentApi)(runtimeComponentId, data, handoff, []);
@@ -337,7 +390,7 @@ const buildComponentSummary = (id, data) => {
337
390
  categories: data.categories ? data.categories : [],
338
391
  tags: data.tags ? data.tags : [],
339
392
  properties: data.properties,
340
- previews: data.previews,
393
+ previews: (0, previews_1.getDocumentedPreviews)(data.previews),
341
394
  path: `${(_a = process.env.HANDOFF_APP_BASE_PATH) !== null && _a !== void 0 ? _a : ''}/api/component/${id}.json`,
342
395
  };
343
396
  };
@@ -19,11 +19,15 @@ declare const buildComponentCss: (data: TransformComponentTokensResult, handoff:
19
19
  css?: string;
20
20
  sass?: string;
21
21
  sharedStyles?: string;
22
+ usage?: string;
22
23
  title?: string;
23
24
  description?: string;
24
25
  previews?: {
25
26
  [key: string]: import("../types").OptionalPreviewRender;
26
27
  };
28
+ internalPatternPreviews?: {
29
+ [key: string]: import("../types").OptionalPreviewRender;
30
+ };
27
31
  properties?: {
28
32
  [key: string]: import("../component").SlotMetadata;
29
33
  };
@@ -32,8 +36,12 @@ declare const buildComponentCss: (data: TransformComponentTokensResult, handoff:
32
36
  js?: string;
33
37
  scss?: string;
34
38
  template?: string;
39
+ component?: string;
40
+ story?: string;
35
41
  schema?: string;
42
+ templates?: string;
36
43
  };
44
+ renderer?: import("../../../index").RendererKind;
37
45
  options?: {
38
46
  preview?: {
39
47
  groupBy?: string;
@@ -17,6 +17,7 @@ const fs_extra_1 = __importDefault(require("fs-extra"));
17
17
  const path_1 = __importDefault(require("path"));
18
18
  const vite_1 = require("vite");
19
19
  const config_1 = require("../../../config");
20
+ const duration_1 = require("../../../utils/duration");
20
21
  const logger_1 = require("../../../utils/logger");
21
22
  const vite_config_1 = __importDefault(require("../../vite-config"));
22
23
  const component_1 = require("../component");
@@ -155,9 +156,9 @@ const buildMainCss = (handoff) => __awaiter(void 0, void 0, void 0, function* ()
155
156
  const stat = yield fs_extra_1.default.stat(runtimeConfig.entries.scss);
156
157
  const entryPath = stat.isDirectory() ? path_1.default.resolve(runtimeConfig.entries.scss, 'main.scss') : runtimeConfig.entries.scss;
157
158
  if (entryPath === runtimeConfig.entries.scss || fs_extra_1.default.existsSync(entryPath)) {
158
- logger_1.Logger.success(`Building main CSS file...`);
159
+ logger_1.Logger.info(`Building styles for global entry (main.css)…`);
160
+ const startedAt = Date.now();
159
161
  try {
160
- // Setup SASS load paths
161
162
  const loadPaths = [
162
163
  path_1.default.resolve(handoff.workingPath),
163
164
  path_1.default.resolve(handoff.workingPath, handoff.exportsDirectory, handoff.getProjectId()),
@@ -173,9 +174,10 @@ const buildMainCss = (handoff) => __awaiter(void 0, void 0, void 0, function* ()
173
174
  loadPaths,
174
175
  handoff,
175
176
  });
177
+ logger_1.Logger.info(`Finished building styles for global entry (main.css) in ${(0, duration_1.formatDurationMs)(Date.now() - startedAt)}`);
176
178
  }
177
- catch (e) {
178
- logger_1.Logger.error(`Failed to build main CSS:`, e);
179
+ catch (_d) {
180
+ // buildCssBundle already logs failure for the entry path
179
181
  }
180
182
  }
181
183
  }