handoff-app 2.0.0-8 → 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.
- package/README.md +19 -3
- package/dist/catalog/normalize.js +3 -0
- package/dist/catalog/previews.js +5 -4
- package/dist/catalog/types.d.ts +8 -0
- package/dist/config/loader.js +2 -0
- package/dist/config/runtime.js +8 -10
- package/dist/handlebars.d.ts +10 -2
- package/dist/transformers/plugins/ssr-render.js +40 -15
- package/dist/transformers/preview/component/css.d.ts +1 -0
- package/dist/transformers/preview/component/css.js +85 -47
- package/dist/transformers/preview/pattern/html.js +3 -1
- package/dist/transformers/preview/types.d.ts +13 -0
- package/dist/transformers/utils/build.d.ts +19 -0
- package/dist/transformers/utils/build.js +83 -1
- package/dist/transformers/utils/module.js +2 -1
- package/dist/types/config.d.ts +6 -0
- package/package.json +1 -1
- package/src/app/components/Component/PageSliceResolver.tsx +1 -0
- package/src/app/components/Component/Preview.tsx +380 -189
- package/src/app/components/Component/slices/ComponentDisplaySlice.tsx +4 -0
- package/src/catalog/README.md +3 -0
- package/src/catalog/normalize.ts +2 -0
- package/src/catalog/previews.ts +6 -3
- package/src/catalog/types.ts +8 -0
- package/src/config/loader.ts +2 -0
- package/src/config/runtime.ts +9 -4
- package/src/handlebars.ts +8 -2
- package/src/transformers/plugins/ssr-render.ts +53 -16
- package/src/transformers/preview/component/css.ts +93 -50
- package/src/transformers/preview/pattern/html.ts +3 -1
- package/src/transformers/preview/types.ts +14 -0
- package/src/transformers/utils/build.ts +80 -0
- package/src/transformers/utils/module.ts +4 -2
- package/src/types/config.ts +6 -0
package/README.md
CHANGED
|
@@ -74,17 +74,19 @@ The declaration supplies stable identity, documentation metadata, source entries
|
|
|
74
74
|
|
|
75
75
|
### React
|
|
76
76
|
|
|
77
|
-
Handoff finds the implementation file from its import, so you do not need to repeat the path
|
|
77
|
+
Handoff finds the implementation file from its import, so you do not need to repeat the path, and
|
|
78
|
+
builds the item's CSS from the stylesheets that import chain pulls in, descendants included.
|
|
78
79
|
Previews are named exports. `Preview<typeof item>` gives them the implementation's argument type.
|
|
79
80
|
|
|
80
81
|
```tsx
|
|
81
82
|
// components/example/Component.tsx
|
|
82
83
|
export type ComponentProps = {
|
|
83
84
|
label: string;
|
|
85
|
+
onSelect?: () => void;
|
|
84
86
|
};
|
|
85
87
|
|
|
86
|
-
export default function Component({ label }: ComponentProps) {
|
|
87
|
-
return <div>{label}</div>;
|
|
88
|
+
export default function Component({ label, onSelect }: ComponentProps) {
|
|
89
|
+
return <div onClick={onSelect}>{label}</div>;
|
|
88
90
|
}
|
|
89
91
|
```
|
|
90
92
|
|
|
@@ -112,6 +114,20 @@ export const Default = {
|
|
|
112
114
|
|
|
113
115
|
The export name is the preview name. Use `name` to set a different display title.
|
|
114
116
|
|
|
117
|
+
Arguments are serialized, so a preview that needs an icon, a callback, JSX children, or its own
|
|
118
|
+
state declares `render` instead. It renders as a component, so a capitalized name lets it use hooks, and the item keeps
|
|
119
|
+
documenting the implementation rather than a wrapper. Name a declaration that holds JSX
|
|
120
|
+
`example.handoff.tsx`.
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
export const Interactive = {
|
|
124
|
+
render: function Interactive() {
|
|
125
|
+
const [label, setLabel] = useState('Example');
|
|
126
|
+
return <Component label={label} onSelect={() => setLabel('Selected')} />;
|
|
127
|
+
},
|
|
128
|
+
} satisfies ComponentPreview;
|
|
129
|
+
```
|
|
130
|
+
|
|
115
131
|
### Handlebars
|
|
116
132
|
|
|
117
133
|
The implementation is the path to the template. Arguments are `Record<string, unknown>` by default;
|
|
@@ -88,6 +88,9 @@ const normalizeCatalogItem = (moduleExports, options) => {
|
|
|
88
88
|
entries[entryKey] = path_1.default.resolve(directory, source.file);
|
|
89
89
|
// Existing builders consume template as the primary build input for every renderer.
|
|
90
90
|
entries.template = entries[entryKey];
|
|
91
|
+
// The renderer and the hydration bundle both read a preview `render` from the declaration.
|
|
92
|
+
if (options.declarationPath)
|
|
93
|
+
entries.declaration = options.declarationPath;
|
|
91
94
|
// A wrong renderer builds a broken preview without failing on its own, so a mismatch is fatal.
|
|
92
95
|
// `config/runtime.ts` turns the throw into a skipped item that retries on the next save.
|
|
93
96
|
const claim = (0, renderers_1.sourceForFile)(source.file);
|
package/dist/catalog/previews.js
CHANGED
|
@@ -8,16 +8,17 @@ const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
|
8
8
|
const lodash_1 = require("lodash");
|
|
9
9
|
/**
|
|
10
10
|
* A declaration module exports more than previews: the item itself, the implementation it
|
|
11
|
-
* re-exports, helper values. Accept only a plain object that owns
|
|
12
|
-
* component or helper is never mistaken for a preview.
|
|
11
|
+
* re-exports, helper values. Accept only a plain object that owns one of the preview fields, so an
|
|
12
|
+
* exported component or helper is never mistaken for a preview.
|
|
13
13
|
*
|
|
14
14
|
* `getCsfStoryEntries` does not fit here: on a CSF file every named export is a story, and on a
|
|
15
15
|
* declaration file it is not.
|
|
16
16
|
*/
|
|
17
|
+
const PREVIEW_KEYS = ['args', 'name', 'render'];
|
|
17
18
|
const isPreviewDeclaration = (value) => {
|
|
18
19
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
19
20
|
return false;
|
|
20
|
-
return
|
|
21
|
+
return PREVIEW_KEYS.some((key) => Object.prototype.hasOwnProperty.call(value, key));
|
|
21
22
|
};
|
|
22
23
|
/** Matches an exported binding, an export list, or a CommonJS export assignment. */
|
|
23
24
|
const EXPORT_PATTERNS = [
|
|
@@ -99,7 +100,7 @@ const createCatalogPreviews = (moduleExports, declarationPath) => {
|
|
|
99
100
|
throw new Error(`Preview "${exportName}" uses removed fields. Use args and name. See UPGRADE.md#catalog-items.`);
|
|
100
101
|
}
|
|
101
102
|
const preview = value;
|
|
102
|
-
previews[exportName] = Object.assign({ title: preview.name || (0, lodash_1.startCase)(exportName), values: Object.assign({}, ((_a = preview.args) !== null && _a !== void 0 ? _a : {})), url: (_b = preview.url) !== null && _b !== void 0 ? _b : '' }, (preview.usage ? { usage: preview.usage } : {}));
|
|
103
|
+
previews[exportName] = Object.assign(Object.assign({ title: preview.name || (0, lodash_1.startCase)(exportName), values: Object.assign({}, ((_a = preview.args) !== null && _a !== void 0 ? _a : {})), url: (_b = preview.url) !== null && _b !== void 0 ? _b : '' }, (preview.usage ? { usage: preview.usage } : {})), (typeof preview.render === 'function' ? { render: preview.render } : {}));
|
|
103
104
|
}
|
|
104
105
|
return declarationPath ? (0, exports.orderPreviews)(previews, (0, exports.readExportOrder)(declarationPath)) : previews;
|
|
105
106
|
};
|
package/dist/catalog/types.d.ts
CHANGED
|
@@ -43,6 +43,14 @@ export type CatalogPreview<TArgs = Record<string, unknown>> = {
|
|
|
43
43
|
args?: TArgs;
|
|
44
44
|
usage?: string;
|
|
45
45
|
url?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Renders this preview in place of the implementation, for props it cannot carry as data: an icon
|
|
48
|
+
* component, a callback, JSX children, or state the preview owns.
|
|
49
|
+
*
|
|
50
|
+
* Rendered as a component with `args`, so it can use hooks. The item still documents the
|
|
51
|
+
* implementation, so a wrapper never becomes the published API.
|
|
52
|
+
*/
|
|
53
|
+
render?: (args: TArgs) => unknown;
|
|
46
54
|
};
|
|
47
55
|
/**
|
|
48
56
|
* `__args` exists only in the type system so `Preview<typeof item>` can infer the preview argument type.
|
package/dist/config/loader.js
CHANGED
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.initConfig = exports.initConfigWithMetadata = exports.resolveProfileSelection = exports.HandoffConfigError = exports.CONFIG_FILE_PREFERENCE = void 0;
|
|
7
7
|
const esbuild_1 = __importDefault(require("esbuild"));
|
|
8
|
+
const build_1 = require("../transformers/utils/build");
|
|
8
9
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
9
10
|
const mergeWith_1 = __importDefault(require("lodash/mergeWith"));
|
|
10
11
|
const module_1 = require("module");
|
|
@@ -40,6 +41,7 @@ const evaluateTypeScriptConfig = (filePath, handoffModulePath) => {
|
|
|
40
41
|
target: 'node16',
|
|
41
42
|
logLevel: 'silent',
|
|
42
43
|
external: ['handoff-app'],
|
|
44
|
+
loader: build_1.STYLE_IMPORT_LOADERS,
|
|
43
45
|
});
|
|
44
46
|
const code = (_b = (_a = buildResult.outputFiles) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.text;
|
|
45
47
|
if (!code) {
|
package/dist/config/runtime.js
CHANGED
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.toLowerCaseKeysAndValues = exports.getComponentsForPath = exports.isComponentDirectory = exports.initRuntimeConfig = exports.getCatalogItemDirectories = exports.resolveComponentDeclaration = void 0;
|
|
7
7
|
const esbuild_1 = __importDefault(require("esbuild"));
|
|
8
|
+
const build_1 = require("../transformers/utils/build");
|
|
8
9
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
9
10
|
const module_1 = require("module");
|
|
10
11
|
const path_1 = __importDefault(require("path"));
|
|
@@ -17,7 +18,8 @@ const markdown_1 = require("../utils/markdown");
|
|
|
17
18
|
const pages_1 = require("../utils/pages");
|
|
18
19
|
const path_2 = require("../utils/path");
|
|
19
20
|
const page_1 = require("./normalizers/page");
|
|
20
|
-
|
|
21
|
+
/** `.tsx`/`.jsx` are accepted so a preview can write its `render` as JSX. */
|
|
22
|
+
const DECLARATION_EXTENSIONS = ['tsx', 'ts', 'jsx', 'js', 'cjs'];
|
|
21
23
|
const getDeclarationFiles = (componentBaseName) => DECLARATION_EXTENSIONS.map((ext) => `${componentBaseName}.handoff.${ext}`);
|
|
22
24
|
const findPreferredDeclaration = (componentDir, componentBaseName) => {
|
|
23
25
|
const exactCandidates = getDeclarationFiles(componentBaseName);
|
|
@@ -25,7 +27,7 @@ const findPreferredDeclaration = (componentDir, componentBaseName) => {
|
|
|
25
27
|
if (exactMatch)
|
|
26
28
|
return exactMatch;
|
|
27
29
|
const allFiles = fs_extra_1.default.existsSync(componentDir) ? fs_extra_1.default.readdirSync(componentDir) : [];
|
|
28
|
-
const declarationFiles = allFiles.filter((file) => /\.handoff\.(ts|js|cjs)$/.test(file)).sort((a, b) => a.localeCompare(b));
|
|
30
|
+
const declarationFiles = allFiles.filter((file) => /\.handoff\.(tsx|ts|jsx|js|cjs)$/.test(file)).sort((a, b) => a.localeCompare(b));
|
|
29
31
|
if (!declarationFiles.length)
|
|
30
32
|
return undefined;
|
|
31
33
|
for (const ext of DECLARATION_EXTENSIONS) {
|
|
@@ -52,6 +54,7 @@ const evaluateDeclaration = (filePath, handoffModulePath) => {
|
|
|
52
54
|
logLevel: 'silent',
|
|
53
55
|
jsx: 'automatic',
|
|
54
56
|
external: ['react', 'react-dom', 'handoff-app'],
|
|
57
|
+
loader: build_1.STYLE_IMPORT_LOADERS,
|
|
55
58
|
});
|
|
56
59
|
const code = (_b = (_a = buildResult.outputFiles) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.text;
|
|
57
60
|
if (!code) {
|
|
@@ -178,7 +181,7 @@ const initRuntimeConfig = (handoff) => {
|
|
|
178
181
|
const declaration = (0, exports.resolveComponentDeclaration)(itemPath, itemBaseName);
|
|
179
182
|
if (!declaration) {
|
|
180
183
|
const declarationFiles = getDeclarationFiles(itemBaseName);
|
|
181
|
-
logger_1.Logger.warn(`Missing catalog declaration (use .handoff.ts, .handoff.js, or .handoff.cjs; see UPGRADE.md#catalog-items): ${path_1.default.resolve(itemPath, declarationFiles.join(' or '))}`);
|
|
184
|
+
logger_1.Logger.warn(`Missing catalog declaration (use .handoff.tsx, .handoff.ts, .handoff.jsx, .handoff.js, or .handoff.cjs; see UPGRADE.md#catalog-items): ${path_1.default.resolve(itemPath, declarationFiles.join(' or '))}`);
|
|
182
185
|
continue;
|
|
183
186
|
}
|
|
184
187
|
const declarationPath = path_1.default.resolve(itemPath, declaration.fileName);
|
|
@@ -361,8 +364,8 @@ const injectPatternPreviews = (result) => {
|
|
|
361
364
|
}
|
|
362
365
|
// Case 3: args present (with or without preview base) -> create synthetic preview
|
|
363
366
|
let resolvedValues = {};
|
|
367
|
+
const basePreview = ref.preview ? (_b = component.previews) === null || _b === void 0 ? void 0 : _b[ref.preview] : undefined;
|
|
364
368
|
if (ref.preview) {
|
|
365
|
-
const basePreview = (_b = component.previews) === null || _b === void 0 ? void 0 : _b[ref.preview];
|
|
366
369
|
if (basePreview) {
|
|
367
370
|
resolvedValues = Object.assign({}, basePreview.values);
|
|
368
371
|
}
|
|
@@ -374,12 +377,7 @@ const injectPatternPreviews = (result) => {
|
|
|
374
377
|
resolvedValues = Object.assign(Object.assign({}, resolvedValues), ref.args);
|
|
375
378
|
const syntheticKey = `__pattern_${patternId}_${i}`;
|
|
376
379
|
component.internalPatternPreviews = component.internalPatternPreviews || {};
|
|
377
|
-
component.internalPatternPreviews[syntheticKey] = {
|
|
378
|
-
title: syntheticKey,
|
|
379
|
-
values: resolvedValues,
|
|
380
|
-
url: '',
|
|
381
|
-
sourcePreview: ref.preview,
|
|
382
|
-
};
|
|
380
|
+
component.internalPatternPreviews[syntheticKey] = Object.assign({ title: syntheticKey, values: resolvedValues, url: '', sourcePreview: ref.preview }, ((basePreview === null || basePreview === void 0 ? void 0 : basePreview.render) ? { render: basePreview.render } : {}));
|
|
383
381
|
ref.resolvedPreview = syntheticKey;
|
|
384
382
|
if (ref.resolved === undefined) {
|
|
385
383
|
ref.resolved = true;
|
package/dist/handlebars.d.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
import type { CatalogItem, CatalogItemMeta } from './catalog/types';
|
|
2
|
-
export type { CatalogItem, CatalogItemMeta,
|
|
1
|
+
import type { CatalogItem, CatalogItemMeta, CatalogPreview as RendererPreview } from './catalog/types';
|
|
2
|
+
export type { CatalogItem, CatalogItemMeta, CompositionRef, SourceDescriptor } from './catalog/types';
|
|
3
|
+
/** A template renderer never calls a preview `render`, so declaring one is an error, not a no-op. */
|
|
4
|
+
export type CatalogPreview<TArgs = Record<string, unknown>> = Omit<RendererPreview<TArgs>, 'render'> & {
|
|
5
|
+
render?: never;
|
|
6
|
+
};
|
|
7
|
+
/** Preview type for a Handlebars catalog item, with its argument type applied. */
|
|
8
|
+
export type Preview<TItem> = TItem extends {
|
|
9
|
+
readonly __args?: infer TArgs;
|
|
10
|
+
} ? CatalogPreview<Partial<NonNullable<TArgs>>> : never;
|
|
3
11
|
/**
|
|
4
12
|
* Declares a Handlebars catalog item. `implementation` is the path to its `.hbs` template. The
|
|
5
13
|
* renderer comes from this module, so the build fails an item whose file does not match it.
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
//
|
|
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
|
|
98
|
-
if (
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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
|