handoff-app 2.0.0-5 → 2.0.0-7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +174 -41
- package/UPGRADE.md +198 -0
- package/config/config.template.js +1 -4
- package/config/config.template.ts +1 -2
- package/dist/app-builder/build.js +11 -6
- package/dist/app-builder/client-config.js +1 -0
- package/dist/app-builder/config-diff/strategies/pattern.js +3 -3
- package/dist/app-builder/watchers/component.js +5 -5
- package/dist/app-builder/watchers/utils.d.ts +2 -3
- package/dist/app-builder/watchers.d.ts +8 -15
- package/dist/app-builder/watchers.js +53 -54
- package/dist/cache/build-cache.js +18 -22
- package/dist/catalog/define.d.ts +6 -0
- package/dist/catalog/define.js +72 -0
- package/dist/catalog/implementation.d.ts +27 -0
- package/dist/catalog/implementation.js +133 -0
- package/dist/catalog/index.d.ts +6 -0
- package/dist/catalog/index.js +22 -0
- package/dist/catalog/normalize.d.ts +16 -0
- package/dist/catalog/normalize.js +115 -0
- package/dist/catalog/previews.d.ts +18 -0
- package/dist/catalog/previews.js +106 -0
- package/dist/catalog/renderers.d.ts +43 -0
- package/dist/catalog/renderers.js +62 -0
- package/dist/catalog/types.d.ts +63 -0
- package/dist/cli/make.d.ts +0 -5
- package/dist/cli/make.js +22 -67
- package/dist/cli/scaffold.js +25 -33
- package/dist/commands/checkout/index.d.ts +7 -4
- package/dist/commands/checkout/index.js +9 -6
- package/dist/commands/compat/push-args.d.ts +2 -2
- package/dist/commands/index.js +0 -2
- package/dist/commands/publish/index.d.ts +9 -6
- package/dist/commands/publish/index.js +11 -8
- package/dist/commands/pull/index.js +1 -1
- package/dist/commands/push/index.js +1 -1
- package/dist/commands/utils.d.ts +15 -4
- package/dist/commands/utils.js +19 -6
- package/dist/config/catalog-include.d.ts +43 -0
- package/dist/config/catalog-include.js +203 -0
- package/dist/config/defaults.d.ts +2 -0
- package/dist/config/defaults.js +4 -1
- package/dist/config/index.d.ts +3 -3
- package/dist/config/index.js +6 -5
- package/dist/config/loader.js +9 -1
- package/dist/config/runtime.d.ts +5 -6
- package/dist/config/runtime.js +154 -154
- package/dist/handlebars.d.ts +9 -0
- package/dist/handlebars.js +16 -0
- package/dist/index.d.ts +15 -4
- package/dist/index.js +29 -15
- package/dist/pattern.d.ts +7 -0
- package/dist/pattern.js +13 -0
- package/dist/react.d.ts +43 -0
- package/dist/react.js +11 -0
- package/dist/registry/asset-storage/define.d.ts +1 -1
- package/dist/registry/asset-storage/define.js +1 -1
- package/dist/registry/checkout/index.d.ts +9 -0
- package/dist/registry/checkout/index.js +335 -149
- package/dist/registry/db/schema.d.ts +34 -0
- package/dist/registry/db/schema.js +2 -0
- package/dist/registry/publish/index.d.ts +13 -2
- package/dist/registry/publish/index.js +109 -25
- package/dist/registry/publish/package.js +14 -1
- package/dist/registry/selection.d.ts +20 -0
- package/dist/registry/selection.js +37 -1
- package/dist/store/filesystem.d.ts +1 -1
- package/dist/store/filesystem.js +7 -7
- package/dist/store/registry.js +24 -3
- package/dist/transformers/plugins/csf-render.js +4 -2
- package/dist/transformers/plugins/ssr-render.js +14 -5
- package/dist/transformers/preview/component/api.d.ts +8 -0
- package/dist/transformers/preview/component/api.js +11 -15
- package/dist/transformers/preview/component/artifacts.d.ts +1 -1
- package/dist/transformers/preview/component/artifacts.js +11 -7
- package/dist/transformers/preview/component/builder.js +15 -18
- package/dist/transformers/preview/component/css.d.ts +2 -0
- package/dist/transformers/preview/component/css.js +8 -11
- package/dist/transformers/preview/component/html.js +18 -16
- package/dist/transformers/preview/component/javascript.js +2 -4
- package/dist/transformers/preview/pattern/builder.js +31 -18
- package/dist/transformers/preview/types.d.ts +15 -1
- package/dist/types/config.d.ts +36 -11
- package/docs/api.md +3 -0
- package/docs/cli.md +1 -1
- package/drizzle/0006_add_component_source_format.sql +3 -0
- package/drizzle/meta/0006_snapshot.json +1941 -0
- package/drizzle/meta/_journal.json +8 -1
- package/package.json +37 -2
- package/src/app/components/Component/Preview.tsx +36 -50
- package/src/app/components/CopyToClipboard.tsx +27 -26
- package/src/app/components/Layout/Header.tsx +4 -12
- package/src/app/components/Layout/RuntimeModeBadge.tsx +18 -0
- package/src/app/components/Markdown/CodeHighlight.tsx +73 -70
- package/src/app/components/McpIntegration/McpConfigDialog.tsx +349 -0
- package/src/app/components/McpIntegration/McpIcon.tsx +10 -0
- package/src/app/components/Navigation/MobileNav.tsx +3 -1
- package/src/app/components/context/PreviewContext.tsx +5 -5
- package/src/app/components/ui/dialog.tsx +73 -0
- package/src/app/components/util/index.ts +7 -15
- package/src/app/lib/docs-api/backend.ts +5 -1
- package/src/app/lib/docs-api/page-discovery.ts +1 -6
- package/src/app/lib/docs-api/page-rendering.ts +21 -1
- package/src/app/lib/docs-api/records.ts +25 -10
- package/src/app/lib/docs-api/registry.ts +4 -2
- package/src/app/lib/docs-api/runtime-config.ts +5 -0
- package/src/app/lib/docs-api/search.ts +7 -2
- package/src/app/lib/mcp/auth.ts +64 -0
- package/src/app/lib/mcp/index.ts +2 -0
- package/src/app/lib/mcp/server.ts +343 -0
- package/src/app/lib/mcp/shape.ts +596 -0
- package/src/app/lib/registry-api/transfer.ts +7 -5
- package/src/app/next.config.mjs +3 -0
- package/src/app/pages/[...slug]/index.tsx +16 -30
- package/src/app/pages/api/mcp.ts +56 -0
- package/src/app/pages/index.tsx +10 -9
- package/src/app-builder/build.ts +12 -8
- package/src/app-builder/client-config.ts +2 -1
- package/src/app-builder/config-diff/strategies/pattern.ts +1 -1
- package/src/app-builder/watchers/component.ts +3 -3
- package/src/app-builder/watchers/utils.ts +2 -3
- package/src/app-builder/watchers.ts +46 -50
- package/src/cache/build-cache.ts +11 -14
- package/src/catalog/README.md +92 -0
- package/src/catalog/define.ts +79 -0
- package/src/catalog/implementation.ts +145 -0
- package/src/catalog/index.ts +31 -0
- package/src/catalog/normalize.ts +143 -0
- package/src/catalog/previews.ts +111 -0
- package/src/catalog/renderers.ts +94 -0
- package/src/catalog/types.ts +74 -0
- package/src/cli/README.md +1 -1
- package/src/cli/make.ts +20 -72
- package/src/cli/scaffold.ts +25 -31
- package/src/commands/README.md +1 -1
- package/src/commands/checkout/index.ts +17 -7
- package/src/commands/compat/push-args.ts +2 -2
- package/src/commands/index.ts +0 -2
- package/src/commands/publish/index.ts +19 -9
- package/src/commands/pull/index.ts +2 -2
- package/src/commands/push/index.ts +2 -2
- package/src/commands/utils.ts +22 -7
- package/src/config/README.md +2 -1
- package/src/config/catalog-include.ts +227 -0
- package/src/config/defaults.ts +3 -0
- package/src/config/index.ts +4 -4
- package/src/config/loader.ts +12 -0
- package/src/config/runtime.ts +186 -191
- package/src/handlebars.ts +18 -0
- package/src/index.ts +39 -26
- package/src/pattern.ts +14 -0
- package/src/react.ts +52 -0
- package/src/registry/asset-storage/define.ts +1 -1
- package/src/registry/checkout/index.ts +416 -152
- package/src/registry/db/schema.ts +2 -0
- package/src/registry/publish/index.ts +123 -26
- package/src/registry/publish/package.ts +14 -1
- package/src/registry/selection.ts +44 -0
- package/src/store/filesystem.ts +4 -4
- package/src/store/registry.ts +13 -2
- package/src/transformers/plugins/csf-render.ts +4 -2
- package/src/transformers/plugins/ssr-render.ts +15 -5
- package/src/transformers/preview/component/api.ts +13 -1
- package/src/transformers/preview/component/artifacts.ts +8 -4
- package/src/transformers/preview/component/builder.ts +17 -4
- package/src/transformers/preview/component/css.ts +6 -10
- package/src/transformers/preview/component/html.ts +30 -19
- package/src/transformers/preview/component/javascript.ts +1 -3
- package/src/transformers/preview/pattern/builder.ts +33 -20
- package/src/transformers/preview/types.ts +15 -1
- package/src/types/config.ts +38 -11
- package/templates/default/README.md.tpl +10 -0
- package/templates/default/components/button/button.react.handoff.js.tpl +19 -23
- package/templates/default/components/button/button.react.handoff.ts.tpl +22 -22
- package/templates/default/handoff.config.js.tpl +11 -18
- package/templates/default/handoff.config.ts.tpl +11 -18
- package/templates/default/handoff.config.with-examples.js.tpl +12 -16
- package/templates/default/handoff.config.with-examples.ts.tpl +12 -16
- package/config/templates/component/template.json +0 -49
- package/config/templates/template.html +0 -7
- package/dist/commands/make/template.d.ts +0 -8
- package/dist/commands/make/template.js +0 -43
- package/dist/config/entries.d.ts +0 -47
- package/dist/config/entries.js +0 -162
- package/dist/config/normalizers/declaration.d.ts +0 -11
- package/dist/config/normalizers/declaration.js +0 -122
- package/dist/config/normalizers/pattern.d.ts +0 -8
- package/dist/config/normalizers/pattern.js +0 -41
- package/dist/declarations/helpers.d.ts +0 -6
- package/dist/declarations/helpers.js +0 -19
- package/dist/declarations/index.d.ts +0 -2
- package/dist/declarations/index.js +0 -9
- package/dist/declarations/types.d.ts +0 -60
- package/src/commands/make/template.ts +0 -46
- package/src/config/entries.ts +0 -175
- package/src/config/normalizers/declaration.ts +0 -179
- package/src/config/normalizers/pattern.ts +0 -59
- package/src/declarations/helpers.ts +0 -36
- package/src/declarations/index.ts +0 -19
- package/src/declarations/types.ts +0 -70
- /package/dist/{declarations → catalog}/types.js +0 -0
|
@@ -108,6 +108,8 @@ export const components = pgTable(
|
|
|
108
108
|
group: text('group'),
|
|
109
109
|
type: text('type'),
|
|
110
110
|
renderer: text('renderer'),
|
|
111
|
+
/** How the implementation file is written (`csf`), when the renderer alone does not say. */
|
|
112
|
+
sourceFormat: text('source_format'),
|
|
111
113
|
tags: jsonb('tags').$type<string[]>(),
|
|
112
114
|
categories: jsonb('categories').$type<string[]>(),
|
|
113
115
|
/** Full normalized component record. */
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Connected-workspace publish orchestration.
|
|
3
3
|
*
|
|
4
|
-
* `publish <
|
|
5
|
-
*
|
|
4
|
+
* `publish <catalog|pages> <id>` prepares one entity and uploads its record, source files, applicable
|
|
5
|
+
* rendered artifacts, and build metadata through the shared registry client. A catalog item's lane
|
|
6
|
+
* comes from its declaration, so the caller names items and this module resolves the lane.
|
|
6
7
|
*
|
|
7
8
|
* The build is targeted: a component builds the global artifacts + that component; a pattern builds
|
|
8
9
|
* the global artifacts + the components it composes + the pattern itself. This is the smallest build
|
|
@@ -18,7 +19,7 @@ import { Logger } from '../../utils/logger';
|
|
|
18
19
|
import { createRegistryClient, RegistryClientError, type RegistryClient } from '../client';
|
|
19
20
|
import { resolveAuthenticatedRegistryConnection } from '../connection';
|
|
20
21
|
import type { EntitySummary, TransferEntityKind } from '../transfer';
|
|
21
|
-
import { selectIds } from '../selection';
|
|
22
|
+
import { quoteIds, selectIds, splitCatalogIds } from '../selection';
|
|
22
23
|
import { describePublishError, describeUploadFailure } from './errors';
|
|
23
24
|
import { assertRequiredArtifactsPresent, buildPublishPackage, PublishPackageError } from './package';
|
|
24
25
|
|
|
@@ -45,6 +46,26 @@ export const assertPublishableWorkspace = (handoff: Handoff): void => {
|
|
|
45
46
|
}
|
|
46
47
|
};
|
|
47
48
|
|
|
49
|
+
/** The entity lanes a catalog run visits, in dependency order: a pattern composes components. */
|
|
50
|
+
const CATALOG_LANES = ['component', 'pattern'] as const;
|
|
51
|
+
|
|
52
|
+
const isCatalogLane = (kind: TransferEntityKind): boolean => CATALOG_LANES.some((lane) => lane === kind);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Refuse a catalog run while the workspace declares one id twice. Discovery keeps the first
|
|
56
|
+
* declaration and skips the rest, so a publish uploads a catalog that silently omits an item.
|
|
57
|
+
*/
|
|
58
|
+
const assertUniqueCatalogIds = (handoff: Handoff): void => {
|
|
59
|
+
const duplicates = handoff.runtimeConfig?.duplicateCatalogIds ?? [];
|
|
60
|
+
if (duplicates.length === 0) return;
|
|
61
|
+
|
|
62
|
+
throw new PublishError(
|
|
63
|
+
`Catalog id${duplicates.length > 1 ? 's' : ''} declared twice: ` +
|
|
64
|
+
duplicates.map(({ id, kept, skipped }) => `"${id}" (${kept} and ${skipped})`).join('; ') +
|
|
65
|
+
'. Give each catalog item a unique id.'
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
48
69
|
/**
|
|
49
70
|
* Ensure the workspace is a connected workspace able to publish: workspace runtime mode (registry
|
|
50
71
|
* hosts do not publish) and a resolved registry URL + access token. Throws an actionable
|
|
@@ -103,7 +124,7 @@ const runTargetedBuild = async (handoff: Handoff, kind: TransferEntityKind, id:
|
|
|
103
124
|
// Pages carry no rendered artifacts (raw markdown is rendered at runtime), so there is nothing to
|
|
104
125
|
// build — the package is assembled directly from the discovered page record + its `.md`.
|
|
105
126
|
if (kind === 'page') {
|
|
106
|
-
if (!handoff.runtimeConfig?.
|
|
127
|
+
if (!handoff.runtimeConfig?.entities.pages[id]) {
|
|
107
128
|
throw new PublishPackageError(`Page "${id}" is not declared in this workspace.`);
|
|
108
129
|
}
|
|
109
130
|
return;
|
|
@@ -115,14 +136,14 @@ const runTargetedBuild = async (handoff: Handoff, kind: TransferEntityKind, id:
|
|
|
115
136
|
await buildMainCss(handoff);
|
|
116
137
|
|
|
117
138
|
if (kind === 'component') {
|
|
118
|
-
if (!handoff.runtimeConfig?.
|
|
139
|
+
if (!handoff.runtimeConfig?.entities.components[id]) {
|
|
119
140
|
throw new PublishPackageError(`Component "${id}" is not declared in this workspace.`);
|
|
120
141
|
}
|
|
121
142
|
await processComponents(handoff, id);
|
|
122
143
|
return;
|
|
123
144
|
}
|
|
124
145
|
|
|
125
|
-
const pattern = handoff.runtimeConfig?.
|
|
146
|
+
const pattern = handoff.runtimeConfig?.entities.patterns[id];
|
|
126
147
|
if (!pattern) {
|
|
127
148
|
throw new PublishPackageError(`Pattern "${id}" is not declared in this workspace.`);
|
|
128
149
|
}
|
|
@@ -130,7 +151,7 @@ const runTargetedBuild = async (handoff: Handoff, kind: TransferEntityKind, id:
|
|
|
130
151
|
// then compose just this pattern.
|
|
131
152
|
const referencedComponentIds = new Set(pattern.components.map((ref) => ref.id));
|
|
132
153
|
for (const componentId of Array.from(referencedComponentIds)) {
|
|
133
|
-
if (handoff.runtimeConfig?.
|
|
154
|
+
if (handoff.runtimeConfig?.entities.components[componentId]) {
|
|
134
155
|
await processComponents(handoff, componentId);
|
|
135
156
|
}
|
|
136
157
|
}
|
|
@@ -142,6 +163,7 @@ const runTargetedBuild = async (handoff: Handoff, kind: TransferEntityKind, id:
|
|
|
142
163
|
* receive a targeted build, while pages are packaged directly from their source.
|
|
143
164
|
*/
|
|
144
165
|
export const publishEntity = async (handoff: Handoff, kind: TransferEntityKind, id: string): Promise<void> => {
|
|
166
|
+
if (isCatalogLane(kind)) assertUniqueCatalogIds(handoff);
|
|
145
167
|
const { client, url } = await resolveTransport(handoff);
|
|
146
168
|
|
|
147
169
|
if (!handoff.skipBuild) {
|
|
@@ -218,24 +240,18 @@ const selectEntityIds = (available: string[], requested: string[] | undefined, k
|
|
|
218
240
|
};
|
|
219
241
|
|
|
220
242
|
/**
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
243
|
+
* Assemble and upload the selected entities of one kind. The caller runs the build, because a catalog
|
|
244
|
+
* run covers both entity lanes from one build.
|
|
245
|
+
*
|
|
246
|
+
* A per-entity failure is collected and never aborts the rest. Failures are reported here, and the
|
|
247
|
+
* count is returned so the caller raises one error for the whole run.
|
|
226
248
|
*/
|
|
227
|
-
|
|
228
|
-
const { client, url } =
|
|
229
|
-
|
|
230
|
-
if (!handoff.skipBuild) {
|
|
231
|
-
Logger.info(`Building ${kind}s for publish…`);
|
|
232
|
-
await runBulkBuild(handoff, kind);
|
|
233
|
-
}
|
|
234
|
-
|
|
249
|
+
const uploadEntities = async (handoff: Handoff, transport: PublishTransport, kind: TransferEntityKind, ids?: string[]): Promise<number> => {
|
|
250
|
+
const { client, url } = transport;
|
|
235
251
|
const targets = selectEntityIds(await listWorkspaceEntityIds(handoff, kind), ids, kind);
|
|
236
252
|
if (targets.length === 0) {
|
|
237
253
|
Logger.success(`No ${kind}s are declared in this workspace; nothing to publish.`);
|
|
238
|
-
return;
|
|
254
|
+
return 0;
|
|
239
255
|
}
|
|
240
256
|
|
|
241
257
|
// Without a registry there is nothing to compare against, so a dry run reports every target.
|
|
@@ -281,10 +297,91 @@ export const publishEntities = async (handoff: Handoff, kind: TransferEntityKind
|
|
|
281
297
|
`${kind[0].toUpperCase()}${kind.slice(1)}s publish complete — ${published} ${handoff.dryRun ? 'would be published' : 'published'}, ` +
|
|
282
298
|
`${unchanged} unchanged${failed.length ? `, ${failed.length} failed` : ''}.`
|
|
283
299
|
);
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
300
|
+
for (const failure of failed) {
|
|
301
|
+
Logger.error(` - ${failure.id}: ${failure.message}`);
|
|
302
|
+
}
|
|
303
|
+
return failed.length;
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Publish the components, patterns or pages declared in this connected workspace: every one of the
|
|
308
|
+
* kind, or only `ids` when given. Builds the kind once, assembles each entity's package, skips
|
|
309
|
+
* entities whose content hash already matches the registry (unless `--force`), and reports
|
|
310
|
+
* published/unchanged/failed counts. A per-entity failure is collected and never aborts the rest; the
|
|
311
|
+
* run throws at the end if any entity failed.
|
|
312
|
+
*/
|
|
313
|
+
export const publishEntities = async (handoff: Handoff, kind: TransferEntityKind, ids?: string[]): Promise<void> => {
|
|
314
|
+
if (isCatalogLane(kind)) assertUniqueCatalogIds(handoff);
|
|
315
|
+
const transport = await resolveTransport(handoff);
|
|
316
|
+
|
|
317
|
+
if (!handoff.skipBuild) {
|
|
318
|
+
Logger.info(`Building ${kind}s for publish…`);
|
|
319
|
+
await runBulkBuild(handoff, kind);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const failed = await uploadEntities(handoff, transport, kind, ids);
|
|
323
|
+
if (failed > 0) {
|
|
324
|
+
throw new PublishError(`${failed} ${kind}(s) failed to publish.`);
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Resolve which lane each requested catalog id belongs to. Without ids every lane runs in full. An
|
|
330
|
+
* unknown id is reported against the whole catalog, because the caller never chose a lane to report
|
|
331
|
+
* it against.
|
|
332
|
+
*/
|
|
333
|
+
const resolveCatalogLanes = async (handoff: Handoff, ids?: string[]): Promise<{ kind: TransferEntityKind; ids?: string[] }[]> => {
|
|
334
|
+
if (!ids) {
|
|
335
|
+
return CATALOG_LANES.map((kind) => ({ kind }));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const available = {
|
|
339
|
+
component: await listWorkspaceEntityIds(handoff, 'component'),
|
|
340
|
+
pattern: await listWorkspaceEntityIds(handoff, 'pattern'),
|
|
341
|
+
};
|
|
342
|
+
const { lanes, unknown, ambiguous } = splitCatalogIds(ids, available);
|
|
343
|
+
|
|
344
|
+
if (ambiguous.length > 0) {
|
|
345
|
+
throw new PublishError(
|
|
346
|
+
`${quoteIds(ambiguous)} names both a component and a pattern in this workspace. Give each catalog item a unique id.`
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
if (unknown.length > 0) {
|
|
350
|
+
const declared = [...available.component, ...available.pattern];
|
|
351
|
+
throw new PublishError(
|
|
352
|
+
`No catalog item named ${quoteIds(unknown)} is declared in this workspace. ` +
|
|
353
|
+
`Declared catalog items: ${declared.join(', ') || '(none)'}.`
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return CATALOG_LANES.map((kind) => ({ kind, ids: lanes[kind] })).filter((lane) => lane.ids.length > 0);
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Publish the catalog items declared in this connected workspace: every one, or only `ids` when
|
|
362
|
+
* given. An item takes the component lane or the pattern lane depending on whether it declares an
|
|
363
|
+
* implementation or a composition, so the caller names items and never names a lane.
|
|
364
|
+
*
|
|
365
|
+
* One build covers both lanes, because building the patterns already builds every component they
|
|
366
|
+
* compose. A failing lane is reported and never aborts the other. The run throws at the end if any
|
|
367
|
+
* item failed.
|
|
368
|
+
*/
|
|
369
|
+
export const publishCatalog = async (handoff: Handoff, ids?: string[]): Promise<void> => {
|
|
370
|
+
assertUniqueCatalogIds(handoff);
|
|
371
|
+
const transport = await resolveTransport(handoff);
|
|
372
|
+
const lanes = await resolveCatalogLanes(handoff, ids);
|
|
373
|
+
|
|
374
|
+
if (!handoff.skipBuild) {
|
|
375
|
+
Logger.info('Building catalog items for publish…');
|
|
376
|
+
// The pattern build runs the component build first, so one call covers both lanes.
|
|
377
|
+
await runBulkBuild(handoff, 'pattern');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
let failed = 0;
|
|
381
|
+
for (const lane of lanes) {
|
|
382
|
+
failed += await uploadEntities(handoff, transport, lane.kind, lane.ids);
|
|
383
|
+
}
|
|
384
|
+
if (failed > 0) {
|
|
385
|
+
throw new PublishError(`${failed} catalog item(s) failed to publish.`);
|
|
289
386
|
}
|
|
290
387
|
};
|
|
@@ -33,7 +33,8 @@ export class PublishPackageError extends Error {
|
|
|
33
33
|
type RenderedEntityKind = Extract<TransferEntityKind, 'component' | 'pattern'>;
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
* Build a registry-safe `item` record: the built summary enriched with the renderer
|
|
36
|
+
* Build a registry-safe `item` record: the built summary enriched with the renderer, its source
|
|
37
|
+
* format, and the entry
|
|
37
38
|
* source-file paths **remapped** from absolute workspace locations to the registry-safe relative
|
|
38
39
|
* paths the uploaded source files use. Workspace-absolute paths never cross to the registry, and a
|
|
39
40
|
* later checkout can reconstruct the entity from the relative entries + uploaded files.
|
|
@@ -58,6 +59,18 @@ const enrichItem = async (
|
|
|
58
59
|
item.renderer = renderer;
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
const sourceFormat = (runtime as { sourceFormat?: string }).sourceFormat;
|
|
63
|
+
if (sourceFormat) {
|
|
64
|
+
item.sourceFormat = sourceFormat;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const componentExport = (runtime as ComponentListObject).componentExport;
|
|
68
|
+
if (componentExport) item.componentExport = componentExport;
|
|
69
|
+
if (kind === 'component') {
|
|
70
|
+
// PostgreSQL jsonb does not preserve object key order.
|
|
71
|
+
item.previewOrder = Object.keys((runtime as ComponentListObject).previews ?? {});
|
|
72
|
+
}
|
|
73
|
+
|
|
61
74
|
if (rawEntries && sourceDir) {
|
|
62
75
|
const remapped: Record<string, string> = {};
|
|
63
76
|
for (const [key, value] of Object.entries(rawEntries)) {
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
* Every bulk operation discovers what is available (declared locally or published remotely) and may
|
|
5
5
|
* be asked for only part of it. Splitting the request into what exists and what does not lets each
|
|
6
6
|
* caller build its own actionable error while the matching lives in one place.
|
|
7
|
+
*
|
|
8
|
+
* A catalog request needs one more step. A catalog item normalizes into a component or a pattern
|
|
9
|
+
* from its declaration, so the CLI names the item and the lane is looked up here.
|
|
7
10
|
*/
|
|
8
11
|
|
|
9
12
|
/** Narrow an available list to the requested subset, separating out ids that do not exist. */
|
|
@@ -17,3 +20,44 @@ export const selectIds = (available: string[], requested?: string[]): { selected
|
|
|
17
20
|
unknown: requested.filter((id) => !availableIds.has(id)),
|
|
18
21
|
};
|
|
19
22
|
};
|
|
23
|
+
|
|
24
|
+
/** Catalog item ids grouped by lane. */
|
|
25
|
+
export interface CatalogLanes {
|
|
26
|
+
component: string[];
|
|
27
|
+
pattern: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Route each requested catalog id to its lane. The two lanes are keyed separately, so nothing stops a
|
|
32
|
+
* workspace from declaring one id twice. Such an id is reported rather than guessed, because picking
|
|
33
|
+
* a lane silently would act on the wrong entity.
|
|
34
|
+
*/
|
|
35
|
+
export const splitCatalogIds = (
|
|
36
|
+
requested: string[],
|
|
37
|
+
available: CatalogLanes
|
|
38
|
+
): { lanes: CatalogLanes; unknown: string[]; ambiguous: string[] } => {
|
|
39
|
+
const components = new Set(available.component);
|
|
40
|
+
const patterns = new Set(available.pattern);
|
|
41
|
+
const lanes: CatalogLanes = { component: [], pattern: [] };
|
|
42
|
+
const unknown: string[] = [];
|
|
43
|
+
const ambiguous: string[] = [];
|
|
44
|
+
|
|
45
|
+
for (const id of requested) {
|
|
46
|
+
const isComponent = components.has(id);
|
|
47
|
+
const isPattern = patterns.has(id);
|
|
48
|
+
if (isComponent && isPattern) {
|
|
49
|
+
ambiguous.push(id);
|
|
50
|
+
} else if (isComponent) {
|
|
51
|
+
lanes.component.push(id);
|
|
52
|
+
} else if (isPattern) {
|
|
53
|
+
lanes.pattern.push(id);
|
|
54
|
+
} else {
|
|
55
|
+
unknown.push(id);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { lanes, unknown, ambiguous };
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** Quote ids for an error message: `"button", "card"`. */
|
|
63
|
+
export const quoteIds = (ids: string[]): string => ids.map((id) => `"${id}"`).join(', ');
|
package/src/store/filesystem.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Filesystem-backed implementation of the normalized store abstraction.
|
|
3
3
|
*
|
|
4
|
-
* It is a thin read view over the already-resolved `runtimeConfig.
|
|
4
|
+
* It is a thin read view over the already-resolved `runtimeConfig.entities`, so the records it
|
|
5
5
|
* returns are the same normalized component/pattern records the build consumes today — discovery
|
|
6
6
|
* and normalization (modern `*.handoff.*` + legacy `{dirname}.*`, react/handlebars/csf/plain-object
|
|
7
7
|
* declarations, CSF story discovery, `entries` resolution) all happen upstream in
|
|
@@ -71,7 +71,7 @@ export class FilesystemComponentStore implements ComponentStore {
|
|
|
71
71
|
constructor(private readonly context: FilesystemStoreContext) {}
|
|
72
72
|
|
|
73
73
|
private get records(): Record<string, ComponentListObject> {
|
|
74
|
-
return this.context.runtimeConfig?.
|
|
74
|
+
return this.context.runtimeConfig?.entities.components ?? {};
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
list(): ComponentListObject[] {
|
|
@@ -97,7 +97,7 @@ export class FilesystemPatternStore implements PatternStore {
|
|
|
97
97
|
constructor(private readonly context: FilesystemStoreContext) {}
|
|
98
98
|
|
|
99
99
|
private get records(): Record<string, PatternListObject> {
|
|
100
|
-
return this.context.runtimeConfig?.
|
|
100
|
+
return this.context.runtimeConfig?.entities.patterns ?? {};
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
list(): PatternListObject[] {
|
|
@@ -145,7 +145,7 @@ export class FilesystemPageStore implements PageStore {
|
|
|
145
145
|
constructor(private readonly context: FilesystemStoreContext) {}
|
|
146
146
|
|
|
147
147
|
private get records(): Record<string, PageListObject> {
|
|
148
|
-
return this.context.runtimeConfig?.
|
|
148
|
+
return this.context.runtimeConfig?.entities.pages ?? {};
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
list(): PageListObject[] {
|
package/src/store/registry.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* returned exactly as it was published.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import { orderPreviews } from '../catalog/previews';
|
|
17
18
|
import { and, eq, inArray, or, sql, type SQLWrapper } from 'drizzle-orm';
|
|
18
19
|
import type { AssetStorage, AssetStorageReadResult } from '../registry/asset-storage/types';
|
|
19
20
|
import type { RegistryDatabase } from '../registry/db/client';
|
|
@@ -86,17 +87,27 @@ const toTextFileResource = (row: RegistryFileRow): TextFileResource | null => {
|
|
|
86
87
|
};
|
|
87
88
|
};
|
|
88
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Restore source order after jsonb storage without exposing transfer-only metadata. `entries` and
|
|
92
|
+
* `componentExport` describe the publisher's source layout and exist for checkout. The workspace
|
|
93
|
+
* has no equivalent, so the docs API must not serve them.
|
|
94
|
+
*/
|
|
95
|
+
const readComponentRecord = (record: ComponentListObject): ComponentListObject => {
|
|
96
|
+
const { previewOrder, entries, componentExport, ...item } = record;
|
|
97
|
+
return { ...item, previews: orderPreviews(item.previews ?? {}, previewOrder ?? []) };
|
|
98
|
+
};
|
|
99
|
+
|
|
89
100
|
export class RegistryComponentStore implements ComponentStore {
|
|
90
101
|
constructor(private readonly context: RegistryStoreContext) {}
|
|
91
102
|
|
|
92
103
|
async list(): Promise<ComponentListObject[]> {
|
|
93
104
|
const rows = await this.context.db.select({ record: components.record }).from(components);
|
|
94
|
-
return rows.map((row) => row.record);
|
|
105
|
+
return rows.map((row) => readComponentRecord(row.record));
|
|
95
106
|
}
|
|
96
107
|
|
|
97
108
|
async get(id: string): Promise<ComponentListObject | null> {
|
|
98
109
|
const rows = await this.context.db.select({ record: components.record }).from(components).where(eq(components.id, id)).limit(1);
|
|
99
|
-
return rows[0]
|
|
110
|
+
return rows[0] ? readComponentRecord(rows[0].record) : null;
|
|
100
111
|
}
|
|
101
112
|
|
|
102
113
|
/**
|
|
@@ -7,6 +7,7 @@ import ReactDOMServer from 'react-dom/server';
|
|
|
7
7
|
import reactElementToJSXString from 'react-element-to-jsx-string';
|
|
8
8
|
import { Plugin } from 'vite';
|
|
9
9
|
import Handoff from '../..';
|
|
10
|
+
import { orderPreviews, readExportOrderFromSource } from '../../catalog';
|
|
10
11
|
import { Logger } from '../../utils/logger';
|
|
11
12
|
import { generateDocsArtifact, getPropertiesForComponentFromDocs } from '../docgen';
|
|
12
13
|
import { SlotMetadata, SlotType } from '../preview/component';
|
|
@@ -358,9 +359,10 @@ export function csfRenderPlugin(
|
|
|
358
359
|
Object.entries(incomingPreviews).filter(([key]) => key.startsWith('__pattern_'))
|
|
359
360
|
);
|
|
360
361
|
|
|
361
|
-
// Include all named stories as previews and preserve injected pattern previews.
|
|
362
|
+
// Include all named stories as previews and preserve injected pattern previews. Story order
|
|
363
|
+
// comes from the source text: esbuild emits its export map alphabetically.
|
|
362
364
|
componentData.previews = {
|
|
363
|
-
...createCsfStoryPreviews(moduleExports),
|
|
365
|
+
...orderPreviews(createCsfStoryPreviews(moduleExports), readExportOrderFromSource(sourceCode)),
|
|
364
366
|
...patternPreviews,
|
|
365
367
|
};
|
|
366
368
|
|
|
@@ -60,6 +60,13 @@ const CLIENT_ARTIFACT_SUFFIX = 'client.js';
|
|
|
60
60
|
/** Logical artifact path of a component's client/hydration bundle. */
|
|
61
61
|
const clientArtifactPath = (componentId: string): string => `component/${componentId}.${CLIENT_ARTIFACT_SUFFIX}`;
|
|
62
62
|
|
|
63
|
+
function selectComponentExport(moduleExports: Record<string, any>, exportName?: string): ReactComponent | null {
|
|
64
|
+
if (exportName && exportName !== 'default' && moduleExports?.[exportName]) {
|
|
65
|
+
return moduleExports[exportName];
|
|
66
|
+
}
|
|
67
|
+
return moduleExports?.default ?? null;
|
|
68
|
+
}
|
|
69
|
+
|
|
63
70
|
/**
|
|
64
71
|
* Loads and processes component schema using hierarchical approach
|
|
65
72
|
* @param componentData - Component transformation data
|
|
@@ -85,7 +92,7 @@ async function loadComponentSchemaAndModule(
|
|
|
85
92
|
if (!componentData.entries?.schema) {
|
|
86
93
|
try {
|
|
87
94
|
const moduleExports = await buildAndEvaluateModule(componentPath, handoff);
|
|
88
|
-
component = moduleExports.exports.
|
|
95
|
+
component = selectComponentExport(moduleExports.exports, componentData.componentExport);
|
|
89
96
|
|
|
90
97
|
// Try to load schema from component exports
|
|
91
98
|
properties = await loadSchemaFromComponent(moduleExports.exports, handoff);
|
|
@@ -103,7 +110,7 @@ async function loadComponentSchemaAndModule(
|
|
|
103
110
|
if (!component) {
|
|
104
111
|
try {
|
|
105
112
|
const moduleExports = await buildAndEvaluateModule(componentPath, handoff);
|
|
106
|
-
component = moduleExports.exports.
|
|
113
|
+
component = selectComponentExport(moduleExports.exports, componentData.componentExport);
|
|
107
114
|
} catch (error) {
|
|
108
115
|
Logger.error(`Failed to load component for rendering "${componentPath}":`, error);
|
|
109
116
|
return [null, null];
|
|
@@ -128,13 +135,16 @@ async function loadComponentSchemaAndModule(
|
|
|
128
135
|
*
|
|
129
136
|
* @param componentId - Component identifier used to match pattern mount points to this bundle
|
|
130
137
|
* @param componentPath - Path to the component file
|
|
138
|
+
* @param componentExport - Export holding the implementation; the default export when absent
|
|
131
139
|
* @returns Client-side hydration source code
|
|
132
140
|
*/
|
|
133
|
-
function generateClientHydrationSource(componentId: string, componentPath: string): string {
|
|
141
|
+
function generateClientHydrationSource(componentId: string, componentPath: string, componentExport?: string): string {
|
|
142
|
+
const importClause = componentExport && componentExport !== 'default' ? `{ ${componentExport} as Component }` : 'Component';
|
|
143
|
+
|
|
134
144
|
return `
|
|
135
145
|
import React from 'react';
|
|
136
146
|
import { hydrateRoot } from 'react-dom/client';
|
|
137
|
-
import
|
|
147
|
+
import ${importClause} from '${normalizePath(componentPath)}';
|
|
138
148
|
|
|
139
149
|
const parseProps = (propsId) => {
|
|
140
150
|
const raw = propsId ? document.getElementById(propsId)?.textContent : '{}';
|
|
@@ -304,7 +314,7 @@ export function ssrRenderPlugin(
|
|
|
304
314
|
// `component/<id>.client.js` artifact. The hydration source only imports the component and
|
|
305
315
|
// reads props from the in-document `__APP_PROPS__` element, so it is identical across every
|
|
306
316
|
// preview of this component.
|
|
307
|
-
const clientHydrationSource = generateClientHydrationSource(componentId, componentPath);
|
|
317
|
+
const clientHydrationSource = generateClientHydrationSource(componentId, componentPath, componentData.componentExport);
|
|
308
318
|
const clientBuildConfig = {
|
|
309
319
|
...DEFAULT_CLIENT_BUILD_CONFIG,
|
|
310
320
|
logLevel: 'silent' as const,
|
|
@@ -5,6 +5,14 @@ import Handoff from '../../../index';
|
|
|
5
5
|
import { ComponentListObject, TransformComponentTokensResult } from '../types';
|
|
6
6
|
import { getDocumentedPreviews } from './previews';
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Strip the fields that must not reach the written artifact.
|
|
10
|
+
*
|
|
11
|
+
* The docs API serves this artifact publicly, and publish uploads it verbatim. An absolute path in
|
|
12
|
+
* the artifact exposes the filesystem layout of the build machine, so this drops the workspace
|
|
13
|
+
* locations `path` and `entries`. The build takes those locations from the runtime config, and
|
|
14
|
+
* publish remaps `entries` onto the registry record.
|
|
15
|
+
*/
|
|
8
16
|
export const sanitizeComponentApiData = (
|
|
9
17
|
component: TransformComponentTokensResult
|
|
10
18
|
): TransformComponentTokensResult => {
|
|
@@ -13,9 +21,11 @@ export const sanitizeComponentApiData = (
|
|
|
13
21
|
const sanitized = {
|
|
14
22
|
...component,
|
|
15
23
|
previews: getDocumentedPreviews(component.previews),
|
|
16
|
-
};
|
|
24
|
+
} as NonNullable<TransformComponentTokensResult> & { path?: string };
|
|
17
25
|
|
|
18
26
|
delete sanitized.internalPatternPreviews;
|
|
27
|
+
delete sanitized.entries;
|
|
28
|
+
delete sanitized.path;
|
|
19
29
|
|
|
20
30
|
return sanitized;
|
|
21
31
|
};
|
|
@@ -150,6 +160,8 @@ export const readComponentMetadataApi = async (handoff: Handoff, id: string): Pr
|
|
|
150
160
|
properties: componentData.properties,
|
|
151
161
|
previews: getDocumentedPreviews(componentData.previews),
|
|
152
162
|
path: buildComponentDetailUrl(id, process.env.HANDOFF_APP_BASE_PATH ?? ''),
|
|
163
|
+
...(componentData.renderer ? { renderer: componentData.renderer } : {}),
|
|
164
|
+
...(componentData.sourceFormat ? { sourceFormat: componentData.sourceFormat } : {}),
|
|
153
165
|
};
|
|
154
166
|
};
|
|
155
167
|
export default writeComponentApi;
|
|
@@ -16,7 +16,7 @@ const CLIENT_ARTIFACT_SUFFIX = '.client.js';
|
|
|
16
16
|
export const getComponentApiPath = (handoff: Handoff) => path.resolve(getAPIPath(handoff), 'component');
|
|
17
17
|
|
|
18
18
|
const getComponentPreviewKeys = async (handoff: Handoff, componentId: string): Promise<Set<string>> => {
|
|
19
|
-
const runtimeComponent = handoff.runtimeConfig?.
|
|
19
|
+
const runtimeComponent = handoff.runtimeConfig?.entities.components[componentId];
|
|
20
20
|
const previewKeys = new Set<string>();
|
|
21
21
|
|
|
22
22
|
const outputFilePath = path.resolve(getComponentApiPath(handoff), `${componentId}.json`);
|
|
@@ -67,15 +67,19 @@ export const removeComponentApi = async (handoff: Handoff, id: string): Promise<
|
|
|
67
67
|
}
|
|
68
68
|
};
|
|
69
69
|
|
|
70
|
-
export const syncComponentArtifacts = async (handoff: Handoff): Promise<void> => {
|
|
70
|
+
export const syncComponentArtifacts = async (handoff: Handoff, removedIds: readonly string[] = []): Promise<void> => {
|
|
71
71
|
const componentPath = getComponentApiPath(handoff);
|
|
72
72
|
await fs.ensureDir(componentPath);
|
|
73
73
|
|
|
74
|
-
const runtimeComponents = handoff.runtimeConfig?.
|
|
74
|
+
const runtimeComponents = handoff.runtimeConfig?.entities.components ?? {};
|
|
75
75
|
const runtimeIds = Object.keys(runtimeComponents);
|
|
76
76
|
const entries = await fs.readdir(componentPath);
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
// `getArtifactComponentId` prunes a preview file only when it can name the owner. A deleted
|
|
79
|
+
// component is gone from the runtime config, and its `.json` is already removed, so nothing can
|
|
80
|
+
// match it. Naming it here restores the match without making its files valid. Every artifact it
|
|
81
|
+
// owns is then pruned, including the `__pattern_*` previews that the `.json` does not list.
|
|
82
|
+
const discoveredArtifactIds = new Set<string>([...runtimeIds, ...removedIds]);
|
|
79
83
|
for (const entry of entries) {
|
|
80
84
|
const parsed = path.parse(entry);
|
|
81
85
|
if (parsed.ext === '.json' || parsed.ext === '.js' || parsed.ext === '.css') {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Types as CoreTypes } from 'handoff-core';
|
|
2
2
|
import cloneDeep from 'lodash/cloneDeep';
|
|
3
3
|
import { buildComponentDetailUrl } from '../../../artifacts/url';
|
|
4
|
+
import type { SourceFormat } from '../../../catalog/renderers';
|
|
4
5
|
import {
|
|
5
6
|
BuildCache,
|
|
6
7
|
checkOutputExists,
|
|
@@ -67,8 +68,16 @@ type ComponentBuildPlan = {
|
|
|
67
68
|
validationMode: boolean;
|
|
68
69
|
};
|
|
69
70
|
|
|
71
|
+
/**
|
|
72
|
+
* A source format reads its previews out of the source file. A placeholder there is a preview
|
|
73
|
+
* nothing can render: the docs would offer a "Default" entry with no artifact. Only a renderer
|
|
74
|
+
* that renders whatever previews it is given needs the placeholder.
|
|
75
|
+
*/
|
|
76
|
+
const ownsItsPreviews = (data: { sourceFormat?: SourceFormat } | undefined): boolean => !!data?.sourceFormat;
|
|
77
|
+
|
|
70
78
|
const ensureDefaultPreview = (data: TransformComponentTokensResult): void => {
|
|
71
79
|
if (!data) return;
|
|
80
|
+
if (ownsItsPreviews(data)) return;
|
|
72
81
|
if (!data.previews || Object.keys(data.previews).length === 0) {
|
|
73
82
|
data.previews = {
|
|
74
83
|
default: {
|
|
@@ -180,7 +189,7 @@ export async function processComponents(
|
|
|
180
189
|
const documentationObject = await handoff.getDocumentationObject();
|
|
181
190
|
const components = documentationObject?.components ?? ({} as CoreTypes.IDocumentationObject['components']);
|
|
182
191
|
// Resolve the component set through the storage-agnostic store (v2). The filesystem store is a
|
|
183
|
-
// read view over `runtimeConfig.
|
|
192
|
+
// read view over `runtimeConfig.entities.components`, so these are the same records the build has
|
|
184
193
|
// always used — rebuilt into the id-keyed map the build logic below expects.
|
|
185
194
|
const runtimeComponents: Record<string, ComponentListObject> = {};
|
|
186
195
|
for (const component of await handoff.store.components.list()) {
|
|
@@ -191,7 +200,7 @@ export async function processComponents(
|
|
|
191
200
|
if (id && !runtimeComponents[id]) {
|
|
192
201
|
await removeComponentApi(handoff, id);
|
|
193
202
|
await removeComponentFromSummaryApi(handoff, id);
|
|
194
|
-
await syncComponentArtifacts(handoff);
|
|
203
|
+
await syncComponentArtifacts(handoff, [id]);
|
|
195
204
|
return [];
|
|
196
205
|
}
|
|
197
206
|
|
|
@@ -302,7 +311,7 @@ export async function processComponents(
|
|
|
302
311
|
// If this is NOT a figma component, add the default generic preview.
|
|
303
312
|
// We add it here (before merge) so that if the user explicitly provided previews in 'restMetadata',
|
|
304
313
|
// those will override this default (standard "config overrides defaults" behavior).
|
|
305
|
-
if (!restMetadata.figmaComponentId) {
|
|
314
|
+
if (!restMetadata.figmaComponentId && !ownsItsPreviews(restMetadata)) {
|
|
306
315
|
componentDefaults.previews = {
|
|
307
316
|
default: {
|
|
308
317
|
title: 'Default',
|
|
@@ -337,12 +346,14 @@ export async function processComponents(
|
|
|
337
346
|
data.css = existingData.css;
|
|
338
347
|
data.sass = existingData.sass;
|
|
339
348
|
}
|
|
340
|
-
// If we're not building previews, preserve pre-existing HTML, code snippet, usage,
|
|
349
|
+
// If we're not building previews, preserve pre-existing HTML, code snippet, usage, previews,
|
|
350
|
+
// and the code language they were rendered in.
|
|
341
351
|
if (!buildPlan.previews) {
|
|
342
352
|
data.html = existingData.html;
|
|
343
353
|
data.code = existingData.code;
|
|
344
354
|
data.usage = existingData.usage;
|
|
345
355
|
data.previews = existingData.previews;
|
|
356
|
+
data.format = existingData.format;
|
|
346
357
|
}
|
|
347
358
|
/**
|
|
348
359
|
* Always keep validation results from the previous data,
|
|
@@ -466,6 +477,8 @@ const buildComponentSummary = (id: string, data: TransformComponentTokensResult)
|
|
|
466
477
|
properties: data.properties,
|
|
467
478
|
previews: getDocumentedPreviews(data.previews),
|
|
468
479
|
path: buildComponentDetailUrl(id, process.env.HANDOFF_APP_BASE_PATH ?? ''),
|
|
480
|
+
...(data.renderer ? { renderer: data.renderer } : {}),
|
|
481
|
+
...(data.sourceFormat ? { sourceFormat: data.sourceFormat } : {}),
|
|
469
482
|
};
|
|
470
483
|
};
|
|
471
484
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { InlineConfig, build as viteBuild } from 'vite';
|
|
4
|
-
import { initRuntimeConfig } from '../../../config';
|
|
5
4
|
import Handoff from '../../../index';
|
|
6
5
|
import { formatDurationMs } from '../../../utils/duration';
|
|
7
6
|
import { Logger } from '../../../utils/logger';
|
|
@@ -184,13 +183,13 @@ const buildComponentCss = async (data: TransformComponentTokensResult, handoff:
|
|
|
184
183
|
export const buildMainCss = async (handoff: Handoff): Promise<void> => {
|
|
185
184
|
const outputPath = getComponentOutputPath(handoff);
|
|
186
185
|
const mainCssPath = path.resolve(outputPath, MAIN_COMPONENT_CSS_FILE);
|
|
187
|
-
const
|
|
186
|
+
const scssEntry = handoff.runtimeConfig?.entries?.scss;
|
|
188
187
|
|
|
189
|
-
if (
|
|
190
|
-
const stat = await fs.stat(
|
|
191
|
-
const entryPath = stat.isDirectory() ? path.resolve(
|
|
188
|
+
if (scssEntry && fs.existsSync(scssEntry)) {
|
|
189
|
+
const stat = await fs.stat(scssEntry);
|
|
190
|
+
const entryPath = stat.isDirectory() ? path.resolve(scssEntry, 'main.scss') : scssEntry;
|
|
192
191
|
|
|
193
|
-
if (entryPath ===
|
|
192
|
+
if (entryPath === scssEntry || fs.existsSync(entryPath)) {
|
|
194
193
|
Logger.info(`Building styles for global entry (${MAIN_COMPONENT_CSS_FILE})…`);
|
|
195
194
|
const startedAt = Date.now();
|
|
196
195
|
// Drop the previous artifact so a failed/empty rebuild cannot preserve stale global CSS that
|
|
@@ -198,15 +197,12 @@ export const buildMainCss = async (handoff: Handoff): Promise<void> => {
|
|
|
198
197
|
await fs.remove(mainCssPath);
|
|
199
198
|
try {
|
|
200
199
|
const loadPaths = [
|
|
200
|
+
path.dirname(scssEntry),
|
|
201
201
|
path.resolve(handoff.workingPath),
|
|
202
202
|
path.resolve(handoff.workingPath, handoff.exportsDirectory, handoff.getProjectId()),
|
|
203
203
|
path.resolve(handoff.workingPath, 'node_modules'),
|
|
204
204
|
];
|
|
205
205
|
|
|
206
|
-
if (handoff.runtimeConfig?.entries?.scss) {
|
|
207
|
-
loadPaths.unshift(path.dirname(handoff.runtimeConfig.entries.scss));
|
|
208
|
-
}
|
|
209
|
-
|
|
210
206
|
await buildCssBundle({
|
|
211
207
|
entry: entryPath,
|
|
212
208
|
outputPath,
|