handoff-app 2.0.0-7 → 2.0.0-9

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 (125) hide show
  1. package/README.md +130 -14
  2. package/UPGRADE.md +20 -0
  3. package/dist/app-builder/build.js +10 -2
  4. package/dist/app-builder/client-config.js +3 -1
  5. package/dist/app-builder/watchers.js +17 -7
  6. package/dist/cache/build-cache.js +5 -1
  7. package/dist/catalog/normalize.js +3 -0
  8. package/dist/catalog/previews.js +5 -4
  9. package/dist/catalog/types.d.ts +8 -0
  10. package/dist/cli/auth/device.d.ts +6 -1
  11. package/dist/cli/auth/device.js +5 -2
  12. package/dist/cli/auth/store.d.ts +15 -6
  13. package/dist/cli/auth/store.js +72 -19
  14. package/dist/commands/login/index.d.ts +5 -0
  15. package/dist/commands/login/index.js +18 -6
  16. package/dist/commands/logout/index.d.ts +5 -0
  17. package/dist/commands/logout/index.js +54 -15
  18. package/dist/commands/types.d.ts +2 -0
  19. package/dist/commands/utils.d.ts +21 -7
  20. package/dist/commands/utils.js +11 -10
  21. package/dist/config/catalog-include.d.ts +2 -2
  22. package/dist/config/defaults.d.ts +3 -3
  23. package/dist/config/defaults.js +54 -48
  24. package/dist/config/env.d.ts +9 -0
  25. package/dist/config/env.js +43 -0
  26. package/dist/config/errors.d.ts +4 -0
  27. package/dist/config/errors.js +11 -0
  28. package/dist/config/from-env.d.ts +24 -0
  29. package/dist/config/from-env.js +7 -0
  30. package/dist/config/helpers.d.ts +2 -0
  31. package/dist/config/helpers.js +3 -1
  32. package/dist/config/index.d.ts +2 -1
  33. package/dist/config/index.js +6 -1
  34. package/dist/config/loader.d.ts +29 -11
  35. package/dist/config/loader.js +121 -46
  36. package/dist/config/resolve-env.d.ts +5 -0
  37. package/dist/config/resolve-env.js +102 -0
  38. package/dist/config/runtime.d.ts +3 -3
  39. package/dist/config/runtime.js +8 -10
  40. package/dist/config/validator.d.ts +2 -2
  41. package/dist/config/validator.js +2 -4
  42. package/dist/handlebars.d.ts +10 -2
  43. package/dist/index.d.ts +33 -4
  44. package/dist/index.js +60 -16
  45. package/dist/pipeline/index.d.ts +1 -1
  46. package/dist/pipeline/index.js +1 -1
  47. package/dist/registry/asset-storage/adapters/vercel-blob.js +1 -1
  48. package/dist/registry/asset-storage/resolve.d.ts +2 -2
  49. package/dist/registry/asset-storage/resolve.js +4 -1
  50. package/dist/registry/checkout/index.js +4 -4
  51. package/dist/registry/connection.d.ts +28 -24
  52. package/dist/registry/connection.js +53 -67
  53. package/dist/registry/db/driver.d.ts +4 -11
  54. package/dist/registry/db/driver.js +4 -16
  55. package/dist/registry/db/index.d.ts +1 -1
  56. package/dist/registry/db/index.js +1 -2
  57. package/dist/registry/publish/index.js +4 -4
  58. package/dist/store/filesystem.d.ts +2 -2
  59. package/dist/transformers/plugins/ssr-render.js +40 -15
  60. package/dist/transformers/preview/component/css.d.ts +1 -0
  61. package/dist/transformers/preview/component/css.js +85 -47
  62. package/dist/transformers/preview/pattern/html.js +3 -1
  63. package/dist/transformers/preview/types.d.ts +13 -0
  64. package/dist/transformers/utils/build.d.ts +19 -0
  65. package/dist/transformers/utils/build.js +83 -1
  66. package/dist/transformers/utils/module.js +2 -1
  67. package/dist/types/config.d.ts +54 -29
  68. package/docs/api.md +31 -0
  69. package/docs/cli.md +2 -2
  70. package/package.json +1 -1
  71. package/src/app/components/Component/PageSliceResolver.tsx +1 -0
  72. package/src/app/components/Component/Preview.tsx +380 -189
  73. package/src/app/components/Component/slices/ComponentDisplaySlice.tsx +4 -0
  74. package/src/app/pages/account/tokens.tsx +3 -1
  75. package/src/app-builder/build.ts +10 -2
  76. package/src/app-builder/client-config.ts +5 -3
  77. package/src/app-builder/watchers.ts +18 -8
  78. package/src/cache/build-cache.ts +5 -1
  79. package/src/catalog/README.md +3 -0
  80. package/src/catalog/normalize.ts +2 -0
  81. package/src/catalog/previews.ts +6 -3
  82. package/src/catalog/types.ts +8 -0
  83. package/src/cli/auth/device.ts +8 -3
  84. package/src/cli/auth/store.ts +75 -18
  85. package/src/commands/login/index.ts +20 -6
  86. package/src/commands/logout/index.ts +74 -20
  87. package/src/commands/types.ts +2 -0
  88. package/src/commands/utils.ts +14 -5
  89. package/src/config/README.md +61 -1
  90. package/src/config/catalog-include.ts +2 -2
  91. package/src/config/defaults.ts +24 -14
  92. package/src/config/env.ts +41 -0
  93. package/src/config/errors.ts +7 -0
  94. package/src/config/from-env.ts +26 -0
  95. package/src/config/helpers.ts +4 -2
  96. package/src/config/index.ts +4 -1
  97. package/src/config/loader.ts +158 -50
  98. package/src/config/resolve-env.ts +94 -0
  99. package/src/config/runtime.ts +12 -7
  100. package/src/config/validator.ts +4 -6
  101. package/src/handlebars.ts +8 -2
  102. package/src/index.d.ts +2 -2
  103. package/src/index.ts +85 -8
  104. package/src/pipeline/index.ts +1 -1
  105. package/src/registry/asset-storage/adapters/vercel-blob.ts +1 -1
  106. package/src/registry/asset-storage/resolve.ts +6 -3
  107. package/src/registry/checkout/index.ts +5 -5
  108. package/src/registry/connection.ts +72 -71
  109. package/src/registry/db/driver.ts +6 -17
  110. package/src/registry/db/index.ts +0 -1
  111. package/src/registry/publish/index.ts +5 -5
  112. package/src/store/filesystem.ts +2 -2
  113. package/src/transformers/plugins/ssr-render.ts +53 -16
  114. package/src/transformers/preview/component/css.ts +93 -50
  115. package/src/transformers/preview/pattern/html.ts +3 -1
  116. package/src/transformers/preview/types.ts +14 -0
  117. package/src/transformers/utils/build.ts +80 -0
  118. package/src/transformers/utils/module.ts +4 -2
  119. package/src/types/config.ts +51 -29
  120. package/templates/default/env.tpl +10 -0
  121. package/templates/default/gitignore.tpl +7 -0
  122. package/templates/default/handoff.config.js.tpl +21 -172
  123. package/templates/default/handoff.config.ts.tpl +21 -156
  124. package/templates/default/handoff.config.with-examples.js.tpl +20 -171
  125. package/templates/default/handoff.config.with-examples.ts.tpl +20 -155
@@ -121,12 +121,25 @@ function loadComponentSchemaAndModule(componentData, componentPath, handoff) {
121
121
  * @param componentExport - Export holding the implementation; the default export when absent
122
122
  * @returns Client-side hydration source code
123
123
  */
124
- function generateClientHydrationSource(componentId, componentPath, componentExport) {
124
+ function generateClientHydrationSource(componentId, componentPath, componentExport, declarationPath) {
125
125
  const importClause = componentExport && componentExport !== 'default' ? `{ ${componentExport} as Component }` : 'Component';
126
+ // A function cannot travel in the JSON props, so the client re-reads `render` from the module that
127
+ // defines it. Conditional, so an unaffected component does not ship its declaration's imports.
128
+ const declarationImport = declarationPath ? `import * as previewDeclaration from '${(0, vite_1.normalizePath)(declarationPath)}';` : '';
129
+ const resolveComponent = declarationPath
130
+ ? `(mount) => {
131
+ const previewKey = mount.getAttribute('data-handoff-preview');
132
+ const render = previewKey ? previewDeclaration[previewKey]?.render : undefined;
133
+ return typeof render === 'function' ? render : Component;
134
+ }`
135
+ : '() => Component';
126
136
  return `
127
137
  import React from 'react';
128
138
  import { hydrateRoot } from 'react-dom/client';
129
139
  import ${importClause} from '${(0, vite_1.normalizePath)(componentPath)}';
140
+ ${declarationImport}
141
+
142
+ const resolveComponent = ${resolveComponent};
130
143
 
131
144
  const parseProps = (propsId) => {
132
145
  const raw = propsId ? document.getElementById(propsId)?.textContent : '{}';
@@ -139,12 +152,14 @@ function generateClientHydrationSource(componentId, componentPath, componentExpo
139
152
 
140
153
  const standaloneRoot = document.getElementById('${PLUGIN_CONSTANTS.ROOT_ELEMENT_ID}');
141
154
  if (standaloneRoot && !standaloneRoot.hasAttribute('data-handoff-component')) {
142
- hydrateRoot(standaloneRoot, <Component {...parseProps('${PLUGIN_CONSTANTS.PROPS_SCRIPT_ID}')} />);
155
+ const StandaloneComponent = resolveComponent(standaloneRoot);
156
+ hydrateRoot(standaloneRoot, <StandaloneComponent {...parseProps('${PLUGIN_CONSTANTS.PROPS_SCRIPT_ID}')} />);
143
157
  }
144
158
 
145
159
  const patternMounts = document.querySelectorAll('[data-handoff-component=' + JSON.stringify('${componentId}') + ']');
146
160
  patternMounts.forEach((mount) => {
147
- hydrateRoot(mount, <Component {...parseProps(mount.getAttribute('data-handoff-props'))} />);
161
+ const MountComponent = resolveComponent(mount);
162
+ hydrateRoot(mount, <MountComponent {...parseProps(mount.getAttribute('data-handoff-props'))} />);
148
163
  });
149
164
  `;
150
165
  }
@@ -156,11 +171,11 @@ function generateClientHydrationSource(componentId, componentPath, componentExpo
156
171
  * artifact cannot be loaded.
157
172
  * @param componentId - Component identifier
158
173
  * @param previewTitle - Title for the preview
159
- * @param renderedHtml - Server-rendered HTML content
174
+ * @param renderedHtml - Server-rendered HTML, embedded verbatim so the client can hydrate it
160
175
  * @param props - Component props as JSON
161
176
  * @returns Complete HTML document
162
177
  */
163
- function generateHtmlDocument(componentId, previewTitle, renderedHtml, props, sharedArtifacts, componentArtifacts) {
178
+ function generateHtmlDocument(componentId, previewKey, previewTitle, renderedHtml, props, sharedArtifacts, componentArtifacts) {
164
179
  var _a;
165
180
  const basePath = (_a = process.env.HANDOFF_APP_BASE_PATH) !== null && _a !== void 0 ? _a : '';
166
181
  const clientArtifactUrl = (0, url_1.buildArtifactUrl)(clientArtifactPath(componentId), basePath);
@@ -193,7 +208,7 @@ function generateHtmlDocument(componentId, previewTitle, renderedHtml, props, sh
193
208
  <title>${previewTitle}</title>
194
209
  </head>
195
210
  <body>
196
- <div id="${PLUGIN_CONSTANTS.ROOT_ELEMENT_ID}">${renderedHtml}</div>
211
+ <div id="${PLUGIN_CONSTANTS.ROOT_ELEMENT_ID}" data-handoff-preview="${previewKey}">${renderedHtml}</div>
197
212
  </body>
198
213
  </html>`;
199
214
  }
@@ -224,7 +239,7 @@ function ssrRenderPlugin(componentData, documentationComponents, handoff) {
224
239
  },
225
240
  generateBundle(_, bundle) {
226
241
  return __awaiter(this, void 0, void 0, function* () {
227
- var _a, _b;
242
+ var _a, _b, _c, _d, _e;
228
243
  // Remove all JS chunks to prevent conflicts
229
244
  for (const [fileName, chunkInfo] of Object.entries(bundle)) {
230
245
  if (chunkInfo.type === 'chunk' && fileName.includes(PLUGIN_CONSTANTS.SCRIPT_ID)) {
@@ -275,15 +290,19 @@ function ssrRenderPlugin(componentData, documentationComponents, handoff) {
275
290
  // `component/<id>.client.js` artifact. The hydration source only imports the component and
276
291
  // reads props from the in-document `__APP_PROPS__` element, so it is identical across every
277
292
  // preview of this component.
278
- const clientHydrationSource = generateClientHydrationSource(componentId, componentPath, componentData.componentExport);
293
+ const declarationPath = Object.values((_a = componentData.previews) !== null && _a !== void 0 ? _a : {}).some((preview) => typeof preview.render === 'function')
294
+ ? (_b = componentData.entries) === null || _b === void 0 ? void 0 : _b.declaration
295
+ : undefined;
296
+ const clientHydrationSource = generateClientHydrationSource(componentId, componentPath, componentData.componentExport, declarationPath);
279
297
  const clientBuildConfig = Object.assign(Object.assign({}, build_1.DEFAULT_CLIENT_BUILD_CONFIG), { logLevel: 'silent', stdin: {
280
298
  contents: clientHydrationSource,
281
299
  resolveDir: process.cwd(),
282
300
  loader: 'tsx',
283
301
  }, plugins: [(0, build_1.createReactResolvePlugin)(handoff.workingPath, handoff.modulePath)] });
284
- const finalClientBuildConfig = ((_b = (_a = handoff.config) === null || _a === void 0 ? void 0 : _a.hooks) === null || _b === void 0 ? void 0 : _b.clientBuildConfig)
302
+ const hookedClientBuildConfig = ((_d = (_c = handoff.config) === null || _c === void 0 ? void 0 : _c.hooks) === null || _d === void 0 ? void 0 : _d.clientBuildConfig)
285
303
  ? handoff.config.hooks.clientBuildConfig(clientBuildConfig)
286
304
  : clientBuildConfig;
305
+ const finalClientBuildConfig = (0, build_1.withStyleLoaders)(hookedClientBuildConfig);
287
306
  let clientBundleJs;
288
307
  try {
289
308
  const bundledClient = yield esbuild_1.default.build(finalClientBuildConfig);
@@ -315,11 +334,17 @@ function ssrRenderPlugin(componentData, documentationComponents, handoff) {
315
334
  const componentArtifacts = (0, shared_artifacts_1.resolveComponentArtifactPresence)(handoff, componentId);
316
335
  // Generate previews for each variation
317
336
  for (const previewKey in componentData.previews) {
318
- const previewProps = componentData.previews[previewKey].values;
319
- // Server-side render the component
320
- const serverRenderedHtml = server_1.default.renderToString(react_1.default.createElement(ReactComponent, previewProps));
321
- const formattedHtml = yield (0, html_1.formatHtml)(serverRenderedHtml);
322
- finalHtml = generateHtmlDocument(componentId, componentData.previews[previewKey].title, formattedHtml, previewProps, sharedArtifacts, componentArtifacts);
337
+ const preview = componentData.previews[previewKey];
338
+ const previewProps = preview.values;
339
+ // A synthetic pattern preview is not a named export, so the client looks up its
340
+ // `sourcePreview` instead. A preview without one has no `render` either.
341
+ const hydrationPreviewKey = (_e = preview.sourcePreview) !== null && _e !== void 0 ? _e : previewKey;
342
+ // Rendered as a component rather than called, so a preview can use hooks and own state.
343
+ const PreviewComponent = (typeof preview.render === 'function' ? preview.render : ReactComponent);
344
+ // The markup must reach the document exactly as rendered. Formatting it adds text nodes React
345
+ // does not expect, so it discards the server markup and re-renders the preview on the client.
346
+ const serverRenderedHtml = server_1.default.renderToString(react_1.default.createElement(PreviewComponent, previewProps));
347
+ finalHtml = generateHtmlDocument(componentId, hydrationPreviewKey, preview.title, serverRenderedHtml, previewProps, sharedArtifacts, componentArtifacts);
323
348
  // Emit preview files
324
349
  this.emitFile({
325
350
  type: 'asset',
@@ -340,7 +365,7 @@ function ssrRenderPlugin(componentData, documentationComponents, handoff) {
340
365
  templateFileName: path_1.default.basename(componentPath),
341
366
  });
342
367
  }
343
- // Format final HTML and update component data
368
+ // The previews are already emitted, so this formats only the docs code view.
344
369
  finalHtml = yield (0, html_1.formatHtml)(finalHtml);
345
370
  componentData.format = 'react';
346
371
  componentData.preview = '';
@@ -42,6 +42,7 @@ declare const buildComponentCss: (data: TransformComponentTokensResult, handoff:
42
42
  story?: string;
43
43
  schema?: string;
44
44
  templates?: string;
45
+ declaration?: string;
45
46
  };
46
47
  renderer?: import("../../../index").RendererKind;
47
48
  sourceFormat?: import("../../../index").SourceFormat;
@@ -18,6 +18,7 @@ const path_1 = __importDefault(require("path"));
18
18
  const vite_1 = require("vite");
19
19
  const duration_1 = require("../../../utils/duration");
20
20
  const logger_1 = require("../../../utils/logger");
21
+ const build_1 = require("../../utils/build");
21
22
  const vite_config_1 = __importDefault(require("../../vite-config"));
22
23
  const component_1 = require("../component");
23
24
  const { pathToFileURL } = require('url');
@@ -88,14 +89,49 @@ const buildCssBundle = (_a) => __awaiter(void 0, [_a], void 0, function* ({ entr
88
89
  }
89
90
  }
90
91
  });
92
+ const STYLE_SOURCE_EXTENSIONS = new Set(['.scss', '.sass', '.css']);
93
+ /**
94
+ * The stylesheets of one item, in cascade order and deduplicated by resolved path.
95
+ *
96
+ * Discovered imports come first and the declared entry last, so a declared entry can override what
97
+ * the component imports. A project that both declares and imports one file must build it once.
98
+ */
99
+ const resolveStyleSources = (data, handoff) => __awaiter(void 0, void 0, void 0, function* () {
100
+ var _a, _b;
101
+ const template = (_a = data.entries) === null || _a === void 0 ? void 0 : _a.template;
102
+ const discovered = template ? yield (0, build_1.collectStyleImports)(path_1.default.resolve(template), handoff) : [];
103
+ const declared = ((_b = data.entries) === null || _b === void 0 ? void 0 : _b.scss) ? [path_1.default.resolve(data.entries.scss)] : [];
104
+ const ordered = [];
105
+ const seen = new Set();
106
+ for (const source of [...discovered, ...declared]) {
107
+ if (!seen.has(source) && STYLE_SOURCE_EXTENSIONS.has(path_1.default.extname(source)) && fs_extra_1.default.existsSync(source)) {
108
+ seen.add(source);
109
+ ordered.push(source);
110
+ }
111
+ }
112
+ return ordered;
113
+ });
114
+ /**
115
+ * A single Sass entry that `@use`s every source in order.
116
+ *
117
+ * `@use` emits a repeated file once and inlines a plain `.css` source, which `@import` would leave
118
+ * as an unresolvable URL. Namespaces are explicit because two sources sharing a basename would
119
+ * claim the same default namespace and fail the compile.
120
+ */
121
+ const writeCombinedStyleEntry = (sources, outputPath, id) => __awaiter(void 0, void 0, void 0, function* () {
122
+ const entryPath = path_1.default.resolve(outputPath, `.${id}.entry.scss`);
123
+ const rules = sources.map((source, index) => `@use '${source.split(path_1.default.sep).join('/')}' as ns${index};`);
124
+ yield fs_extra_1.default.outputFile(entryPath, `${rules.join('\n')}\n`);
125
+ return entryPath;
126
+ });
91
127
  const buildComponentCss = (data, handoff) => __awaiter(void 0, void 0, void 0, function* () {
92
128
  var _a, _b, _c;
93
129
  const id = data.id;
94
130
  logger_1.Logger.debug(`buildComponentCss`, id);
95
131
  const outputPath = (0, component_1.getComponentOutputPath)(handoff);
96
132
  const builtCssPath = path_1.default.resolve(outputPath, `${id}.css`);
97
- const entry = (_a = data.entries) === null || _a === void 0 ? void 0 : _a.scss;
98
- if (!entry) {
133
+ const sources = yield resolveStyleSources(data, handoff);
134
+ if (sources.length === 0) {
99
135
  // Keep generated output aligned with the current component declaration.
100
136
  yield fs_extra_1.default.remove(builtCssPath);
101
137
  delete data['css'];
@@ -103,59 +139,61 @@ const buildComponentCss = (data, handoff) => __awaiter(void 0, void 0, void 0, f
103
139
  delete data['sharedStyles'];
104
140
  return data;
105
141
  }
106
- const extension = path_1.default.extname(entry);
107
- if (!extension) {
108
- return data;
109
- }
142
+ // A single source compiles directly, so its relative asset URLs and documented Sass stay as authored.
143
+ const combinedEntry = sources.length > 1 ? yield writeCombinedStyleEntry(sources, outputPath, id) : undefined;
144
+ const entry = combinedEntry !== null && combinedEntry !== void 0 ? combinedEntry : sources[0];
110
145
  try {
111
- if (extension === '.scss' || extension === '.css') {
112
- // Remove the previous artifact before rebuilding so "no emitted CSS"
113
- // doesn't silently preserve stale output from an earlier build.
114
- yield fs_extra_1.default.remove(builtCssPath);
115
- // Read the original source
116
- const sourceContent = yield fs_extra_1.default.readFile(entry, 'utf8');
117
- if (extension === '.scss') {
118
- data['sass'] = sourceContent;
119
- }
120
- // Setup SASS load paths
121
- const loadPaths = [
122
- path_1.default.resolve(handoff.workingPath),
123
- path_1.default.resolve(handoff.workingPath, handoff.exportsDirectory, handoff.getProjectId()),
124
- path_1.default.resolve(handoff.workingPath, 'node_modules'),
125
- ];
126
- if ((_c = (_b = handoff.runtimeConfig) === null || _b === void 0 ? void 0 : _b.entries) === null || _c === void 0 ? void 0 : _c.scss) {
127
- loadPaths.unshift(path_1.default.dirname(handoff.runtimeConfig.entries.scss));
128
- }
129
- yield buildCssBundle({
130
- entry,
131
- outputPath,
132
- outputFilename: `${id}.css`,
133
- loadPaths,
134
- handoff,
135
- });
136
- // Read the built CSS
137
- if (fs_extra_1.default.existsSync(builtCssPath)) {
138
- const builtCss = yield fs_extra_1.default.readFile(builtCssPath, 'utf8');
139
- data['css'] = builtCss;
140
- // Handle shared styles — if the component SCSS uses the split marker,
141
- // extract shared styles from the portion above it
142
- const splitCSS = builtCss.split('/* COMPONENT STYLES*/');
143
- if (splitCSS && splitCSS.length > 1) {
144
- data['css'] = splitCSS[1];
145
- data['sharedStyles'] = splitCSS[0];
146
- yield fs_extra_1.default.writeFile(path_1.default.resolve(outputPath, exports.SHARED_COMPONENT_CSS_FILE), data['sharedStyles']);
147
- }
148
- }
149
- else {
150
- delete data['css'];
151
- delete data['sharedStyles'];
146
+ // Remove the previous artifact before rebuilding so "no emitted CSS"
147
+ // doesn't silently preserve stale output from an earlier build.
148
+ yield fs_extra_1.default.remove(builtCssPath);
149
+ // Read the original source
150
+ const primarySource = ((_a = data.entries) === null || _a === void 0 ? void 0 : _a.scss) ? path_1.default.resolve(data.entries.scss) : sources[0];
151
+ if (path_1.default.extname(primarySource) === '.scss') {
152
+ data['sass'] = yield fs_extra_1.default.readFile(primarySource, 'utf8');
153
+ }
154
+ // Setup SASS load paths
155
+ const loadPaths = [
156
+ path_1.default.resolve(handoff.workingPath),
157
+ path_1.default.resolve(handoff.workingPath, handoff.exportsDirectory, handoff.getProjectId()),
158
+ path_1.default.resolve(handoff.workingPath, 'node_modules'),
159
+ ];
160
+ if ((_c = (_b = handoff.runtimeConfig) === null || _b === void 0 ? void 0 : _b.entries) === null || _c === void 0 ? void 0 : _c.scss) {
161
+ loadPaths.unshift(path_1.default.dirname(handoff.runtimeConfig.entries.scss));
162
+ }
163
+ yield buildCssBundle({
164
+ entry,
165
+ outputPath,
166
+ outputFilename: `${id}.css`,
167
+ loadPaths,
168
+ handoff,
169
+ });
170
+ // Read the built CSS
171
+ if (fs_extra_1.default.existsSync(builtCssPath)) {
172
+ const builtCss = yield fs_extra_1.default.readFile(builtCssPath, 'utf8');
173
+ data['css'] = builtCss;
174
+ // Handle shared styles — if the component SCSS uses the split marker,
175
+ // extract shared styles from the portion above it
176
+ const splitCSS = builtCss.split('/* COMPONENT STYLES*/');
177
+ if (splitCSS && splitCSS.length > 1) {
178
+ data['css'] = splitCSS[1];
179
+ data['sharedStyles'] = splitCSS[0];
180
+ yield fs_extra_1.default.writeFile(path_1.default.resolve(outputPath, exports.SHARED_COMPONENT_CSS_FILE), data['sharedStyles']);
152
181
  }
153
182
  }
183
+ else {
184
+ delete data['css'];
185
+ delete data['sharedStyles'];
186
+ }
154
187
  }
155
188
  catch (e) {
156
189
  logger_1.Logger.error(`Failed to build CSS for "${id}"`);
157
190
  throw e;
158
191
  }
192
+ finally {
193
+ if (combinedEntry) {
194
+ yield fs_extra_1.default.remove(combinedEntry);
195
+ }
196
+ }
159
197
  return data;
160
198
  });
161
199
  /**
@@ -48,7 +48,9 @@ const composePatternHtml = (_patternId, patternTitle, fragments, basePath, share
48
48
  cssHrefs.add((0, url_1.buildArtifactUrl)(`component/${componentId}.css`, basePath));
49
49
  }
50
50
  }
51
- const doc = (0, node_html_parser_1.parse)(html);
51
+ // React SSR separates adjacent text children with `<!-- -->`. The parser drops comments by
52
+ // default, which would make every composed React fragment fail to hydrate.
53
+ const doc = (0, node_html_parser_1.parse)(html, { comment: true });
52
54
  const suffix = `_p${i}`;
53
55
  const namespacedRootId = `root${suffix}`;
54
56
  const namespacedPropsId = `__APP_PROPS__${suffix}`;
@@ -14,6 +14,8 @@ export type PageSliceType = 'BEST_PRACTICES' | 'COMPONENT_DISPLAY' | 'VALIDATION
14
14
  export interface BasePageSlice {
15
15
  type: PageSliceType;
16
16
  }
17
+ /** A fluid preview renders at the width the reader's stage has, so it is never scaled down. */
18
+ export type PreviewWidth = 'fluid' | number;
17
19
  export interface BestPracticesPageSlice extends BasePageSlice {
18
20
  type: 'BEST_PRACTICES';
19
21
  }
@@ -22,6 +24,8 @@ export interface ComponentDisplayPageSlice extends BasePageSlice {
22
24
  showPreview?: boolean;
23
25
  showCodeHighlight?: boolean;
24
26
  defaultHeight?: string;
27
+ /** Viewport width for the preview, in pixels. Defaults to 'fluid'. */
28
+ defaultWidth?: PreviewWidth;
25
29
  filterBy?: Filter;
26
30
  }
27
31
  export interface ValidationResultsPageSlice extends BasePageSlice {
@@ -100,6 +104,8 @@ export type ComponentObject = {
100
104
  story?: string;
101
105
  /** Optional path to schema file (if available) */
102
106
  schema?: string;
107
+ /** Declaration file path; a preview `render` is read from it at build and hydration time. */
108
+ declaration?: string;
103
109
  /** Optional path to templates directory */
104
110
  templates?: string;
105
111
  };
@@ -187,6 +193,8 @@ export type TransformComponentTokensResult = {
187
193
  story?: string;
188
194
  schema?: string;
189
195
  templates?: string;
196
+ /** See `ComponentObject.entries.declaration`. */
197
+ declaration?: string;
190
198
  };
191
199
  renderer?: RendererKind;
192
200
  /** See `ComponentObject.sourceFormat`. Distinct from `format`, the preview's code language. */
@@ -208,6 +216,11 @@ export type OptionalPreviewRender = {
208
216
  };
209
217
  url: string;
210
218
  usage?: string;
219
+ /**
220
+ * Build-time only: the server renders it and the client re-reads it from the declaration module.
221
+ * `JSON.stringify` drops it from published artifacts.
222
+ */
223
+ render?: (args: Record<string, any>) => unknown;
211
224
  /** Optional source preview/story key used to derive this preview */
212
225
  sourcePreview?: string;
213
226
  };
@@ -1,4 +1,14 @@
1
1
  import esbuild from 'esbuild';
2
+ /**
3
+ * Keeps co-located stylesheet imports out of the JavaScript graph; the CSS pipeline compiles them.
4
+ *
5
+ * esbuild has no `.scss` loader, and a `.css` import fails too because these builds run with
6
+ * `write: false` and no output path. `empty` rather than a plugin: `esbuild.buildSync` rejects
7
+ * plugins, and the declaration and config loaders are synchronous.
8
+ *
9
+ * CSS Modules are not covered — the empty module leaves the class map undefined.
10
+ */
11
+ export declare const STYLE_IMPORT_LOADERS: Record<string, esbuild.Loader>;
2
12
  /**
3
13
  * Default esbuild configuration for SSR builds
4
14
  */
@@ -7,6 +17,15 @@ export declare const DEFAULT_SSR_BUILD_CONFIG: esbuild.BuildOptions;
7
17
  * Default esbuild configuration for client builds
8
18
  */
9
19
  export declare const DEFAULT_CLIENT_BUILD_CONFIG: esbuild.BuildOptions;
20
+ /** Apply after a consumer hook: a hook that replaces `loader` would otherwise drop these. */
21
+ export declare function withStyleLoaders<T extends esbuild.BuildOptions>(config: T): T;
22
+ /**
23
+ * Stylesheet paths imported anywhere in a component's module graph, in cascade order.
24
+ *
25
+ * An unbuildable graph yields an empty list rather than throwing, leaving the CSS step with the
26
+ * declared entry.
27
+ */
28
+ export declare function collectStyleImports(entryPath: string, handoff: any): Promise<string[]>;
10
29
  /**
11
30
  * Resolves a module from multiple search directories
12
31
  * @param id - Module ID to resolve
@@ -1,12 +1,35 @@
1
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
+ };
2
11
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
12
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
13
  };
5
14
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.DEFAULT_CLIENT_BUILD_CONFIG = exports.DEFAULT_SSR_BUILD_CONFIG = void 0;
15
+ exports.DEFAULT_CLIENT_BUILD_CONFIG = exports.DEFAULT_SSR_BUILD_CONFIG = exports.STYLE_IMPORT_LOADERS = void 0;
16
+ exports.withStyleLoaders = withStyleLoaders;
17
+ exports.collectStyleImports = collectStyleImports;
7
18
  exports.resolveModule = resolveModule;
8
19
  exports.createReactResolvePlugin = createReactResolvePlugin;
20
+ const esbuild_1 = __importDefault(require("esbuild"));
9
21
  const path_1 = __importDefault(require("path"));
22
+ const STYLE_EXTENSIONS = ['.css', '.scss', '.sass'];
23
+ /**
24
+ * Keeps co-located stylesheet imports out of the JavaScript graph; the CSS pipeline compiles them.
25
+ *
26
+ * esbuild has no `.scss` loader, and a `.css` import fails too because these builds run with
27
+ * `write: false` and no output path. `empty` rather than a plugin: `esbuild.buildSync` rejects
28
+ * plugins, and the declaration and config loaders are synchronous.
29
+ *
30
+ * CSS Modules are not covered — the empty module leaves the class map undefined.
31
+ */
32
+ exports.STYLE_IMPORT_LOADERS = Object.fromEntries(STYLE_EXTENSIONS.map((extension) => [extension, 'empty']));
10
33
  /**
11
34
  * Default esbuild configuration for SSR builds
12
35
  */
@@ -17,6 +40,7 @@ exports.DEFAULT_SSR_BUILD_CONFIG = {
17
40
  platform: 'node',
18
41
  jsx: 'automatic',
19
42
  external: ['react', 'react-dom', '@opentelemetry/api'],
43
+ loader: exports.STYLE_IMPORT_LOADERS,
20
44
  };
21
45
  /**
22
46
  * Default esbuild configuration for client builds
@@ -29,7 +53,65 @@ exports.DEFAULT_CLIENT_BUILD_CONFIG = {
29
53
  jsx: 'automatic',
30
54
  sourcemap: false,
31
55
  minify: false,
56
+ loader: exports.STYLE_IMPORT_LOADERS,
32
57
  };
58
+ /** Apply after a consumer hook: a hook that replaces `loader` would otherwise drop these. */
59
+ function withStyleLoaders(config) {
60
+ return Object.assign(Object.assign({}, config), { loader: Object.assign(Object.assign({}, config.loader), exports.STYLE_IMPORT_LOADERS) });
61
+ }
62
+ /**
63
+ * Stylesheets of the module graph in cascade order: every import of a module before the module
64
+ * itself, siblings in source order.
65
+ *
66
+ * This is the order a bundler emits CSS in, and the cascade depends on it. A component that
67
+ * overrides a rule of a component it imports ties that rule on specificity, so the later of the
68
+ * two wins. esbuild's `onLoad` callbacks do not give this order: they fire roughly breadth-first,
69
+ * which puts a component's own stylesheet ahead of the ones it imports and reverses every such
70
+ * override.
71
+ */
72
+ function orderStyleImports(metafile, entryPath) {
73
+ const entryKey = Object.keys(metafile.inputs).find((key) => path_1.default.resolve(key) === entryPath);
74
+ if (!entryKey)
75
+ return [];
76
+ const ordered = [];
77
+ const visited = new Set();
78
+ const visit = (key) => {
79
+ var _a, _b;
80
+ if (visited.has(key))
81
+ return;
82
+ visited.add(key);
83
+ for (const imported of (_b = (_a = metafile.inputs[key]) === null || _a === void 0 ? void 0 : _a.imports) !== null && _b !== void 0 ? _b : []) {
84
+ if (!imported.external && metafile.inputs[imported.path])
85
+ visit(imported.path);
86
+ }
87
+ if (STYLE_EXTENSIONS.includes(path_1.default.extname(key)))
88
+ ordered.push(path_1.default.resolve(key));
89
+ };
90
+ visit(entryKey);
91
+ return ordered;
92
+ }
93
+ /**
94
+ * Stylesheet paths imported anywhere in a component's module graph, in cascade order.
95
+ *
96
+ * An unbuildable graph yields an empty list rather than throwing, leaving the CSS step with the
97
+ * declared entry.
98
+ */
99
+ function collectStyleImports(entryPath, handoff) {
100
+ return __awaiter(this, void 0, void 0, function* () {
101
+ var _a, _b;
102
+ const baseConfig = Object.assign(Object.assign({}, exports.DEFAULT_SSR_BUILD_CONFIG), { entryPoints: [entryPath], logLevel: 'silent' });
103
+ const hookedConfig = ((_b = (_a = handoff === null || handoff === void 0 ? void 0 : handoff.config) === null || _a === void 0 ? void 0 : _a.hooks) === null || _b === void 0 ? void 0 : _b.ssrBuildConfig) ? handoff.config.hooks.ssrBuildConfig(baseConfig) : baseConfig;
104
+ try {
105
+ // The `empty` loader keeps stylesheets out of the JavaScript, and the metafile still records
106
+ // them as inputs, so the graph carries the order.
107
+ const result = yield esbuild_1.default.build(Object.assign(Object.assign({}, withStyleLoaders(hookedConfig)), { metafile: true }));
108
+ return result.metafile ? orderStyleImports(result.metafile, entryPath) : [];
109
+ }
110
+ catch (_c) {
111
+ return [];
112
+ }
113
+ });
114
+ }
33
115
  /**
34
116
  * Resolves a module from multiple search directories
35
117
  * @param id - Module ID to resolve
@@ -19,9 +19,10 @@ const build_1 = require("./build");
19
19
  function createModuleBuildConfig(entryPath, handoff) {
20
20
  var _a, _b;
21
21
  const defaultBuildConfig = Object.assign(Object.assign({}, build_1.DEFAULT_SSR_BUILD_CONFIG), { entryPoints: [entryPath] });
22
- return ((_b = (_a = handoff.config) === null || _a === void 0 ? void 0 : _a.hooks) === null || _b === void 0 ? void 0 : _b.ssrBuildConfig)
22
+ const hookedBuildConfig = ((_b = (_a = handoff.config) === null || _a === void 0 ? void 0 : _a.hooks) === null || _b === void 0 ? void 0 : _b.ssrBuildConfig)
23
23
  ? handoff.config.hooks.ssrBuildConfig(defaultBuildConfig)
24
24
  : defaultBuildConfig;
25
+ return (0, build_1.withStyleLoaders)(hookedBuildConfig);
25
26
  }
26
27
  function evaluateBuiltModule(code) {
27
28
  const mod = { exports: {} };