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
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import Handoff from '..';
|
|
4
|
+
import { Logger } from '../utils/logger';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Vercel Build Output API layout.
|
|
8
|
+
*
|
|
9
|
+
* `--package vercel` emits the Vercel Build Output API directory (`.vercel/output`) instead of the
|
|
10
|
+
* sites-directory deliverable. This module is the single seam that produces that layout; it is
|
|
11
|
+
* invoked as the final-assembly branch from the build functions and never re-stages or re-runs Next.
|
|
12
|
+
*
|
|
13
|
+
* Hard constraint: `.vercel/output` must live at the **repo root** (`handoff.workingPath`) with that
|
|
14
|
+
* exact name. Vercel ignores any `sitesOutputDirectory`-style override for this packaging.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Build Output API config version emitted in `.vercel/output/config.json`. */
|
|
18
|
+
const BUILD_OUTPUT_API_VERSION = 3;
|
|
19
|
+
|
|
20
|
+
/** Absolute path of the `.vercel/output` directory at the repo root. */
|
|
21
|
+
export const getVercelOutputPath = (handoff: Handoff): string => path.resolve(handoff.workingPath, '.vercel', 'output');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the app base path exactly as `next.config.mjs`'s `resolveBasePath` does, so the emitted
|
|
25
|
+
* `config.json` reflects the same `basePath` the static export was built with. Empty string means no
|
|
26
|
+
* base path (the Build Output API omits the field rather than carrying an empty value).
|
|
27
|
+
*/
|
|
28
|
+
const resolveBasePath = (rawBasePath: string | undefined): string => {
|
|
29
|
+
if (!rawBasePath) {
|
|
30
|
+
return '';
|
|
31
|
+
}
|
|
32
|
+
const trimmed = rawBasePath.replace(/^\/+|\/+$/g, '');
|
|
33
|
+
return trimmed ? `/${trimmed}` : '';
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Lay a materialized static export under `.vercel/output/static/` and write a minimal Build Output
|
|
38
|
+
* API v3 `config.json` carrying the app's `trailingSlash` and resolved `basePath`. Pure CDN files —
|
|
39
|
+
* no functions, no database.
|
|
40
|
+
*
|
|
41
|
+
* @param exportDir Absolute path of the materialized static export (the same content the static
|
|
42
|
+
* target writes to `out/<projectId>`).
|
|
43
|
+
*/
|
|
44
|
+
export const writeStaticVercelOutput = async (handoff: Handoff, exportDir: string): Promise<void> => {
|
|
45
|
+
const outputPath = getVercelOutputPath(handoff);
|
|
46
|
+
await fs.remove(outputPath);
|
|
47
|
+
await fs.ensureDir(outputPath);
|
|
48
|
+
|
|
49
|
+
// Lay the export under static/ — pure CDN files, served directly by Vercel.
|
|
50
|
+
const staticDir = path.join(outputPath, 'static');
|
|
51
|
+
await fs.copy(exportDir, staticDir, { overwrite: true });
|
|
52
|
+
|
|
53
|
+
// The staged Next app sets `trailingSlash: true` (src/app/next.config.mjs); the registry/static
|
|
54
|
+
// exports inherit it. Carry it and the resolved base path so the static deploy serves correct URLs.
|
|
55
|
+
const basePath = resolveBasePath(handoff.config?.app?.base_path);
|
|
56
|
+
const config: { version: number; trailingSlash: boolean; basePath?: string } = {
|
|
57
|
+
version: BUILD_OUTPUT_API_VERSION,
|
|
58
|
+
trailingSlash: true,
|
|
59
|
+
};
|
|
60
|
+
if (basePath) {
|
|
61
|
+
config.basePath = basePath;
|
|
62
|
+
}
|
|
63
|
+
await fs.writeFile(path.join(outputPath, 'config.json'), `${JSON.stringify(config, null, 2)}\n`);
|
|
64
|
+
|
|
65
|
+
Logger.success(`Packaged static Vercel Build Output API artifact at ${outputPath} (static/ + config.json).`);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Node.js runtime identifier baked into the registry function's `.vc-config.json`. The `pg` adapter
|
|
70
|
+
* (and the Neon serverless driver in its Node fallback) need the Node.js runtime, not the edge
|
|
71
|
+
* runtime — this is the required baseline. Vercel resolves the patch
|
|
72
|
+
* version within this major; pinning the major keeps the artifact stable.
|
|
73
|
+
*/
|
|
74
|
+
const VERCEL_NODE_RUNTIME = 'nodejs20.x';
|
|
75
|
+
|
|
76
|
+
/** Name (sans `.func`) of the single registry function. Served at `/` and the catch-all route. */
|
|
77
|
+
const REGISTRY_FUNCTION_NAME = 'index';
|
|
78
|
+
|
|
79
|
+
/** Handler file the Node launcher executes; wraps the Next standalone server as a request listener. */
|
|
80
|
+
const REGISTRY_FUNCTION_HANDLER = 'index.js';
|
|
81
|
+
|
|
82
|
+
/** Catch-all docs pages may be overridden by registry records and must always reach the function. */
|
|
83
|
+
const MUTABLE_REGISTRY_PAGE_SRC_ROUTE = '/[...slug]';
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Node launcher emitted into the registry function directory. The Next `standalone` `server.js`
|
|
87
|
+
* binds a port via `startServer`, which is not the Vercel Node function contract (a module exporting
|
|
88
|
+
* a `(req, res)` request listener). This launcher reuses the exact same request pipeline that
|
|
89
|
+
* `startServer` boots — the router-server handler from `next/dist/server/lib/start-server` — but
|
|
90
|
+
* without the port bind, so it serves requests identically to `node server.js`.
|
|
91
|
+
*
|
|
92
|
+
* Why not the public `next().getRequestHandler()`: that handler resolves page/document and `/api`
|
|
93
|
+
* routes, but does NOT resolve on-demand `/_next/data/<buildId>/…json` requests for
|
|
94
|
+
* `fallback:'blocking'` pages — it 404s them. That broke client-side `<Link>` navigation to the
|
|
95
|
+
* component/pattern detail pages (their data fetch went to the function and came back 404, which the
|
|
96
|
+
* Next router then turns into a hard navigation). `getRequestHandlers` returns the full router-server
|
|
97
|
+
* handler (data-route resolution, dynamic param extraction, `trailingSlash` redirects), so on-demand
|
|
98
|
+
* data routes resolve the same way they do under the standalone server. `minimalMode:false` keeps
|
|
99
|
+
* the function doing full in-process routing (the catch-all route forwards everything here).
|
|
100
|
+
*
|
|
101
|
+
* Runtime mode, adapter, and the DB env-var *name* are already baked into the resolved config the
|
|
102
|
+
* standalone build wrote to `.next/required-server-files.json`; the connection-string value is still
|
|
103
|
+
* read from the named env var at request time, never baked.
|
|
104
|
+
*/
|
|
105
|
+
const REGISTRY_FUNCTION_LAUNCHER = `// Generated by handoff-app (\`build --target registry --package vercel\`). Do not edit.
|
|
106
|
+
// Wraps the Next.js standalone server as a Vercel Build Output API Node function.
|
|
107
|
+
process.env.NODE_ENV = 'production';
|
|
108
|
+
process.chdir(__dirname);
|
|
109
|
+
|
|
110
|
+
const requiredServerFiles = require('./.next/required-server-files.json');
|
|
111
|
+
const nextConfig = requiredServerFiles.config;
|
|
112
|
+
|
|
113
|
+
// The standalone runtime reads its resolved config from this env var (mirrors standalone server.js).
|
|
114
|
+
process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(nextConfig);
|
|
115
|
+
|
|
116
|
+
// require('next') first so its module hooks are installed before pulling the internal server lib,
|
|
117
|
+
// matching the require order of the generated standalone server.js.
|
|
118
|
+
require('next');
|
|
119
|
+
const { getRequestHandlers } = require('next/dist/server/lib/start-server');
|
|
120
|
+
|
|
121
|
+
// Initialize the router-server handler once on cold start and reuse it across warm invocations.
|
|
122
|
+
// This is the same handler the standalone server.js uses; it does NOT bind a port (the port bind
|
|
123
|
+
// lives in startServer, which we never call), so it fits the (req, res) Node function contract.
|
|
124
|
+
let handlersPromise;
|
|
125
|
+
const ready = () =>
|
|
126
|
+
(handlersPromise =
|
|
127
|
+
handlersPromise ||
|
|
128
|
+
getRequestHandlers({
|
|
129
|
+
dir: __dirname,
|
|
130
|
+
port: parseInt(process.env.PORT, 10) || 3000,
|
|
131
|
+
hostname: process.env.HOSTNAME || 'localhost',
|
|
132
|
+
isDev: false,
|
|
133
|
+
minimalMode: false,
|
|
134
|
+
}));
|
|
135
|
+
|
|
136
|
+
module.exports = async (req, res) => {
|
|
137
|
+
const { requestHandler } = await ready();
|
|
138
|
+
return requestHandler(req, res);
|
|
139
|
+
};
|
|
140
|
+
`;
|
|
141
|
+
|
|
142
|
+
/** Build Output API Node serverless-function config (`.vc-config.json`). */
|
|
143
|
+
interface VercelNodeFunctionConfig {
|
|
144
|
+
runtime: string;
|
|
145
|
+
handler: string;
|
|
146
|
+
launcherType: 'Nodejs';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Options for {@link writeRegistryVercelOutput}. The build module owns the standalone assembly (entry
|
|
151
|
+
* flattening + static/public/config layout) and passes it in, keeping this seam free of any
|
|
152
|
+
* standalone-layout knowledge and avoiding a circular import.
|
|
153
|
+
*/
|
|
154
|
+
export interface RegistryVercelOutputOptions {
|
|
155
|
+
/** Absolute path of the staged Next app (source of `.next/static` + `public`). */
|
|
156
|
+
appPath: string;
|
|
157
|
+
/** Absolute path of the Next `standalone` build root (`<appPath>/.next/standalone`). */
|
|
158
|
+
standaloneRoot: string;
|
|
159
|
+
/**
|
|
160
|
+
* Lay the standalone bundle out under the given entry dir (flatten the server entry, copy static
|
|
161
|
+
* assets, public tree, and runtime config beside it) and return the absolute server-entry path.
|
|
162
|
+
* Shared with the standalone deliverable so both layouts stay identical.
|
|
163
|
+
*/
|
|
164
|
+
assembleStandalone: (handoff: Handoff, appPath: string, standaloneRoot: string, entryDir: string) => Promise<string>;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Copy the genuinely-static prerendered pages into the Edge CDN layer (`static/`) so their HTML and
|
|
169
|
+
* `_next/data` JSON are served directly by the CDN instead of invoking the single registry function.
|
|
170
|
+
* This is a load/latency optimization, not a correctness requirement: the function (see the launcher)
|
|
171
|
+
* now resolves on-demand data routes itself, so anything not copied here still resolves via the
|
|
172
|
+
* catch-all.
|
|
173
|
+
*
|
|
174
|
+
* Routes materialized from the mutable docs catch-all also appear in `routes` when package defaults
|
|
175
|
+
* are prerendered. They are identified by `srcRoute` and deliberately left behind the function so a
|
|
176
|
+
* published DB record can override the fallback. Other `fallback:'blocking'` dynamic routes are
|
|
177
|
+
* absent and naturally reach the function. Immutable pages — e.g. the header's `<Link href="/">`
|
|
178
|
+
* prefetch of `/_next/data/<buildId>/index.json` — are served straight from the CDN.
|
|
179
|
+
*
|
|
180
|
+
* No-op when nothing is prerendered (empty `routes`). Paths are emitted base-path-less, matching the
|
|
181
|
+
* existing `_next/static` copy and the registry route table.
|
|
182
|
+
*/
|
|
183
|
+
const copyPrerenderedStaticPages = async (appPath: string, staticOutDir: string): Promise<void> => {
|
|
184
|
+
const nextDir = path.resolve(appPath, '.next');
|
|
185
|
+
const manifestPath = path.join(nextDir, 'prerender-manifest.json');
|
|
186
|
+
const buildIdPath = path.join(nextDir, 'BUILD_ID');
|
|
187
|
+
if (!fs.existsSync(manifestPath) || !fs.existsSync(buildIdPath)) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const manifest = (await fs.readJson(manifestPath)) as {
|
|
192
|
+
routes?: Record<string, { dataRoute?: string | null; srcRoute?: string | null }>;
|
|
193
|
+
};
|
|
194
|
+
const routes = manifest.routes ?? {};
|
|
195
|
+
const serverPages = path.join(nextDir, 'server', 'pages');
|
|
196
|
+
|
|
197
|
+
let copied = 0;
|
|
198
|
+
for (const [route, routeConfig] of Object.entries(routes)) {
|
|
199
|
+
// A static copy would win at the Build Output API filesystem route and permanently hide a
|
|
200
|
+
// published override (and its on-demand revalidation) from the registry function.
|
|
201
|
+
if (routeConfig.srcRoute === MUTABLE_REGISTRY_PAGE_SRC_ROUTE) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// `.next/server/pages/<rel>.{html,json}` — `/` maps to `index`, nested routes keep their path.
|
|
206
|
+
const rel = route === '/' ? 'index' : route.replace(/^\/+/, '');
|
|
207
|
+
const htmlSrc = path.join(serverPages, `${rel}.html`);
|
|
208
|
+
const jsonSrc = path.join(serverPages, `${rel}.json`);
|
|
209
|
+
|
|
210
|
+
if (fs.existsSync(htmlSrc)) {
|
|
211
|
+
// trailingSlash:true → serve as `<route>/index.html`; `/` → `static/index.html`.
|
|
212
|
+
const htmlDestDir = route === '/' ? staticOutDir : path.join(staticOutDir, rel);
|
|
213
|
+
await fs.ensureDir(htmlDestDir);
|
|
214
|
+
await fs.copy(htmlSrc, path.join(htmlDestDir, 'index.html'), { overwrite: true });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// The data URL (with the build's BUILD_ID baked in) comes from the manifest's `dataRoute`.
|
|
218
|
+
const dataRoute = routeConfig.dataRoute;
|
|
219
|
+
if (dataRoute && fs.existsSync(jsonSrc)) {
|
|
220
|
+
const jsonDest = path.join(staticOutDir, dataRoute.replace(/^\/+/, ''));
|
|
221
|
+
await fs.ensureDir(path.dirname(jsonDest));
|
|
222
|
+
await fs.copy(jsonSrc, jsonDest, { overwrite: true });
|
|
223
|
+
copied += 1;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
Logger.info(`Copied ${copied} prerendered static page data file(s) into the Edge CDN layer.`);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Package the dynamic registry app as the Vercel Build Output API directory. Lays the traced
|
|
232
|
+
* standalone bundle into a single Node function (`functions/index.func/`)
|
|
233
|
+
* wrapping the Next standalone server, copies the immutable static assets + public tree under
|
|
234
|
+
* `static/` for the Edge CDN, and writes a route table that serves static files first and routes
|
|
235
|
+
* everything else (SSR pages, the docs read API, the registry API) to the function.
|
|
236
|
+
*
|
|
237
|
+
* Hard constraint: `.vercel/output` lives at the repo root with that exact name (see module header).
|
|
238
|
+
*/
|
|
239
|
+
export const writeRegistryVercelOutput = async (handoff: Handoff, options: RegistryVercelOutputOptions): Promise<void> => {
|
|
240
|
+
const { appPath, standaloneRoot, assembleStandalone } = options;
|
|
241
|
+
|
|
242
|
+
const outputPath = getVercelOutputPath(handoff);
|
|
243
|
+
await fs.remove(outputPath);
|
|
244
|
+
await fs.ensureDir(outputPath);
|
|
245
|
+
|
|
246
|
+
// The function directory *is* the standalone bundle root: the traced server + node_modules, the
|
|
247
|
+
// server entry flattened to its root, and `.next/static` + public + runtime config beside it. The
|
|
248
|
+
// launcher constructs a NextServer from this directory, so it needs the full standalone layout.
|
|
249
|
+
const functionDir = path.join(outputPath, 'functions', `${REGISTRY_FUNCTION_NAME}.func`);
|
|
250
|
+
await fs.ensureDir(functionDir);
|
|
251
|
+
await assembleStandalone(handoff, appPath, standaloneRoot, functionDir);
|
|
252
|
+
|
|
253
|
+
// The standalone `server.js` binds a port; replace the entry with the Build Output API Node
|
|
254
|
+
// launcher (a `(req, res)` request listener) and declare the Node runtime so the pg/Neon drivers
|
|
255
|
+
// work. `server.js` is left in place (harmless, unused) to keep the bundle a valid standalone too.
|
|
256
|
+
await fs.writeFile(path.join(functionDir, REGISTRY_FUNCTION_HANDLER), REGISTRY_FUNCTION_LAUNCHER);
|
|
257
|
+
const functionConfig: VercelNodeFunctionConfig = {
|
|
258
|
+
runtime: VERCEL_NODE_RUNTIME,
|
|
259
|
+
handler: REGISTRY_FUNCTION_HANDLER,
|
|
260
|
+
launcherType: 'Nodejs',
|
|
261
|
+
};
|
|
262
|
+
await fs.writeFile(path.join(functionDir, '.vc-config.json'), `${JSON.stringify(functionConfig, null, 2)}\n`);
|
|
263
|
+
|
|
264
|
+
// Serve the app's immutable assets straight from the Edge CDN. `_next/static` is content-hashed and
|
|
265
|
+
// public files sit at the URL root; copying them under `static/` lets the route table satisfy them
|
|
266
|
+
// via `handle: filesystem` before the catch-all reaches the function.
|
|
267
|
+
const staticOutDir = path.join(outputPath, 'static');
|
|
268
|
+
await fs.ensureDir(staticOutDir);
|
|
269
|
+
const nextStaticSrc = path.resolve(appPath, '.next', 'static');
|
|
270
|
+
if (fs.existsSync(nextStaticSrc)) {
|
|
271
|
+
await fs.copy(nextStaticSrc, path.join(staticOutDir, '_next', 'static'), { overwrite: true });
|
|
272
|
+
}
|
|
273
|
+
const publicSrc = path.resolve(appPath, 'public');
|
|
274
|
+
if (fs.existsSync(publicSrc)) {
|
|
275
|
+
await fs.copy(publicSrc, staticOutDir, { overwrite: true });
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Serve genuinely-static prerendered pages (HTML + `_next/data` JSON) from the CDN as an
|
|
279
|
+
// optimization so they skip the function. Dynamic `fallback:'blocking'` pages are not copied (their
|
|
280
|
+
// data is live), and the function now resolves their on-demand `_next/data` routes itself.
|
|
281
|
+
await copyPrerenderedStaticPages(appPath, staticOutDir);
|
|
282
|
+
|
|
283
|
+
// Route table: try static files first (`handle: filesystem` serves anything copied into static/),
|
|
284
|
+
// then route every remaining request — SSR/on-demand pages, their `/_next/data/*` routes,
|
|
285
|
+
// /api/docs/*, /api/registry/* — to the single function. The `.func` suffix is dropped from the
|
|
286
|
+
// URL, so `index.func` is reached via `/index`.
|
|
287
|
+
const config = {
|
|
288
|
+
version: BUILD_OUTPUT_API_VERSION,
|
|
289
|
+
routes: [{ handle: 'filesystem' }, { src: '/(.*)', dest: `/${REGISTRY_FUNCTION_NAME}` }],
|
|
290
|
+
};
|
|
291
|
+
await fs.writeFile(path.join(outputPath, 'config.json'), `${JSON.stringify(config, null, 2)}\n`);
|
|
292
|
+
|
|
293
|
+
Logger.success(
|
|
294
|
+
`Packaged registry Vercel Build Output API artifact at ${outputPath} ` +
|
|
295
|
+
`(functions/${REGISTRY_FUNCTION_NAME}.func + static/ + config.json).`
|
|
296
|
+
);
|
|
297
|
+
};
|
|
@@ -7,6 +7,8 @@ export interface WatcherState {
|
|
|
7
7
|
runtimeComponentsWatcher: chokidar.FSWatcher | null;
|
|
8
8
|
runtimeConfigurationWatcher: chokidar.FSWatcher | null;
|
|
9
9
|
componentDirectoriesWatcher: chokidar.FSWatcher | null;
|
|
10
|
+
/** Watches entries.patterns parent directories for newly created pattern subdirectories. */
|
|
11
|
+
patternDirectoriesWatcher: chokidar.FSWatcher | null;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
/**
|
|
@@ -2,6 +2,7 @@ import chokidar from 'chokidar';
|
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import Handoff from '..';
|
|
5
|
+
import { buildPatterns } from '../pipeline/patterns';
|
|
5
6
|
import processComponents, { ComponentSegment } from '../transformers/preview/component/builder';
|
|
6
7
|
import { buildMainCss } from '../transformers/preview/component/css';
|
|
7
8
|
import { buildMainJS } from '../transformers/preview/component/javascript';
|
|
@@ -209,6 +210,15 @@ export const watchRuntimeComponents = (
|
|
|
209
210
|
}
|
|
210
211
|
};
|
|
211
212
|
|
|
213
|
+
const rebuildPatternComponentPreviews = async (handoff: Handoff, patternId: string) => {
|
|
214
|
+
const pattern = handoff.runtimeConfig?.entries?.patterns?.[patternId];
|
|
215
|
+
if (!pattern?.components?.length) return;
|
|
216
|
+
|
|
217
|
+
for (const ref of pattern.components) {
|
|
218
|
+
await processComponents(handoff, ref.id, ComponentSegment.Previews);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
212
222
|
/**
|
|
213
223
|
* Watches the runtime configuration for changes.
|
|
214
224
|
*
|
|
@@ -256,6 +266,8 @@ export const watchRuntimeConfiguration = (handoff: Handoff, state: WatcherState)
|
|
|
256
266
|
await processComponents(handoff);
|
|
257
267
|
} else if (handle) {
|
|
258
268
|
await handle.apply(handoff, entryAfter?.entityId);
|
|
269
|
+
} else if (entryAfter?.kind === 'pattern') {
|
|
270
|
+
await rebuildPatternComponentPreviews(handoff, entryAfter.entityId);
|
|
259
271
|
} else {
|
|
260
272
|
const effectiveId = entryAfter?.entityId ?? path.basename(path.dirname(changedFile));
|
|
261
273
|
await processComponents(handoff, effectiveId);
|
|
@@ -273,22 +285,50 @@ export const watchRuntimeConfiguration = (handoff: Handoff, state: WatcherState)
|
|
|
273
285
|
};
|
|
274
286
|
|
|
275
287
|
/**
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
288
|
+
* Shared factory for watching a parent directory for newly created entity
|
|
289
|
+
* subdirectories (components or patterns). Handles the common scaffolding so
|
|
290
|
+
* that callers only describe what differs for their entity kind:
|
|
291
|
+
*
|
|
292
|
+
* - which config-entry paths to watch
|
|
293
|
+
* - how to read the current known ids after a reload
|
|
294
|
+
* - which WatcherState slot to use
|
|
295
|
+
* - a log label for the entity kind
|
|
296
|
+
* - the rebuild work to run once the runtime config is fresh
|
|
280
297
|
*/
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
298
|
+
const watchEntityDirectories = (handoff: Handoff, state: WatcherState, chokidarConfig: chokidar.WatchOptions, options: {
|
|
299
|
+
/** Config paths to watch, e.g. handoff.config.entries?.components */
|
|
300
|
+
getConfigPaths: (handoff: Handoff) => string[];
|
|
301
|
+
/** Current known entity ids from runtime config, called again after reload to refresh the set */
|
|
302
|
+
getKnownIds: (handoff: Handoff) => string[];
|
|
303
|
+
/** Read the active FSWatcher from state */
|
|
304
|
+
getWatcher: (state: WatcherState) => chokidar.FSWatcher | null;
|
|
305
|
+
/** Write the active FSWatcher back into state */
|
|
306
|
+
setWatcher: (state: WatcherState, watcher: chokidar.FSWatcher) => void;
|
|
307
|
+
/** Prefix for the scheduleHandler key, e.g. 'newComponent' or 'newPattern' */
|
|
308
|
+
scheduleKeyPrefix: string;
|
|
309
|
+
/** Human-readable label used in log messages, e.g. 'component' or 'pattern' */
|
|
310
|
+
entityLabel: string;
|
|
311
|
+
/**
|
|
312
|
+
* Entity-specific rebuild work, called after reload + persistClientConfig +
|
|
313
|
+
* watcher re-registration. Receives the handoff instance (with fresh runtime
|
|
314
|
+
* config), the triggering directory name, and actual entity ids discovered
|
|
315
|
+
* after reload.
|
|
316
|
+
*/
|
|
317
|
+
onDetected: (handoff: Handoff, context: { dirName: string; addedIds: string[] }) => Promise<void>;
|
|
318
|
+
}) => {
|
|
319
|
+
const { getConfigPaths, getKnownIds, getWatcher, setWatcher, scheduleKeyPrefix, entityLabel, onDetected } = options;
|
|
320
|
+
|
|
321
|
+
const existingWatcher = getWatcher(state);
|
|
322
|
+
if (existingWatcher) {
|
|
323
|
+
existingWatcher.close();
|
|
284
324
|
}
|
|
285
325
|
|
|
286
|
-
const
|
|
287
|
-
if (
|
|
326
|
+
const configPaths = getConfigPaths(handoff);
|
|
327
|
+
if (configPaths.length === 0) return;
|
|
288
328
|
|
|
289
329
|
const dirsToWatch: string[] = [];
|
|
290
|
-
for (const
|
|
291
|
-
const resolved = path.resolve(handoff.workingPath,
|
|
330
|
+
for (const configPath of configPaths) {
|
|
331
|
+
const resolved = path.resolve(handoff.workingPath, configPath);
|
|
292
332
|
if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
|
|
293
333
|
dirsToWatch.push(resolved);
|
|
294
334
|
}
|
|
@@ -296,40 +336,124 @@ export const watchComponentDirectories = (handoff: Handoff, state: WatcherState,
|
|
|
296
336
|
|
|
297
337
|
if (dirsToWatch.length === 0) return;
|
|
298
338
|
|
|
299
|
-
|
|
339
|
+
// Snapshot known ids at watch startup so we only react to genuinely new
|
|
340
|
+
// entities, not to file changes inside already-known directories.
|
|
341
|
+
const knownIds = new Set(getKnownIds(handoff));
|
|
300
342
|
|
|
301
|
-
|
|
343
|
+
const watcher = chokidar.watch(dirsToWatch, {
|
|
302
344
|
...chokidarConfig,
|
|
303
345
|
depth: 1,
|
|
304
346
|
});
|
|
347
|
+
setWatcher(state, watcher);
|
|
305
348
|
|
|
306
|
-
|
|
349
|
+
watcher.on('add', async (file) => {
|
|
307
350
|
const basename = path.basename(file);
|
|
308
351
|
const dirName = path.basename(path.dirname(file));
|
|
309
352
|
|
|
310
|
-
|
|
311
|
-
|
|
353
|
+
// Only react to the primary declaration file for the directory (e.g.
|
|
354
|
+
// button.json, button.js, or button.handoff.ts inside a button/ subdir).
|
|
355
|
+
// The basename.startsWith(dirName) guard below ensures .ts here only
|
|
356
|
+
// ever matches files named after their parent directory, not arbitrary
|
|
357
|
+
// TypeScript source files.
|
|
358
|
+
const isConfigFile = basename.endsWith('.json') || basename.endsWith('.js') || basename.endsWith('.cjs') || basename.endsWith('.ts');
|
|
359
|
+
const isNewEntity = isConfigFile && basename.startsWith(dirName) && !knownIds.has(dirName);
|
|
312
360
|
|
|
313
|
-
if (!
|
|
361
|
+
if (!isNewEntity) return;
|
|
314
362
|
|
|
315
|
-
await scheduleHandler(state,
|
|
363
|
+
await scheduleHandler(state, `${scheduleKeyPrefix}:${dirName}`, async () => {
|
|
316
364
|
try {
|
|
317
|
-
Logger.warn(`New
|
|
365
|
+
Logger.warn(`New ${entityLabel} detected: ${dirName}. Reloading configuration...`);
|
|
366
|
+
|
|
367
|
+
// Snapshot ids before reload so we can tell whether the new entity
|
|
368
|
+
// was successfully registered afterwards.
|
|
369
|
+
const idsBefore = new Set(getKnownIds(handoff));
|
|
370
|
+
|
|
318
371
|
handoff.reload();
|
|
319
|
-
knownComponents.add(dirName);
|
|
320
372
|
|
|
321
|
-
|
|
322
|
-
|
|
373
|
+
// Refresh from the post-reload runtime config so any ids that were
|
|
374
|
+
// discovered alongside the new entity are also marked as known.
|
|
375
|
+
const idsAfter = getKnownIds(handoff);
|
|
376
|
+
const addedIds = idsAfter.filter((id) => !idsBefore.has(id));
|
|
377
|
+
for (const id of idsAfter) {
|
|
378
|
+
knownIds.add(id);
|
|
323
379
|
}
|
|
324
380
|
|
|
325
381
|
await persistClientConfig(handoff);
|
|
382
|
+
|
|
383
|
+
// Re-register file watchers so the new config file and any runtime
|
|
384
|
+
// files it introduces are covered going forward. This must happen even
|
|
385
|
+
// when the file failed to parse so that watchRuntimeConfiguration can
|
|
386
|
+
// trigger a retry the moment the file is saved with valid content.
|
|
326
387
|
watchRuntimeComponents(handoff, state, getRuntimeComponentsPathsToWatch(handoff));
|
|
327
388
|
watchRuntimeConfiguration(handoff, state);
|
|
328
|
-
|
|
329
|
-
|
|
389
|
+
|
|
390
|
+
// If no new ids appeared after reload the file was empty or invalid.
|
|
391
|
+
// Skip the build — watchRuntimeConfiguration will fire when it is saved.
|
|
392
|
+
if (addedIds.length === 0) {
|
|
393
|
+
Logger.warn(`${entityLabel} "${dirName}" config is empty or incomplete — build will run automatically once the file is saved.`);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
await onDetected(handoff, { dirName, addedIds });
|
|
330
398
|
} catch (e) {
|
|
331
|
-
Logger.error(
|
|
399
|
+
Logger.error(`Error processing new ${entityLabel}:`, e);
|
|
332
400
|
}
|
|
333
401
|
});
|
|
334
402
|
});
|
|
335
403
|
};
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Watches the parent component directories from config.entries.components for
|
|
407
|
+
* new components being added. When a new config file appears in a new
|
|
408
|
+
* subdirectory, reloads the runtime config, restarts the component and
|
|
409
|
+
* configuration watchers, then builds the new component and any patterns that
|
|
410
|
+
* reference it.
|
|
411
|
+
*/
|
|
412
|
+
export const watchComponentDirectories = (handoff: Handoff, state: WatcherState, chokidarConfig: chokidar.WatchOptions) => {
|
|
413
|
+
watchEntityDirectories(handoff, state, chokidarConfig, {
|
|
414
|
+
getConfigPaths: (h) => h.config.entries?.components ?? [],
|
|
415
|
+
getKnownIds: (h) => Object.keys(h.runtimeConfig?.entries?.components ?? {}),
|
|
416
|
+
getWatcher: (s) => s.componentDirectoriesWatcher,
|
|
417
|
+
setWatcher: (s, w) => { s.componentDirectoriesWatcher = w; },
|
|
418
|
+
scheduleKeyPrefix: 'newComponent',
|
|
419
|
+
entityLabel: 'component',
|
|
420
|
+
onDetected: async (handoff, { addedIds }) => {
|
|
421
|
+
for (const componentId of addedIds) {
|
|
422
|
+
await processComponents(handoff, componentId);
|
|
423
|
+
await runAllFinalizers(handoff, { patternRebuildComponentIds: [componentId] });
|
|
424
|
+
}
|
|
425
|
+
},
|
|
426
|
+
});
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Watches the parent pattern directories from config.entries.patterns for new
|
|
431
|
+
* patterns being added. The rebuild sequence intentionally differs from
|
|
432
|
+
* watchComponentDirectories because the dependency arrow is reversed: a new
|
|
433
|
+
* pattern references existing components, so we rebuild only the preview
|
|
434
|
+
* segment of those components (to produce the __pattern_* HTML files that
|
|
435
|
+
* injectPatternPreviews registered) before composing the pattern itself.
|
|
436
|
+
*/
|
|
437
|
+
export const watchPatternDirectories = (handoff: Handoff, state: WatcherState, chokidarConfig: chokidar.WatchOptions) => {
|
|
438
|
+
watchEntityDirectories(handoff, state, chokidarConfig, {
|
|
439
|
+
getConfigPaths: (h) => h.config.entries?.patterns ?? [],
|
|
440
|
+
getKnownIds: (h) => Object.keys(h.runtimeConfig?.entries?.patterns ?? {}),
|
|
441
|
+
getWatcher: (s) => s.patternDirectoriesWatcher,
|
|
442
|
+
setWatcher: (s, w) => { s.patternDirectoriesWatcher = w; },
|
|
443
|
+
scheduleKeyPrefix: 'newPattern',
|
|
444
|
+
entityLabel: 'pattern',
|
|
445
|
+
onDetected: async (handoff, { addedIds }) => {
|
|
446
|
+
// Rebuild only the previews segment for each component the new pattern
|
|
447
|
+
// references. Their JS/CSS/structure are unchanged — only the new
|
|
448
|
+
// synthetic preview HTML files need to be generated so that buildPatterns
|
|
449
|
+
// can assemble the pattern from them.
|
|
450
|
+
for (const patternId of addedIds) {
|
|
451
|
+
await rebuildPatternComponentPreviews(handoff, patternId);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Build only the new pattern directly rather than going through
|
|
455
|
+
// runAllFinalizers, which would rebuild all patterns unnecessarily.
|
|
456
|
+
await buildPatterns(handoff, { onlyPatternIds: new Set(addedIds) });
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { WebSocket } from 'ws';
|
|
1
|
+
import { WebSocket, WebSocketServer } from 'ws';
|
|
2
2
|
import { Logger } from '../utils/logger';
|
|
3
3
|
|
|
4
4
|
export interface HandoffWebSocket extends WebSocket {
|
|
@@ -15,7 +15,7 @@ export interface HandoffWebSocket extends WebSocket {
|
|
|
15
15
|
export const createWebSocketServer = async (port: number = 3001) => {
|
|
16
16
|
Logger.info(`Starting WebSocket server for live reload on port ${port}…`);
|
|
17
17
|
|
|
18
|
-
const wss = new
|
|
18
|
+
const wss = new WebSocketServer({ port });
|
|
19
19
|
|
|
20
20
|
wss.on('listening', () => {
|
|
21
21
|
Logger.info(`WebSocket server listening on ws://localhost:${port} (live reload)`);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Artifacts Module
|
|
2
|
+
|
|
3
|
+
Shared v2 artifact contract: the structured artifact/build-metadata model the workspace and
|
|
4
|
+
registry tracks both produce and consume, plus the one canonical artifact URL builder used by
|
|
5
|
+
all generated HTML and docs read data. Pure types and utilities — no artifact is read or written
|
|
6
|
+
here; consumers (store, docs read API, build, publish/ingest) come in later issues.
|
|
7
|
+
|
|
8
|
+
## Files
|
|
9
|
+
|
|
10
|
+
| File | Purpose |
|
|
11
|
+
|------|---------|
|
|
12
|
+
| `types.ts` | `ArtifactDescriptor`, `ArtifactReference`, `ArtifactBuildMetadata`, `ArtifactSizeDiagnostics`, artifact kind/owner/status unions, and shared-artifact/manifest path + format-version constants |
|
|
13
|
+
| `url.ts` | `buildArtifactUrl()`, `normalizeBasePath()`, `ARTIFACTS_ROUTE_SEGMENT` — centralized `{basePath}/api/docs/artifacts/{path}` URL builder (segment-encoded, basePath-preserved, never query strings) |
|
|
14
|
+
| `index.ts` | Barrel re-exports |
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export {
|
|
2
|
+
ARTIFACT_METADATA_PATH,
|
|
3
|
+
REACT_PREVIEW_FORMAT_VERSION,
|
|
4
|
+
SHARED_MAIN_CSS_ARTIFACT_PATH,
|
|
5
|
+
SHARED_MAIN_JS_ARTIFACT_PATH,
|
|
6
|
+
SHARED_STYLES_CSS_ARTIFACT_PATH,
|
|
7
|
+
type ArtifactBuildMetadata,
|
|
8
|
+
type ArtifactBuildStatus,
|
|
9
|
+
type ArtifactDescriptor,
|
|
10
|
+
type ArtifactKind,
|
|
11
|
+
type ArtifactOwnerKind,
|
|
12
|
+
type ArtifactReference,
|
|
13
|
+
type ArtifactReferenceKind,
|
|
14
|
+
type ArtifactSizeDiagnostics,
|
|
15
|
+
} from './types';
|
|
16
|
+
|
|
17
|
+
export { ARTIFACTS_ROUTE_SEGMENT, buildArtifactUrl, normalizeBasePath } from './url';
|