handoff-app 1.2.2-7 → 2.0.0-0
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/Changelog.md +37 -15
- package/README.md +372 -118
- package/dist/app-builder/build.d.ts +41 -2
- package/dist/app-builder/build.js +455 -18
- package/dist/app-builder/client-config.d.ts +15 -2
- package/dist/app-builder/client-config.js +43 -4
- package/dist/app-builder/config-diff/snapshot.d.ts +1 -6
- package/dist/app-builder/config-diff/snapshot.js +2 -9
- package/dist/app-builder/index.d.ts +1 -1
- package/dist/app-builder/index.js +3 -1
- package/dist/app-builder/nav-shell.d.ts +23 -0
- package/dist/app-builder/nav-shell.js +80 -0
- package/dist/app-builder/static-export.d.ts +17 -0
- package/dist/app-builder/static-export.js +181 -0
- package/dist/app-builder/vercel-output.d.ts +39 -0
- package/dist/app-builder/vercel-output.js +259 -0
- package/dist/app-builder/watchers/utils.d.ts +2 -0
- package/dist/app-builder/watchers.d.ts +14 -4
- package/dist/app-builder/watchers.js +123 -28
- package/dist/app-builder/websocket.js +1 -1
- package/dist/artifacts/index.d.ts +2 -0
- package/dist/artifacts/index.js +13 -0
- package/dist/artifacts/types.d.ts +136 -0
- package/dist/artifacts/types.js +24 -0
- package/dist/artifacts/url.d.ts +49 -0
- package/dist/artifacts/url.js +86 -0
- package/dist/cache/build-cache.js +48 -4
- package/dist/cli.js +0 -0
- package/dist/commands/build/app.js +1 -2
- package/dist/commands/build/index.d.ts +17 -0
- package/dist/commands/build/index.js +66 -0
- package/dist/commands/checkout/index.d.ts +24 -0
- package/dist/commands/checkout/index.js +71 -0
- package/dist/commands/db/migrate.d.ts +11 -0
- package/dist/commands/db/migrate.js +33 -0
- package/dist/commands/index.js +8 -0
- package/dist/commands/publish/index.d.ts +21 -0
- package/dist/commands/publish/index.js +68 -0
- package/dist/config/defaults.js +15 -1
- package/dist/config/loader.js +6 -1
- package/dist/config/normalizers/declaration.js +4 -1
- package/dist/config/normalizers/page.d.ts +18 -0
- package/dist/config/normalizers/page.js +34 -0
- package/dist/config/normalizers/pattern.js +7 -1
- package/dist/config/runtime.d.ts +4 -0
- package/dist/config/runtime.js +53 -5
- package/dist/index.d.ts +65 -1
- package/dist/index.js +123 -3
- package/dist/nav/index.d.ts +83 -0
- package/dist/nav/index.js +9 -0
- package/dist/nav/registry.d.ts +15 -0
- package/dist/nav/registry.js +42 -0
- package/dist/nav/resolver.d.ts +8 -0
- package/dist/nav/resolver.js +192 -0
- package/dist/nav/workspace.d.ts +18 -0
- package/dist/nav/workspace.js +103 -0
- package/dist/pipeline/styles.js +7 -35
- package/dist/pipeline/token-transformers.d.ts +45 -0
- package/dist/pipeline/token-transformers.js +71 -0
- package/dist/registry/asset-storage/adapters/vercel-blob.d.ts +17 -0
- package/dist/registry/asset-storage/adapters/vercel-blob.js +87 -0
- package/dist/registry/asset-storage/define.d.ts +10 -0
- package/dist/registry/asset-storage/define.js +13 -0
- package/dist/registry/asset-storage/resolve.d.ts +45 -0
- package/dist/registry/asset-storage/resolve.js +36 -0
- package/dist/registry/asset-storage/types.d.ts +51 -0
- package/dist/registry/asset-storage/types.js +9 -0
- package/dist/registry/assets/layout.d.ts +22 -0
- package/dist/registry/assets/layout.js +30 -0
- package/dist/registry/assets/sets.d.ts +18 -0
- package/dist/registry/assets/sets.js +44 -0
- package/dist/registry/assets/transfer.d.ts +45 -0
- package/dist/registry/assets/transfer.js +11 -0
- package/dist/registry/checkout/assets.d.ts +16 -0
- package/dist/registry/checkout/assets.js +150 -0
- package/dist/registry/checkout/index.d.ts +33 -0
- package/dist/registry/checkout/index.js +610 -0
- package/dist/registry/checkout/tokens.d.ts +15 -0
- package/dist/registry/checkout/tokens.js +171 -0
- package/dist/registry/client.d.ts +61 -0
- package/dist/registry/client.js +211 -0
- package/dist/registry/connection.d.ts +30 -0
- package/dist/registry/connection.js +36 -0
- package/dist/registry/db/adapter.d.ts +43 -0
- package/dist/registry/db/adapter.js +59 -0
- package/dist/registry/db/client.d.ts +35 -0
- package/dist/registry/db/client.js +94 -0
- package/dist/registry/db/index.d.ts +5 -0
- package/dist/registry/db/index.js +24 -0
- package/dist/registry/db/migrate.d.ts +23 -0
- package/dist/registry/db/migrate.js +64 -0
- package/dist/registry/db/schema.d.ts +4869 -0
- package/dist/registry/db/schema.js +347 -0
- package/dist/registry/path.d.ts +4 -0
- package/dist/registry/path.js +31 -0
- package/dist/registry/publish/artifacts.d.ts +16 -0
- package/dist/registry/publish/artifacts.js +132 -0
- package/dist/registry/publish/assets.d.ts +16 -0
- package/dist/registry/publish/assets.js +129 -0
- package/dist/registry/publish/errors.d.ts +5 -0
- package/dist/registry/publish/errors.js +26 -0
- package/dist/registry/publish/index.d.ts +27 -0
- package/dist/registry/publish/index.js +126 -0
- package/dist/registry/publish/package.d.ts +27 -0
- package/dist/registry/publish/package.js +257 -0
- package/dist/registry/publish/publish-build.d.ts +12 -0
- package/dist/registry/publish/publish-build.js +43 -0
- package/dist/registry/publish/tokens.d.ts +18 -0
- package/dist/registry/publish/tokens.js +112 -0
- package/dist/registry/tokens/sets.d.ts +59 -0
- package/dist/registry/tokens/sets.js +109 -0
- package/dist/registry/tokens/transfer.d.ts +48 -0
- package/dist/registry/tokens/transfer.js +10 -0
- package/dist/registry/transfer.d.ts +75 -0
- package/dist/registry/transfer.js +11 -0
- package/dist/store/filesystem-assets.d.ts +17 -0
- package/dist/store/filesystem-assets.js +147 -0
- package/dist/store/filesystem.d.ts +76 -0
- package/dist/store/filesystem.js +208 -0
- package/dist/store/index.d.ts +20 -0
- package/dist/store/index.js +38 -0
- package/dist/store/registry.d.ts +77 -0
- package/dist/store/registry.js +350 -0
- package/dist/store/source-files.d.ts +24 -0
- package/dist/store/source-files.js +147 -0
- package/dist/store/types.d.ts +185 -0
- package/dist/store/types.js +12 -0
- package/dist/transformers/plugins/csf-render.js +17 -8
- package/dist/transformers/plugins/handlebars-previews.js +10 -3
- package/dist/transformers/plugins/ssr-render.js +116 -49
- package/dist/transformers/preview/component/api.js +3 -1
- package/dist/transformers/preview/component/artifacts.js +8 -1
- package/dist/transformers/preview/component/builder.js +13 -5
- package/dist/transformers/preview/component/css.js +9 -1
- package/dist/transformers/preview/component/javascript.d.ts +9 -3
- package/dist/transformers/preview/component/javascript.js +45 -17
- package/dist/transformers/preview/component/shared-artifacts.d.ts +83 -0
- package/dist/transformers/preview/component/shared-artifacts.js +92 -0
- package/dist/transformers/preview/component.js +5 -2
- package/dist/transformers/preview/pattern/builder.js +28 -9
- package/dist/transformers/preview/pattern/html.d.ts +13 -10
- package/dist/transformers/preview/pattern/html.js +62 -27
- package/dist/transformers/preview/types.d.ts +38 -0
- package/dist/transformers/utils/handlebars.d.ts +3 -0
- package/dist/transformers/utils/handlebars.js +19 -5
- package/dist/types/config.d.ts +118 -25
- package/dist/utils/menu-shell.d.ts +60 -0
- package/dist/utils/menu-shell.js +221 -0
- package/dist/utils/stable-stringify.d.ts +2 -0
- package/dist/utils/stable-stringify.js +8 -0
- package/docs/cli.md +4 -2
- package/drizzle/0000_init_registry_storage.sql +95 -0
- package/drizzle/0001_add_review_metadata.sql +2 -0
- package/drizzle/0002_add_pages.sql +28 -0
- package/drizzle/0003_add_tokens.sql +29 -0
- package/drizzle/0004_add_assets.sql +37 -0
- package/drizzle/meta/0000_snapshot.json +642 -0
- package/drizzle/meta/0001_snapshot.json +654 -0
- package/drizzle/meta/0002_snapshot.json +835 -0
- package/drizzle/meta/0003_snapshot.json +1023 -0
- package/drizzle/meta/0004_snapshot.json +1252 -0
- package/drizzle/meta/_journal.json +41 -0
- package/drizzle.config.ts +16 -0
- package/package.json +10 -1
- package/src/app/components/Component/Preview.tsx +3 -2
- package/src/app/components/Component/RegistryActions.tsx +150 -0
- package/src/app/components/Foundations/DisplayLogo.tsx +5 -3
- package/src/app/components/Layout/Header.tsx +21 -3
- package/src/app/components/Layout/Main.tsx +11 -3
- package/src/app/components/Navigation/MainNav.tsx +4 -2
- package/src/app/components/Navigation/MobileNav.tsx +4 -2
- package/src/app/components/Navigation/SideNav.tsx +46 -11
- package/src/app/components/NotFound.tsx +24 -0
- package/src/app/components/context/NavProvider.tsx +71 -0
- package/src/app/components/old/Header.tsx +3 -3
- package/src/app/components/util/index.ts +203 -380
- package/src/app/components/util/useCollectionAssets.ts +125 -0
- package/src/app/components/util/useFoundationTokens.ts +67 -0
- package/src/app/generated/default-pages.json +1 -0
- package/src/app/generated/nav-shell.json +1 -0
- package/src/app/lib/api/query.ts +6 -0
- package/src/app/lib/api/redact.ts +46 -0
- package/src/app/lib/asset-storage.ts +81 -0
- package/src/app/lib/docs-api/artifacts.ts +116 -0
- package/src/app/lib/docs-api/backend.ts +151 -0
- package/src/app/lib/docs-api/errors.ts +52 -0
- package/src/app/lib/docs-api/handler.ts +34 -0
- package/src/app/lib/docs-api/index.ts +7 -0
- package/src/app/lib/docs-api/records.ts +189 -0
- package/src/app/lib/docs-api/registry.ts +130 -0
- package/src/app/lib/docs-api/runtime-config.ts +173 -0
- package/src/app/lib/docs-api/serve.ts +37 -0
- package/src/app/lib/docs-api/token-detail.ts +19 -0
- package/src/app/lib/registry-api/allowlist.ts +163 -0
- package/src/app/lib/registry-api/asset-transfer.ts +363 -0
- package/src/app/lib/registry-api/auth.ts +68 -0
- package/src/app/lib/registry-api/entity-routes.ts +197 -0
- package/src/app/lib/registry-api/errors.ts +51 -0
- package/src/app/lib/registry-api/files.ts +135 -0
- package/src/app/lib/registry-api/handler.ts +96 -0
- package/src/app/lib/registry-api/index.ts +4 -0
- package/src/app/lib/registry-api/meta.ts +87 -0
- package/src/app/lib/registry-api/revalidate.ts +36 -0
- package/src/app/lib/registry-api/store.ts +348 -0
- package/src/app/lib/registry-api/token-transfer.ts +231 -0
- package/src/app/lib/registry-api/transfer.ts +513 -0
- package/src/app/lib/registry-api/validation.ts +74 -0
- package/src/app/lib/registry-connection.ts +58 -0
- package/src/app/lib/utils.ts +14 -0
- package/src/app/next.config.mjs +72 -13
- package/src/app/pages/[...slug]/index.tsx +74 -20
- package/src/app/pages/_app.tsx +5 -2
- package/src/app/pages/api/docs/artifacts/[...artifactPath].ts +16 -0
- package/src/app/pages/api/docs/assets/[collection]/[...path].ts +50 -0
- package/src/app/pages/api/docs/assets/[collection]/index.ts +22 -0
- package/src/app/pages/api/docs/components/[id]/assets/[asset].ts +33 -0
- package/src/app/pages/api/docs/components/[id]/inspect/[previewId].ts +20 -0
- package/src/app/pages/api/docs/components/[id]/previews/[previewId].ts +21 -0
- package/src/app/pages/api/docs/components/[id].ts +24 -0
- package/src/app/pages/api/docs/components.json.ts +12 -0
- package/src/app/pages/api/docs/nav.json.ts +48 -0
- package/src/app/pages/api/docs/patterns/[id]/preview.ts +19 -0
- package/src/app/pages/api/docs/patterns/[id].ts +24 -0
- package/src/app/pages/api/docs/patterns.json.ts +12 -0
- package/src/app/pages/api/docs/tokens/components/[id].ts +30 -0
- package/src/app/pages/api/docs/tokens/foundations/[type].ts +32 -0
- package/src/app/pages/api/registry/components/[id]/files/[...filePath].ts +19 -0
- package/src/app/pages/api/registry/components/[id]/files/index.ts +19 -0
- package/src/app/pages/api/registry/components/[id].ts +20 -0
- package/src/app/pages/api/registry/components/index.ts +19 -0
- package/src/app/pages/api/registry/health.ts +20 -0
- package/src/app/pages/api/registry/patterns/[id]/files/[...filePath].ts +19 -0
- package/src/app/pages/api/registry/patterns/[id]/files/index.ts +19 -0
- package/src/app/pages/api/registry/patterns/[id].ts +20 -0
- package/src/app/pages/api/registry/patterns/index.ts +19 -0
- package/src/app/pages/api/registry/transfer/[kind]/[id].ts +39 -0
- package/src/app/pages/api/registry/transfer/assets/[collection].ts +24 -0
- package/src/app/pages/api/registry/transfer/assets/blobs/[hash].ts +23 -0
- package/src/app/pages/api/registry/transfer/assets/blobs/have.ts +11 -0
- package/src/app/pages/api/registry/transfer/assets/index.ts +10 -0
- package/src/app/pages/api/registry/transfer/tokens/[...setId].ts +26 -0
- package/src/app/pages/api/registry/transfer/tokens/index.ts +11 -0
- package/src/app/pages/assets/fonts.tsx +41 -17
- package/src/app/pages/assets/index.tsx +2 -2
- package/src/app/pages/assets/logos.tsx +2 -2
- package/src/app/pages/foundations/colors.tsx +18 -8
- package/src/app/pages/foundations/effects.tsx +18 -8
- package/src/app/pages/foundations/icons/[name]/index.tsx +26 -14
- package/src/app/pages/foundations/icons/index.tsx +13 -9
- package/src/app/pages/foundations/index.tsx +1 -1
- package/src/app/pages/foundations/logo.tsx +7 -4
- package/src/app/pages/foundations/typography.tsx +19 -9
- package/src/app/pages/index.tsx +1 -1
- package/src/app/pages/system/component/[component]/index.tsx +87 -17
- package/src/app/pages/system/index.tsx +13 -4
- package/src/app/pages/system/pattern/[pattern]/index.tsx +70 -14
- package/src/app/pages/system/tokens/components/[component]/index.tsx +42 -14
- package/src/app/pages/system/tokens/foundations/colors.tsx +7 -5
- package/src/app/pages/system/tokens/foundations/effects.tsx +7 -5
- package/src/app/pages/system/tokens/foundations/typography.tsx +8 -4
- package/src/app/tsconfig.json +2 -3
- package/src/app-builder/build.ts +555 -11
- package/src/app-builder/client-config.ts +61 -4
- package/src/app-builder/config-diff/snapshot.ts +1 -11
- package/src/app-builder/index.ts +1 -1
- package/src/app-builder/nav-shell.ts +74 -0
- package/src/app-builder/static-export.ts +215 -0
- package/src/app-builder/vercel-output.ts +297 -0
- package/src/app-builder/watchers/utils.ts +2 -0
- package/src/app-builder/watchers.ts +149 -25
- package/src/app-builder/websocket.ts +2 -2
- package/src/artifacts/README.md +14 -0
- package/src/artifacts/index.ts +17 -0
- package/src/artifacts/types.ts +146 -0
- package/src/artifacts/url.ts +90 -0
- package/src/cache/build-cache.ts +47 -2
- package/src/commands/build/app.ts +1 -2
- package/src/commands/build/index.ts +61 -0
- package/src/commands/checkout/index.ts +66 -0
- package/src/commands/db/migrate.ts +25 -0
- package/src/commands/index.ts +8 -0
- package/src/commands/publish/index.ts +63 -0
- package/src/config/defaults.ts +15 -0
- package/src/config/loader.ts +10 -1
- package/src/config/normalizers/declaration.ts +3 -0
- package/src/config/normalizers/page.ts +45 -0
- package/src/config/normalizers/pattern.ts +4 -1
- package/src/config/runtime.ts +48 -2
- package/src/index.ts +126 -3
- package/src/nav/index.ts +94 -0
- package/src/nav/registry.ts +54 -0
- package/src/nav/resolver.ts +217 -0
- package/src/nav/workspace.ts +87 -0
- package/src/pipeline/styles.ts +4 -35
- package/src/pipeline/token-transformers.ts +97 -0
- package/src/registry/asset-storage/adapters/vercel-blob.ts +51 -0
- package/src/registry/asset-storage/define.ts +15 -0
- package/src/registry/asset-storage/resolve.ts +72 -0
- package/src/registry/asset-storage/types.ts +46 -0
- package/src/registry/assets/layout.ts +38 -0
- package/src/registry/assets/sets.ts +48 -0
- package/src/registry/assets/transfer.ts +50 -0
- package/src/registry/checkout/assets.ts +126 -0
- package/src/registry/checkout/index.ts +596 -0
- package/src/registry/checkout/tokens.ts +152 -0
- package/src/registry/client.ts +234 -0
- package/src/registry/connection.ts +50 -0
- package/src/registry/db/adapter.ts +74 -0
- package/src/registry/db/client.ts +88 -0
- package/src/registry/db/index.ts +35 -0
- package/src/registry/db/migrate.ts +56 -0
- package/src/registry/db/schema.ts +444 -0
- package/src/registry/path.ts +26 -0
- package/src/registry/publish/artifacts.ts +147 -0
- package/src/registry/publish/assets.ts +129 -0
- package/src/registry/publish/errors.ts +34 -0
- package/src/registry/publish/index.ts +128 -0
- package/src/registry/publish/package.ts +272 -0
- package/src/registry/publish/publish-build.ts +51 -0
- package/src/registry/publish/tokens.ts +119 -0
- package/src/registry/tokens/sets.ts +132 -0
- package/src/registry/tokens/transfer.ts +53 -0
- package/src/registry/transfer.ts +82 -0
- package/src/store/filesystem-assets.ts +152 -0
- package/src/store/filesystem.ts +237 -0
- package/src/store/index.ts +62 -0
- package/src/store/registry.ts +334 -0
- package/src/store/source-files.ts +155 -0
- package/src/store/types.ts +210 -0
- package/src/transformers/plugins/csf-render.ts +31 -7
- package/src/transformers/plugins/handlebars-previews.ts +22 -3
- package/src/transformers/plugins/ssr-render.ts +145 -57
- package/src/transformers/preview/component/api.ts +2 -1
- package/src/transformers/preview/component/artifacts.ts +10 -1
- package/src/transformers/preview/component/builder.ts +9 -2
- package/src/transformers/preview/component/css.ts +10 -1
- package/src/transformers/preview/component/javascript.ts +55 -20
- package/src/transformers/preview/component/shared-artifacts.ts +146 -0
- package/src/transformers/preview/component.ts +5 -2
- package/src/transformers/preview/pattern/builder.ts +33 -7
- package/src/transformers/preview/pattern/html.ts +71 -27
- package/src/transformers/preview/types.ts +42 -0
- package/src/transformers/utils/handlebars.ts +31 -5
- package/src/types/config.ts +123 -25
- package/src/utils/menu-shell.ts +241 -0
- package/src/utils/stable-stringify.ts +7 -0
- package/templates/default/package.json.tpl +1 -1
- package/handoff.config.js +0 -90
- package/src/app/components/ComponentSearch.tsx +0 -93
|
@@ -6,6 +6,7 @@ import React from 'react';
|
|
|
6
6
|
import ReactDOMServer from 'react-dom/server';
|
|
7
7
|
import { Plugin, normalizePath } from 'vite';
|
|
8
8
|
import Handoff from '../..';
|
|
9
|
+
import { buildArtifactUrl } from '../../artifacts/url';
|
|
9
10
|
import { Logger } from '../../utils/logger';
|
|
10
11
|
import {
|
|
11
12
|
enrichPropertiesWithDocgen,
|
|
@@ -14,7 +15,15 @@ import {
|
|
|
14
15
|
getPropertiesFromGeneratedDocs,
|
|
15
16
|
} from '../docgen';
|
|
16
17
|
import { SlotMetadata } from '../preview/component';
|
|
17
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
renderComponentStyleLink,
|
|
20
|
+
renderGlobalScriptTag,
|
|
21
|
+
renderSharedStyleLinks,
|
|
22
|
+
resolveComponentArtifactPresence,
|
|
23
|
+
resolveSharedArtifactPresence,
|
|
24
|
+
type ComponentArtifactPresence,
|
|
25
|
+
type SharedArtifactPresence,
|
|
26
|
+
} from '../preview/component/shared-artifacts';
|
|
18
27
|
import { TransformComponentTokensResult } from '../preview/types';
|
|
19
28
|
import { DEFAULT_CLIENT_BUILD_CONFIG, createReactResolvePlugin } from '../utils/build';
|
|
20
29
|
import { formatHtml, trimPreview } from '../utils/html';
|
|
@@ -41,6 +50,16 @@ const PLUGIN_CONSTANTS = {
|
|
|
41
50
|
INSPECT_SUFFIX: '-inspect',
|
|
42
51
|
} as const;
|
|
43
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Suffix of the component-owned client/hydration artifact.
|
|
55
|
+
* One artifact per component (`component/<id>.client.js`) drives hydration for every preview of
|
|
56
|
+
* that component; it is referenced via the canonical artifact URL.
|
|
57
|
+
*/
|
|
58
|
+
const CLIENT_ARTIFACT_SUFFIX = 'client.js';
|
|
59
|
+
|
|
60
|
+
/** Logical artifact path of a component's client/hydration bundle. */
|
|
61
|
+
const clientArtifactPath = (componentId: string): string => `component/${componentId}.${CLIENT_ARTIFACT_SUFFIX}`;
|
|
62
|
+
|
|
44
63
|
/**
|
|
45
64
|
* Loads and processes component schema using hierarchical approach
|
|
46
65
|
* @param componentData - Component transformation data
|
|
@@ -95,43 +114,99 @@ async function loadComponentSchemaAndModule(
|
|
|
95
114
|
}
|
|
96
115
|
|
|
97
116
|
/**
|
|
98
|
-
* Generates client-side hydration source code
|
|
117
|
+
* Generates client-side hydration source code for the component-owned client artifact.
|
|
118
|
+
*
|
|
119
|
+
* The bundle is shared by every preview of a component and is emitted once as
|
|
120
|
+
* `component/<id>.client.js`. It hydrates two shapes of mount point so the same artifact serves both
|
|
121
|
+
* a standalone preview document and a composed pattern document without per-fragment inline code:
|
|
122
|
+
*
|
|
123
|
+
* - The standalone preview's `#${ROOT_ELEMENT_ID}` element paired with the `#${PROPS_SCRIPT_ID}`
|
|
124
|
+
* props script (one mount per document).
|
|
125
|
+
* - Any number of pattern mount points marked `[data-handoff-component="<id>"]`, each carrying a
|
|
126
|
+
* `data-handoff-props` attribute naming its (namespaced) props script id. A pattern document
|
|
127
|
+
* references this artifact once per component and the bundle hydrates every matching mount.
|
|
128
|
+
*
|
|
129
|
+
* @param componentId - Component identifier used to match pattern mount points to this bundle
|
|
99
130
|
* @param componentPath - Path to the component file
|
|
100
131
|
* @returns Client-side hydration source code
|
|
101
132
|
*/
|
|
102
|
-
function generateClientHydrationSource(componentPath: string): string {
|
|
133
|
+
function generateClientHydrationSource(componentId: string, componentPath: string): string {
|
|
103
134
|
return `
|
|
104
135
|
import React from 'react';
|
|
105
136
|
import { hydrateRoot } from 'react-dom/client';
|
|
106
137
|
import Component from '${normalizePath(componentPath)}';
|
|
107
138
|
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
139
|
+
const parseProps = (propsId) => {
|
|
140
|
+
const raw = propsId ? document.getElementById(propsId)?.textContent : '{}';
|
|
141
|
+
try {
|
|
142
|
+
return JSON.parse(raw || '{}');
|
|
143
|
+
} catch (_) {
|
|
144
|
+
return {};
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const standaloneRoot = document.getElementById('${PLUGIN_CONSTANTS.ROOT_ELEMENT_ID}');
|
|
149
|
+
if (standaloneRoot && !standaloneRoot.hasAttribute('data-handoff-component')) {
|
|
150
|
+
hydrateRoot(standaloneRoot, <Component {...parseProps('${PLUGIN_CONSTANTS.PROPS_SCRIPT_ID}')} />);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const patternMounts = document.querySelectorAll('[data-handoff-component=' + JSON.stringify('${componentId}') + ']');
|
|
154
|
+
patternMounts.forEach((mount) => {
|
|
155
|
+
hydrateRoot(mount, <Component {...parseProps(mount.getAttribute('data-handoff-props'))} />);
|
|
156
|
+
});
|
|
111
157
|
`;
|
|
112
158
|
}
|
|
113
159
|
|
|
114
160
|
/**
|
|
115
|
-
* Generates complete HTML document with SSR content and
|
|
161
|
+
* Generates complete HTML document with SSR content and a reference to the component-owned client
|
|
162
|
+
* artifact. The preview HTML carries only document structure, server-rendered markup, preview data,
|
|
163
|
+
* stylesheets, and standard artifact references. Interactive React previews treat the client
|
|
164
|
+
* artifact as a required reference, so the script tag surfaces a clear, visible failure if the
|
|
165
|
+
* artifact cannot be loaded.
|
|
116
166
|
* @param componentId - Component identifier
|
|
117
167
|
* @param previewTitle - Title for the preview
|
|
118
168
|
* @param renderedHtml - Server-rendered HTML content
|
|
119
|
-
* @param clientJs - Client-side JavaScript bundle
|
|
120
169
|
* @param props - Component props as JSON
|
|
121
170
|
* @returns Complete HTML document
|
|
122
171
|
*/
|
|
123
|
-
function generateHtmlDocument(
|
|
172
|
+
function generateHtmlDocument(
|
|
173
|
+
componentId: string,
|
|
174
|
+
previewTitle: string,
|
|
175
|
+
renderedHtml: string,
|
|
176
|
+
props: any,
|
|
177
|
+
sharedArtifacts: SharedArtifactPresence,
|
|
178
|
+
componentArtifacts: ComponentArtifactPresence
|
|
179
|
+
): string {
|
|
180
|
+
const basePath = process.env.HANDOFF_APP_BASE_PATH ?? '';
|
|
181
|
+
const clientArtifactUrl = buildArtifactUrl(clientArtifactPath(componentId), basePath);
|
|
182
|
+
const clientLoadErrorMessage = `Failed to load the React client artifact (${clientArtifactPath(componentId)}). This preview cannot hydrate.`;
|
|
183
|
+
// The handler is JS embedded in a double-quoted HTML attribute, so any `"` (e.g. from
|
|
184
|
+
// JSON.stringify) and `&` must be entity-encoded or the attribute terminates early and
|
|
185
|
+
// breaks HTML parsing. Browsers decode the entities before the JS engine runs the handler.
|
|
186
|
+
const clientLoadErrorHandler = `document.getElementById('${PLUGIN_CONSTANTS.ROOT_ELEMENT_ID}').setAttribute('data-handoff-client-error', '1');console.error(${JSON.stringify(
|
|
187
|
+
clientLoadErrorMessage
|
|
188
|
+
)});`
|
|
189
|
+
.replace(/&/g, '&')
|
|
190
|
+
.replace(/"/g, '"');
|
|
191
|
+
// Shared/global stylesheets (`component/main.css`) and the component's own stylesheet
|
|
192
|
+
// (`component/<id>.css`) are emitted only when present so absent optional artifacts never produce a
|
|
193
|
+
// missing-file browser request.
|
|
194
|
+
const sharedStyleLinks = renderSharedStyleLinks(sharedArtifacts, basePath);
|
|
195
|
+
const componentStyleLink = renderComponentStyleLink(componentArtifacts, componentId, basePath);
|
|
196
|
+
// The global script (`component/main.js`) is a classic tag placed before the client module so it
|
|
197
|
+
// runs before dependent component-specific scripts when present.
|
|
198
|
+
const globalScriptTag = renderGlobalScriptTag(sharedArtifacts, basePath);
|
|
124
199
|
return `<!DOCTYPE html>
|
|
125
200
|
<html lang="en">
|
|
126
201
|
<head>
|
|
127
|
-
<meta charset="UTF-8"
|
|
128
|
-
<link rel="stylesheet" href="${
|
|
129
|
-
<
|
|
130
|
-
<
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
${
|
|
134
|
-
|
|
202
|
+
<meta charset="UTF-8" />${sharedStyleLinks ? `\n ${sharedStyleLinks}` : ''}${componentStyleLink ? `\n ${componentStyleLink}` : ''}
|
|
203
|
+
<link rel="stylesheet" href="${basePath}/assets/css/preview.css" />
|
|
204
|
+
<script id="${PLUGIN_CONSTANTS.PROPS_SCRIPT_ID}" type="application/json">${JSON.stringify(props)}</script>${globalScriptTag ? `\n ${globalScriptTag}` : ''}
|
|
205
|
+
<script
|
|
206
|
+
type="module"
|
|
207
|
+
src="${clientArtifactUrl}"
|
|
208
|
+
onerror="${clientLoadErrorHandler}"
|
|
209
|
+
></script>
|
|
135
210
|
<title>${previewTitle}</title>
|
|
136
211
|
</head>
|
|
137
212
|
<body>
|
|
@@ -225,8 +300,58 @@ export function ssrRenderPlugin(
|
|
|
225
300
|
}
|
|
226
301
|
}
|
|
227
302
|
|
|
303
|
+
// Build the component-owned client/hydration bundle once and emit it as a single
|
|
304
|
+
// `component/<id>.client.js` artifact. The hydration source only imports the component and
|
|
305
|
+
// reads props from the in-document `__APP_PROPS__` element, so it is identical across every
|
|
306
|
+
// preview of this component.
|
|
307
|
+
const clientHydrationSource = generateClientHydrationSource(componentId, componentPath);
|
|
308
|
+
const clientBuildConfig = {
|
|
309
|
+
...DEFAULT_CLIENT_BUILD_CONFIG,
|
|
310
|
+
logLevel: 'silent' as const,
|
|
311
|
+
stdin: {
|
|
312
|
+
contents: clientHydrationSource,
|
|
313
|
+
resolveDir: process.cwd(),
|
|
314
|
+
loader: 'tsx' as const,
|
|
315
|
+
},
|
|
316
|
+
plugins: [createReactResolvePlugin(handoff.workingPath, handoff.modulePath)],
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const finalClientBuildConfig = handoff.config?.hooks?.clientBuildConfig
|
|
320
|
+
? handoff.config.hooks.clientBuildConfig(clientBuildConfig)
|
|
321
|
+
: clientBuildConfig;
|
|
322
|
+
|
|
323
|
+
let clientBundleJs: string;
|
|
324
|
+
try {
|
|
325
|
+
const bundledClient = await esbuild.build(finalClientBuildConfig);
|
|
326
|
+
if (bundledClient.warnings.length > 0) {
|
|
327
|
+
const messages = await esbuild.formatMessages(bundledClient.warnings, { kind: 'warning', color: true });
|
|
328
|
+
messages.forEach((msg) => Logger.warn(msg));
|
|
329
|
+
}
|
|
330
|
+
clientBundleJs = bundledClient.outputFiles[0].text;
|
|
331
|
+
} catch (error: any) {
|
|
332
|
+
// The client artifact is a required reference for interactive React previews. If it cannot be
|
|
333
|
+
// built, do not emit preview HTML that points at a missing artifact — fail clearly instead.
|
|
334
|
+
Logger.error(`Failed to build client bundle for ${componentId}`);
|
|
335
|
+
if (error.errors) {
|
|
336
|
+
const messages = await esbuild.formatMessages(error.errors, { kind: 'error', color: true });
|
|
337
|
+
messages.forEach((msg) => Logger.error(msg));
|
|
338
|
+
}
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
this.emitFile({
|
|
343
|
+
type: 'asset',
|
|
344
|
+
fileName: `${componentId}.${CLIENT_ARTIFACT_SUFFIX}`,
|
|
345
|
+
source: clientBundleJs,
|
|
346
|
+
});
|
|
347
|
+
|
|
228
348
|
let finalHtml = '';
|
|
229
349
|
|
|
350
|
+
// Resolve which shared/global and component-owned artifacts exist so generated HTML references
|
|
351
|
+
// them only when present. These are built before component HTML, so this reflects final state.
|
|
352
|
+
const sharedArtifacts = resolveSharedArtifactPresence(handoff);
|
|
353
|
+
const componentArtifacts = resolveComponentArtifactPresence(handoff, componentId);
|
|
354
|
+
|
|
230
355
|
// Generate previews for each variation
|
|
231
356
|
for (const previewKey in componentData.previews) {
|
|
232
357
|
const previewProps = componentData.previews[previewKey].values;
|
|
@@ -235,50 +360,13 @@ export function ssrRenderPlugin(
|
|
|
235
360
|
const serverRenderedHtml = ReactDOMServer.renderToString(React.createElement(ReactComponent, previewProps));
|
|
236
361
|
const formattedHtml = await formatHtml(serverRenderedHtml);
|
|
237
362
|
|
|
238
|
-
// Generate client-side hydration code
|
|
239
|
-
const clientHydrationSource = generateClientHydrationSource(componentPath);
|
|
240
|
-
|
|
241
|
-
// Build client-side bundle
|
|
242
|
-
const clientBuildConfig = {
|
|
243
|
-
...DEFAULT_CLIENT_BUILD_CONFIG,
|
|
244
|
-
logLevel: 'silent' as const,
|
|
245
|
-
stdin: {
|
|
246
|
-
contents: clientHydrationSource,
|
|
247
|
-
resolveDir: process.cwd(),
|
|
248
|
-
loader: 'tsx' as const,
|
|
249
|
-
},
|
|
250
|
-
plugins: [createReactResolvePlugin(handoff.workingPath, handoff.modulePath)],
|
|
251
|
-
};
|
|
252
|
-
|
|
253
|
-
// Apply user's client build config hook if provided
|
|
254
|
-
const finalClientBuildConfig = handoff.config?.hooks?.clientBuildConfig
|
|
255
|
-
? handoff.config.hooks.clientBuildConfig(clientBuildConfig)
|
|
256
|
-
: clientBuildConfig;
|
|
257
|
-
|
|
258
|
-
let clientBundleJs: string;
|
|
259
|
-
try {
|
|
260
|
-
const bundledClient = await esbuild.build(finalClientBuildConfig);
|
|
261
|
-
if (bundledClient.warnings.length > 0) {
|
|
262
|
-
const messages = await esbuild.formatMessages(bundledClient.warnings, { kind: 'warning', color: true });
|
|
263
|
-
messages.forEach((msg) => Logger.warn(msg));
|
|
264
|
-
}
|
|
265
|
-
clientBundleJs = bundledClient.outputFiles[0].text;
|
|
266
|
-
} catch (error: any) {
|
|
267
|
-
Logger.error(`Failed to build client bundle for ${componentId}`);
|
|
268
|
-
if (error.errors) {
|
|
269
|
-
const messages = await esbuild.formatMessages(error.errors, { kind: 'error', color: true });
|
|
270
|
-
messages.forEach((msg) => Logger.error(msg));
|
|
271
|
-
}
|
|
272
|
-
continue;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// Generate complete HTML document
|
|
276
363
|
finalHtml = generateHtmlDocument(
|
|
277
364
|
componentId,
|
|
278
365
|
componentData.previews[previewKey].title,
|
|
279
366
|
formattedHtml,
|
|
280
|
-
|
|
281
|
-
|
|
367
|
+
previewProps,
|
|
368
|
+
sharedArtifacts,
|
|
369
|
+
componentArtifacts
|
|
282
370
|
);
|
|
283
371
|
|
|
284
372
|
// Emit preview files
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { buildComponentDetailUrl } from '../../../artifacts/url';
|
|
3
4
|
import Handoff from '../../../index';
|
|
4
5
|
import { ComponentListObject, TransformComponentTokensResult } from '../types';
|
|
5
6
|
import { getDocumentedPreviews } from './previews';
|
|
@@ -148,7 +149,7 @@ export const readComponentMetadataApi = async (handoff: Handoff, id: string): Pr
|
|
|
148
149
|
tags: componentData.tags ? componentData.tags : [],
|
|
149
150
|
properties: componentData.properties,
|
|
150
151
|
previews: getDocumentedPreviews(componentData.previews),
|
|
151
|
-
path:
|
|
152
|
+
path: buildComponentDetailUrl(id, process.env.HANDOFF_APP_BASE_PATH ?? ''),
|
|
152
153
|
};
|
|
153
154
|
};
|
|
154
155
|
export default writeComponentApi;
|
|
@@ -10,6 +10,9 @@ import { MAIN_COMPONENT_JS_FILE } from './javascript';
|
|
|
10
10
|
const COMPONENT_JS_RESERVED_FILES = new Set([MAIN_COMPONENT_JS_FILE]);
|
|
11
11
|
const COMPONENT_CSS_RESERVED_FILES = new Set([MAIN_COMPONENT_CSS_FILE, SHARED_COMPONENT_CSS_FILE]);
|
|
12
12
|
|
|
13
|
+
/** Suffix of a component-owned client/hydration artifact (`<id>.client.js`), see ssr-render plugin. */
|
|
14
|
+
const CLIENT_ARTIFACT_SUFFIX = '.client.js';
|
|
15
|
+
|
|
13
16
|
export const getComponentApiPath = (handoff: Handoff) => path.resolve(getAPIPath(handoff), 'component');
|
|
14
17
|
|
|
15
18
|
const getComponentPreviewKeys = async (handoff: Handoff, componentId: string): Promise<Set<string>> => {
|
|
@@ -77,7 +80,9 @@ export const syncComponentArtifacts = async (handoff: Handoff): Promise<void> =>
|
|
|
77
80
|
const parsed = path.parse(entry);
|
|
78
81
|
if (parsed.ext === '.json' || parsed.ext === '.js' || parsed.ext === '.css') {
|
|
79
82
|
if (!COMPONENT_JS_RESERVED_FILES.has(entry) && !COMPONENT_CSS_RESERVED_FILES.has(entry)) {
|
|
80
|
-
|
|
83
|
+
// `<id>.client.js` is owned by `<id>`, not by a phantom `<id>.client` component — strip the
|
|
84
|
+
// compound suffix so the owner id matches the real component (path.parse would yield `<id>.client`).
|
|
85
|
+
discoveredArtifactIds.add(entry.endsWith(CLIENT_ARTIFACT_SUFFIX) ? entry.slice(0, -CLIENT_ARTIFACT_SUFFIX.length) : parsed.name);
|
|
81
86
|
}
|
|
82
87
|
}
|
|
83
88
|
}
|
|
@@ -90,6 +95,10 @@ export const syncComponentArtifacts = async (handoff: Handoff): Promise<void> =>
|
|
|
90
95
|
validFiles.add(`${componentId}.js`);
|
|
91
96
|
}
|
|
92
97
|
|
|
98
|
+
if (await fs.pathExists(path.resolve(componentPath, `${componentId}${CLIENT_ARTIFACT_SUFFIX}`))) {
|
|
99
|
+
validFiles.add(`${componentId}${CLIENT_ARTIFACT_SUFFIX}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
93
102
|
if (await fs.pathExists(path.resolve(componentPath, `${componentId}.css`))) {
|
|
94
103
|
validFiles.add(`${componentId}.css`);
|
|
95
104
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Types as CoreTypes } from 'handoff-core';
|
|
2
2
|
import cloneDeep from 'lodash/cloneDeep';
|
|
3
|
+
import { buildComponentDetailUrl } from '../../../artifacts/url';
|
|
3
4
|
import {
|
|
4
5
|
BuildCache,
|
|
5
6
|
checkOutputExists,
|
|
@@ -178,7 +179,13 @@ export async function processComponents(
|
|
|
178
179
|
|
|
179
180
|
const documentationObject = await handoff.getDocumentationObject();
|
|
180
181
|
const components = documentationObject?.components ?? ({} as CoreTypes.IDocumentationObject['components']);
|
|
181
|
-
|
|
182
|
+
// Resolve the component set through the storage-agnostic store (v2). The filesystem store is a
|
|
183
|
+
// read view over `runtimeConfig.entries.components`, so these are the same records the build has
|
|
184
|
+
// always used — rebuilt into the id-keyed map the build logic below expects.
|
|
185
|
+
const runtimeComponents: Record<string, ComponentListObject> = {};
|
|
186
|
+
for (const component of await handoff.store.components.list()) {
|
|
187
|
+
if (component) runtimeComponents[component.id] = component;
|
|
188
|
+
}
|
|
182
189
|
const allComponentIds = Object.keys(runtimeComponents);
|
|
183
190
|
|
|
184
191
|
if (id && !runtimeComponents[id]) {
|
|
@@ -458,7 +465,7 @@ const buildComponentSummary = (id: string, data: TransformComponentTokensResult)
|
|
|
458
465
|
tags: data.tags ? data.tags : [],
|
|
459
466
|
properties: data.properties,
|
|
460
467
|
previews: getDocumentedPreviews(data.previews),
|
|
461
|
-
path:
|
|
468
|
+
path: buildComponentDetailUrl(id, process.env.HANDOFF_APP_BASE_PATH ?? ''),
|
|
462
469
|
};
|
|
463
470
|
};
|
|
464
471
|
|
|
@@ -183,6 +183,7 @@ const buildComponentCss = async (data: TransformComponentTokensResult, handoff:
|
|
|
183
183
|
*/
|
|
184
184
|
export const buildMainCss = async (handoff: Handoff): Promise<void> => {
|
|
185
185
|
const outputPath = getComponentOutputPath(handoff);
|
|
186
|
+
const mainCssPath = path.resolve(outputPath, MAIN_COMPONENT_CSS_FILE);
|
|
186
187
|
const runtimeConfig = initRuntimeConfig(handoff)[0];
|
|
187
188
|
|
|
188
189
|
if (runtimeConfig?.entries?.scss && fs.existsSync(runtimeConfig.entries.scss)) {
|
|
@@ -192,6 +193,9 @@ export const buildMainCss = async (handoff: Handoff): Promise<void> => {
|
|
|
192
193
|
if (entryPath === runtimeConfig.entries.scss || fs.existsSync(entryPath)) {
|
|
193
194
|
Logger.info(`Building styles for global entry (${MAIN_COMPONENT_CSS_FILE})…`);
|
|
194
195
|
const startedAt = Date.now();
|
|
196
|
+
// Drop the previous artifact so a failed/empty rebuild cannot preserve stale global CSS that
|
|
197
|
+
// would still be referenced as current.
|
|
198
|
+
await fs.remove(mainCssPath);
|
|
195
199
|
try {
|
|
196
200
|
const loadPaths = [
|
|
197
201
|
path.resolve(handoff.workingPath),
|
|
@@ -212,10 +216,15 @@ export const buildMainCss = async (handoff: Handoff): Promise<void> => {
|
|
|
212
216
|
});
|
|
213
217
|
Logger.info(`Finished building styles for global entry (${MAIN_COMPONENT_CSS_FILE}) in ${formatDurationMs(Date.now() - startedAt)}`);
|
|
214
218
|
} catch {
|
|
215
|
-
// buildCssBundle already logs failure for the entry path
|
|
219
|
+
// buildCssBundle already logs failure for the entry path; ensure no stale artifact remains.
|
|
220
|
+
await fs.remove(mainCssPath);
|
|
216
221
|
}
|
|
217
222
|
}
|
|
223
|
+
return;
|
|
218
224
|
}
|
|
225
|
+
|
|
226
|
+
// No usable global SCSS entry: drop any stale artifact so it is not referenced as current.
|
|
227
|
+
await fs.remove(mainCssPath);
|
|
219
228
|
};
|
|
220
229
|
|
|
221
230
|
export default buildComponentCss;
|
|
@@ -20,7 +20,12 @@ export const MAIN_COMPONENT_JS_FILE = 'main.js';
|
|
|
20
20
|
* @param options.outputFilename - The name of the output file
|
|
21
21
|
*/
|
|
22
22
|
const buildJsBundle = async (
|
|
23
|
-
{
|
|
23
|
+
{
|
|
24
|
+
entry,
|
|
25
|
+
outputPath,
|
|
26
|
+
outputFilename,
|
|
27
|
+
format = 'cjs',
|
|
28
|
+
}: { entry: string; outputPath: string; outputFilename: string; format?: 'cjs' | 'iife' },
|
|
24
29
|
handoff: Handoff
|
|
25
30
|
) => {
|
|
26
31
|
const absEntryPath = path.resolve(entry);
|
|
@@ -39,13 +44,15 @@ const buildJsBundle = async (
|
|
|
39
44
|
lib: {
|
|
40
45
|
entry: absEntryPath,
|
|
41
46
|
name: path.basename(outputFilename, '.js'),
|
|
42
|
-
formats: [
|
|
47
|
+
formats: [format],
|
|
43
48
|
fileName: () => outputFilename,
|
|
44
49
|
},
|
|
45
50
|
rollupOptions: {
|
|
46
51
|
...viteBaseConfig.build?.rollupOptions,
|
|
47
52
|
output: {
|
|
48
|
-
|
|
53
|
+
// The global IIFE bundle is frequently side-effect-only; `auto` lets a bundle with no
|
|
54
|
+
// exports emit cleanly while `named` preserves the existing component-JS (cjs) behavior.
|
|
55
|
+
exports: format === 'iife' ? 'auto' : 'named',
|
|
49
56
|
},
|
|
50
57
|
},
|
|
51
58
|
outDir: outputPath,
|
|
@@ -127,34 +134,62 @@ export const buildComponentJs = async (data: TransformComponentTokensResult, han
|
|
|
127
134
|
};
|
|
128
135
|
|
|
129
136
|
/**
|
|
130
|
-
* Builds the
|
|
137
|
+
* Builds the global preview script artifact (`component/main.js`) from the configured workspace JS
|
|
138
|
+
* entry.
|
|
131
139
|
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
140
|
+
* The artifact is optional: when no global JS entry is configured, or the configured entry no longer
|
|
141
|
+
* exists, or the build fails/produces no output, any stale `main.js` is removed so a failed rebuild
|
|
142
|
+
* never leaves global JS that appears current. Generated HTML references `main.js` only when this
|
|
143
|
+
* function leaves a fresh artifact on disk, so an omitted artifact yields no dangling reference.
|
|
144
|
+
*
|
|
145
|
+
* Output uses the browser-compatible IIFE format so the artifact can load as a classic script ahead
|
|
146
|
+
* of dependent component scripts while preserving intentional top-level side effects.
|
|
134
147
|
*
|
|
135
148
|
* @param handoff - The Handoff configuration object containing build settings
|
|
136
149
|
* @returns A Promise that resolves when the build process is complete
|
|
137
150
|
*/
|
|
138
151
|
export const buildMainJS = async (handoff: Handoff): Promise<void> => {
|
|
139
152
|
const outputPath = getComponentOutputPath(handoff);
|
|
153
|
+
const mainJsPath = path.resolve(outputPath, MAIN_COMPONENT_JS_FILE);
|
|
140
154
|
const runtimeConfig = initRuntimeConfig(handoff)[0];
|
|
155
|
+
const entry = runtimeConfig?.entries?.js;
|
|
141
156
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
157
|
+
// No usable global JS entry: drop any stale artifact and emit nothing to reference.
|
|
158
|
+
if (!entry || !fs.existsSync(path.resolve(entry))) {
|
|
159
|
+
if (entry) {
|
|
160
|
+
Logger.warn(`Global JS entry "${entry}" was not found; removing stale ${MAIN_COMPONENT_JS_FILE}.`);
|
|
161
|
+
}
|
|
162
|
+
await fs.remove(mainJsPath);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
Logger.info(`Building script for global entry (${MAIN_COMPONENT_JS_FILE})…`);
|
|
167
|
+
const startedAt = Date.now();
|
|
168
|
+
|
|
169
|
+
// Remove the previous artifact before rebuilding so a failed or empty build cannot silently
|
|
170
|
+
// preserve stale global JS that would still be referenced as current.
|
|
171
|
+
await fs.remove(mainJsPath);
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
await buildJsBundle(
|
|
175
|
+
{
|
|
176
|
+
entry,
|
|
177
|
+
outputPath,
|
|
178
|
+
outputFilename: MAIN_COMPONENT_JS_FILE,
|
|
179
|
+
format: 'iife',
|
|
180
|
+
},
|
|
181
|
+
handoff
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
if (await fs.pathExists(mainJsPath)) {
|
|
154
185
|
Logger.info(`Finished building script for global entry (${MAIN_COMPONENT_JS_FILE}) in ${formatDurationMs(Date.now() - startedAt)}`);
|
|
155
|
-
}
|
|
156
|
-
Logger.error(`
|
|
186
|
+
} else {
|
|
187
|
+
Logger.error(`Global script build produced no ${MAIN_COMPONENT_JS_FILE} from "${entry}".`);
|
|
157
188
|
}
|
|
189
|
+
} catch (e) {
|
|
190
|
+
Logger.error(`Failed to build global script (${MAIN_COMPONENT_JS_FILE}) from "${entry}":`, e);
|
|
191
|
+
// Ensure a partial/failed build leaves no stale or broken artifact behind.
|
|
192
|
+
await fs.remove(mainJsPath);
|
|
158
193
|
}
|
|
159
194
|
};
|
|
160
195
|
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import {
|
|
4
|
+
ArtifactReference,
|
|
5
|
+
SHARED_MAIN_CSS_ARTIFACT_PATH,
|
|
6
|
+
SHARED_MAIN_JS_ARTIFACT_PATH,
|
|
7
|
+
SHARED_STYLES_CSS_ARTIFACT_PATH,
|
|
8
|
+
} from '../../../artifacts';
|
|
9
|
+
import { buildArtifactUrl } from '../../../artifacts/url';
|
|
10
|
+
import Handoff from '../../../index';
|
|
11
|
+
import { getComponentOutputPath } from '../component';
|
|
12
|
+
import { MAIN_COMPONENT_CSS_FILE, SHARED_COMPONENT_CSS_FILE } from './css';
|
|
13
|
+
import { MAIN_COMPONENT_JS_FILE } from './javascript';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Centralized shared/global artifact model for generated preview HTML.
|
|
17
|
+
*
|
|
18
|
+
* The shared artifacts (`component/main.css`, `component/main.js`, `component/shared.css`) are
|
|
19
|
+
* optional: each is emitted only when a corresponding source entry is configured and builds
|
|
20
|
+
* successfully. Generated HTML must reference a shared artifact only when it actually exists, so a
|
|
21
|
+
* failed or absent global build never leaves a dangling reference the browser would request.
|
|
22
|
+
*
|
|
23
|
+
* This module is the single place that decides which shared artifacts are present and turns that
|
|
24
|
+
* decision into structured references and the `<link>`/`<script>` tags every renderer emits — so
|
|
25
|
+
* dependency resolution is driven by the artifact model, never by parsing HTML.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** Which shared/global artifacts currently exist in the component output directory. */
|
|
29
|
+
export interface SharedArtifactPresence {
|
|
30
|
+
/** `component/main.js` — the global preview script built from the configured JS entry. */
|
|
31
|
+
mainJs: boolean;
|
|
32
|
+
/** `component/main.css` — the global stylesheet built from the configured SCSS entry. */
|
|
33
|
+
mainCss: boolean;
|
|
34
|
+
/** `component/shared.css` — shared component styles extracted by the SCSS split marker. */
|
|
35
|
+
sharedCss: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resolve which shared/global artifacts are present on disk. Global artifacts are built before
|
|
40
|
+
* component/pattern HTML is generated, so an existence check here reflects whether the artifact can
|
|
41
|
+
* be referenced without producing a missing-file browser request.
|
|
42
|
+
*/
|
|
43
|
+
export const resolveSharedArtifactPresence = (handoff: Handoff): SharedArtifactPresence => {
|
|
44
|
+
const outputPath = getComponentOutputPath(handoff);
|
|
45
|
+
return {
|
|
46
|
+
mainJs: fs.existsSync(path.join(outputPath, MAIN_COMPONENT_JS_FILE)),
|
|
47
|
+
mainCss: fs.existsSync(path.join(outputPath, MAIN_COMPONENT_CSS_FILE)),
|
|
48
|
+
sharedCss: fs.existsSync(path.join(outputPath, SHARED_COMPONENT_CSS_FILE)),
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** Options controlling which shared style references a renderer participates in. */
|
|
53
|
+
export interface SharedStyleOptions {
|
|
54
|
+
/**
|
|
55
|
+
* Whether the rendered component opts into the shared-styles split (`component/shared.css`).
|
|
56
|
+
* Only renderers that consume shared styles (Handlebars) set this; it is still gated on the
|
|
57
|
+
* artifact actually existing.
|
|
58
|
+
*/
|
|
59
|
+
includeSharedStyles?: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Build the structured shared-artifact references a preview/inspect document depends on, filtered to
|
|
64
|
+
* the artifacts that actually exist. Shared/global artifacts are optional references (`required:
|
|
65
|
+
* false`) owned by no entity (`ownerKind: 'asset'`, `ownerId: null`). Stylesheet references precede
|
|
66
|
+
* the script reference so consumers preserve the documented load order.
|
|
67
|
+
*/
|
|
68
|
+
export const getSharedArtifactReferences = (
|
|
69
|
+
presence: SharedArtifactPresence,
|
|
70
|
+
options?: SharedStyleOptions
|
|
71
|
+
): ArtifactReference[] => {
|
|
72
|
+
const references: ArtifactReference[] = [];
|
|
73
|
+
|
|
74
|
+
if (options?.includeSharedStyles && presence.sharedCss) {
|
|
75
|
+
references.push({ path: SHARED_STYLES_CSS_ARTIFACT_PATH, kind: 'shared', required: false, ownerKind: 'asset', ownerId: null });
|
|
76
|
+
}
|
|
77
|
+
if (presence.mainCss) {
|
|
78
|
+
references.push({ path: SHARED_MAIN_CSS_ARTIFACT_PATH, kind: 'shared', required: false, ownerKind: 'asset', ownerId: null });
|
|
79
|
+
}
|
|
80
|
+
if (presence.mainJs) {
|
|
81
|
+
references.push({ path: SHARED_MAIN_JS_ARTIFACT_PATH, kind: 'shared', required: false, ownerKind: 'asset', ownerId: null });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return references;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Render the `<link rel="stylesheet">` tags for the present shared stylesheet artifacts
|
|
89
|
+
* (`component/shared.css` then `component/main.css`), in load order. Returns the tags joined by a
|
|
90
|
+
* newline, or an empty string when no shared stylesheet exists.
|
|
91
|
+
*/
|
|
92
|
+
export const renderSharedStyleLinks = (
|
|
93
|
+
presence: SharedArtifactPresence,
|
|
94
|
+
basePath: string,
|
|
95
|
+
options?: SharedStyleOptions
|
|
96
|
+
): string =>
|
|
97
|
+
getSharedArtifactReferences(presence, options)
|
|
98
|
+
.filter((ref) => ref.path.endsWith('.css'))
|
|
99
|
+
.map((ref) => `<link rel="stylesheet" href="${buildArtifactUrl(ref.path, basePath)}">`)
|
|
100
|
+
.join('\n');
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Render the global preview script tag for `component/main.js` when present, or an empty string when
|
|
104
|
+
* absent. The global script is a classic (non-deferred) tag so it executes before dependent
|
|
105
|
+
* component-specific scripts and preserves intentional top-level side effects.
|
|
106
|
+
*/
|
|
107
|
+
export const renderGlobalScriptTag = (presence: SharedArtifactPresence, basePath: string): string =>
|
|
108
|
+
presence.mainJs ? `<script src="${buildArtifactUrl(SHARED_MAIN_JS_ARTIFACT_PATH, basePath)}"></script>` : '';
|
|
109
|
+
|
|
110
|
+
/** Which of a component's own optional artifacts exist in the component output directory. */
|
|
111
|
+
export interface ComponentArtifactPresence {
|
|
112
|
+
/** `component/<id>.css` — built only when the component declares an SCSS/CSS entry. */
|
|
113
|
+
css: boolean;
|
|
114
|
+
/** `component/<id>.js` — built only when the component declares a JS entry. */
|
|
115
|
+
js: boolean;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Resolve which of a component's own css/js artifacts exist on disk. A component's styles and
|
|
120
|
+
* scripts are built (or removed when their entry is absent) before its preview HTML is generated, so
|
|
121
|
+
* an existence check here reflects whether the artifact can be referenced without producing a
|
|
122
|
+
* missing-file browser request. Like the shared artifacts, these references are optional.
|
|
123
|
+
*/
|
|
124
|
+
export const resolveComponentArtifactPresence = (handoff: Handoff, componentId: string): ComponentArtifactPresence => {
|
|
125
|
+
const outputPath = getComponentOutputPath(handoff);
|
|
126
|
+
return {
|
|
127
|
+
css: fs.existsSync(path.join(outputPath, `${componentId}.css`)),
|
|
128
|
+
js: fs.existsSync(path.join(outputPath, `${componentId}.js`)),
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Render the `<link rel="stylesheet">` tag for a component's own stylesheet (`component/<id>.css`)
|
|
134
|
+
* when it exists, or an empty string when the component declares no styles — so generated HTML never
|
|
135
|
+
* references an absent optional artifact.
|
|
136
|
+
*/
|
|
137
|
+
export const renderComponentStyleLink = (presence: ComponentArtifactPresence, componentId: string, basePath: string): string =>
|
|
138
|
+
presence.css ? `<link rel="stylesheet" href="${buildArtifactUrl(`component/${componentId}.css`, basePath)}">` : '';
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Render the `<script>` tag for a component's own script (`component/<id>.js`) when it exists, or an
|
|
142
|
+
* empty string when the component declares no script. The component script is a classic tag loaded
|
|
143
|
+
* after the global script so global side effects run first.
|
|
144
|
+
*/
|
|
145
|
+
export const renderComponentScriptTag = (presence: ComponentArtifactPresence, componentId: string, basePath: string): string =>
|
|
146
|
+
presence.js ? `<script src="${buildArtifactUrl(`component/${componentId}.js`, basePath)}"></script>` : '';
|
|
@@ -86,8 +86,11 @@ export const getComponentOutputPath = (handoff: Handoff) => path.resolve(getAPIP
|
|
|
86
86
|
* @returns
|
|
87
87
|
*/
|
|
88
88
|
export async function componentTransformer(handoff: Handoff) {
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
// Build the shared/global artifacts first so component/pattern preview HTML can reference them
|
|
90
|
+
// only when they actually exist. A failed/absent global build therefore never produces a dangling
|
|
91
|
+
// reference in generated HTML.
|
|
91
92
|
await buildMainJS(handoff);
|
|
92
93
|
await buildMainCss(handoff);
|
|
94
|
+
const componentData = await processComponents(handoff);
|
|
95
|
+
await writeComponentSummaryAPI(handoff, componentData);
|
|
93
96
|
}
|