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
package/src/app-builder/build.ts
CHANGED
|
@@ -7,9 +7,15 @@ import { buildPatterns } from '../pipeline/patterns';
|
|
|
7
7
|
import processComponents from '../transformers/preview/component/builder';
|
|
8
8
|
import { buildMainCss } from '../transformers/preview/component/css';
|
|
9
9
|
import { buildMainJS } from '../transformers/preview/component/javascript';
|
|
10
|
+
import { resolveApiTokenEnv, resolveDatabaseUrlEnv, resolveRegistryAdapter } from '../registry/db/adapter';
|
|
11
|
+
import { resolveAssetStorageFromConfig } from '../registry/asset-storage/resolve';
|
|
12
|
+
import type { RuntimeMode } from '../types/config';
|
|
10
13
|
import { Logger } from '../utils/logger';
|
|
11
14
|
import { generateTokensApi, persistClientConfig } from './client-config';
|
|
15
|
+
import { generateDefaultPages, generateNavShell } from './nav-shell';
|
|
12
16
|
import { getAppPath, syncPublicFiles } from './paths';
|
|
17
|
+
import { materializeDocsReadModel, validateReferencedArtifacts } from './static-export';
|
|
18
|
+
import { getVercelOutputPath, writeRegistryVercelOutput, writeStaticVercelOutput } from './vercel-output';
|
|
13
19
|
import {
|
|
14
20
|
WatcherState,
|
|
15
21
|
getRuntimeComponentsPathsToWatch,
|
|
@@ -17,14 +23,97 @@ import {
|
|
|
17
23
|
watchComponentDirectories,
|
|
18
24
|
watchGlobalEntries,
|
|
19
25
|
watchPages,
|
|
26
|
+
watchPatternDirectories,
|
|
20
27
|
watchPublicDirectory,
|
|
21
28
|
watchRuntimeComponents,
|
|
22
29
|
watchRuntimeConfiguration,
|
|
23
30
|
} from './watchers';
|
|
24
31
|
import { createWebSocketServer } from './websocket';
|
|
25
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Resolved build target. Bare `handoff-app build` resolves to `static`. The
|
|
35
|
+
* target — not `NODE_ENV` — drives whether Next runs a static export, so a future non-static
|
|
36
|
+
* `next build` path (registry) does not get conflated with static export.
|
|
37
|
+
*/
|
|
38
|
+
export type BuildTarget = 'static' | 'registry';
|
|
39
|
+
|
|
40
|
+
/** The default build target when none is supplied on the CLI. */
|
|
41
|
+
export const DEFAULT_BUILD_TARGET: BuildTarget = 'static';
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Resolved packaging axis — orthogonal to {@link BuildTarget}. It
|
|
45
|
+
* selects *how* the build is packaged, not *what* is built:
|
|
46
|
+
* - `standalone` → the existing sites-directory deliverable (`out/<projectId>` for static, the
|
|
47
|
+
* Node standalone bundle `out/registry` for registry).
|
|
48
|
+
* - `vercel` → the Vercel Build Output API directory (`.vercel/output`) at the repo root.
|
|
49
|
+
*
|
|
50
|
+
* `--package` never implies a {@link BuildTarget}; it is optional and additive. When omitted, the
|
|
51
|
+
* effective package is target-specific (`standalone` for both targets) so existing behavior is
|
|
52
|
+
* unchanged.
|
|
53
|
+
*/
|
|
54
|
+
export type BuildPackage = 'standalone' | 'vercel';
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* An expected build configuration / flow failure surfaced to the CLI — an invalid `(target,
|
|
58
|
+
* package)` combination, an unsupported runtime mode for the target, or a not-yet-available
|
|
59
|
+
* deliverable. These are actionable user-facing conditions, not bugs, so the CLI prints the message
|
|
60
|
+
* on its own (no stack trace). Genuine/unexpected failures stay plain `Error`s and keep their stack.
|
|
61
|
+
*/
|
|
62
|
+
export class HandoffBuildError extends Error {
|
|
63
|
+
constructor(message: string) {
|
|
64
|
+
super(message);
|
|
65
|
+
this.name = 'HandoffBuildError';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Resolve and validate the effective `(target, package)` pair.
|
|
71
|
+
*
|
|
72
|
+
* `--package` is optional and never implies a target. When omitted the effective package is
|
|
73
|
+
* `standalone` for both targets, preserving today's `out/<projectId>` / `out/registry` deliverables.
|
|
74
|
+
* The one rejected combination is `static + standalone`: a static snapshot has no server to package
|
|
75
|
+
* as a Node standalone bundle.
|
|
76
|
+
*/
|
|
77
|
+
const resolveBuildPackage = (target: BuildTarget, buildPackage?: BuildPackage): BuildPackage => {
|
|
78
|
+
const resolved: BuildPackage = buildPackage ?? 'standalone';
|
|
79
|
+
if (target === 'static' && buildPackage === 'standalone') {
|
|
80
|
+
throw new HandoffBuildError(
|
|
81
|
+
'Cannot package the static target as a Node standalone bundle (`--target static --package standalone`). ' +
|
|
82
|
+
'A static snapshot has no server to run; omit `--package` for the static export, or use ' +
|
|
83
|
+
'`--package vercel` for the Vercel Build Output API, or build `--target registry` for the standalone server bundle.'
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return resolved;
|
|
87
|
+
};
|
|
88
|
+
|
|
26
89
|
const escapeForSingleQuotedJsString = (value: string): string => value.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
27
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Guard the static build target against a registry-only runtime configuration. A static export
|
|
93
|
+
* builds and serves the local workspace; a `runtime.mode: 'registry'` project has no local workspace
|
|
94
|
+
* to export, so the build fails clearly rather than producing an empty or misleading site.
|
|
95
|
+
*/
|
|
96
|
+
const assertStaticBuildAllowed = (handoff: Handoff): void => {
|
|
97
|
+
const mode = handoff.config?.runtime?.mode ?? 'workspace';
|
|
98
|
+
if (mode === 'registry') {
|
|
99
|
+
throw new HandoffBuildError(
|
|
100
|
+
'Cannot run the static build target with a registry-only runtime (runtime.mode: "registry"). ' +
|
|
101
|
+
'The static target builds and exports the local workspace; switch runtime.mode to "workspace" ' +
|
|
102
|
+
'or package the registry app with `handoff-app build --target registry`.'
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Output directory for the packaged registry app. Lives under the configurable sites output
|
|
109
|
+
* directory (`sitesOutputDirectory`, default `out`) alongside the static target's `out/<projectId>`
|
|
110
|
+
* deliverable — both build targets emit to one discoverable, gitignored output root, keeping the
|
|
111
|
+
* package's `.handoff` for internal build/staging only. The registry artifact is a single
|
|
112
|
+
* deployable, so it is not keyed by project id.
|
|
113
|
+
*/
|
|
114
|
+
export const getRegistryBuildOutputPath = (handoff: Handoff): string =>
|
|
115
|
+
path.resolve(handoff.workingPath, handoff.sitesDirectory, 'registry');
|
|
116
|
+
|
|
28
117
|
/**
|
|
29
118
|
* Performs cleanup of the application directory by removing the existing app directory if it exists.
|
|
30
119
|
*/
|
|
@@ -37,22 +126,48 @@ const cleanupAppDirectory = async (handoff: Handoff): Promise<void> => {
|
|
|
37
126
|
}
|
|
38
127
|
};
|
|
39
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Options controlling how the project app is staged for a build target.
|
|
131
|
+
*/
|
|
132
|
+
interface InitializeProjectAppOptions {
|
|
133
|
+
/**
|
|
134
|
+
* Whether to stage workspace-derived artifacts (generated tokens API + the workspace `public`
|
|
135
|
+
* tree). The static target serves these from the exported site; the registry target must never
|
|
136
|
+
* depend on workspace source, so it skips them and serves everything from the database instead.
|
|
137
|
+
* Defaults to `true` (workspace dev/static behavior).
|
|
138
|
+
*/
|
|
139
|
+
includeWorkspaceArtifacts?: boolean;
|
|
140
|
+
/**
|
|
141
|
+
* Force the runtime mode baked into the app bundle, irrespective of the source project's
|
|
142
|
+
* `runtime.mode`. The registry build sets this to `registry` so the packaged artifact always runs
|
|
143
|
+
* as a registry deployment even when built from a workspace-mode project (mode stays config-only
|
|
144
|
+
* at runtime — it is fixed here by the build target, not inferred from the deploy environment).
|
|
145
|
+
*/
|
|
146
|
+
runtimeModeOverride?: RuntimeMode;
|
|
147
|
+
}
|
|
148
|
+
|
|
40
149
|
/**
|
|
41
150
|
* Prepares the project application by copying source files and configuring Next.js.
|
|
42
151
|
*
|
|
43
152
|
* @returns The path to the prepared application directory
|
|
44
153
|
*/
|
|
45
|
-
const initializeProjectApp = async (handoff: Handoff): Promise<string> => {
|
|
154
|
+
const initializeProjectApp = async (handoff: Handoff, options: InitializeProjectAppOptions = {}): Promise<string> => {
|
|
155
|
+
const includeWorkspaceArtifacts = options.includeWorkspaceArtifacts ?? true;
|
|
156
|
+
const runtimeMode = options.runtimeModeOverride ?? handoff.config?.runtime?.mode ?? 'workspace';
|
|
46
157
|
const srcPath = path.resolve(handoff.modulePath, 'src', 'app');
|
|
47
158
|
const appPath = getAppPath(handoff);
|
|
48
159
|
|
|
49
|
-
// Publish tokens API
|
|
50
|
-
|
|
160
|
+
// Publish tokens API (workspace-derived; skipped for the registry target which serves from the DB).
|
|
161
|
+
if (includeWorkspaceArtifacts) {
|
|
162
|
+
await generateTokensApi(handoff);
|
|
163
|
+
}
|
|
51
164
|
|
|
52
165
|
// Prepare project app dir
|
|
53
166
|
await fs.ensureDir(appPath);
|
|
54
167
|
await fs.copy(srcPath, appPath, { overwrite: true, filter: (file) => !file.includes('next.config.mjs') });
|
|
55
|
-
|
|
168
|
+
if (includeWorkspaceArtifacts) {
|
|
169
|
+
await syncPublicFiles(handoff);
|
|
170
|
+
}
|
|
56
171
|
|
|
57
172
|
// Copy custom theme CSS if it exists in the user's project
|
|
58
173
|
const customThemePath = path.resolve(handoff.workingPath, 'theme.css');
|
|
@@ -80,6 +195,19 @@ const initializeProjectApp = async (handoff: Handoff): Promise<string> => {
|
|
|
80
195
|
const escapedModulePath = escapeForSingleQuotedJsString(handoffModulePath);
|
|
81
196
|
const escapedExportPath = escapeForSingleQuotedJsString(handoffExportPath);
|
|
82
197
|
const escapedWebsocketPort = escapeForSingleQuotedJsString(String(handoffWebsocketPort));
|
|
198
|
+
// Resolved runtime mode + registry connection inputs (names only) baked into the Next bundle so the
|
|
199
|
+
// deployed registry app resolves its mode/DB env-var name without any build-machine filesystem.
|
|
200
|
+
const escapedRuntimeMode = escapeForSingleQuotedJsString(runtimeMode);
|
|
201
|
+
const escapedRegistryAdapter = escapeForSingleQuotedJsString(resolveRegistryAdapter(handoff.config));
|
|
202
|
+
const escapedDatabaseUrlEnv = escapeForSingleQuotedJsString(resolveDatabaseUrlEnv(handoff.config));
|
|
203
|
+
const escapedApiTokenEnv = escapeForSingleQuotedJsString(resolveApiTokenEnv(handoff.config));
|
|
204
|
+
// Asset storage selection baked (provider + module + env-var names + non-secret options JSON).
|
|
205
|
+
const assetStorage = resolveAssetStorageFromConfig(handoff.config);
|
|
206
|
+
const escapedAssetStorageAdapter = escapeForSingleQuotedJsString(assetStorage.adapterKind);
|
|
207
|
+
const escapedAssetStorageModule = escapeForSingleQuotedJsString(assetStorage.module ?? '');
|
|
208
|
+
const escapedAssetStorageTokenEnv = escapeForSingleQuotedJsString(assetStorage.tokenEnv);
|
|
209
|
+
const escapedAssetStorageMaxInline = escapeForSingleQuotedJsString(String(assetStorage.maxInlineBytes));
|
|
210
|
+
const escapedAssetStorageOptions = escapeForSingleQuotedJsString(JSON.stringify(assetStorage.options ?? {}));
|
|
83
211
|
const placeholderValues: Record<string, string> = {
|
|
84
212
|
'%HANDOFF_PROJECT_ID%': escapedProjectId,
|
|
85
213
|
'%HANDOFF_APP_BASE_PATH%': escapedAppBasePath,
|
|
@@ -87,19 +215,54 @@ const initializeProjectApp = async (handoff: Handoff): Promise<string> => {
|
|
|
87
215
|
'%HANDOFF_MODULE_PATH%': escapedModulePath,
|
|
88
216
|
'%HANDOFF_EXPORT_PATH%': escapedExportPath,
|
|
89
217
|
'%HANDOFF_WEBSOCKET_PORT%': escapedWebsocketPort,
|
|
218
|
+
'%HANDOFF_RUNTIME_MODE%': escapedRuntimeMode,
|
|
219
|
+
'%HANDOFF_REGISTRY_ADAPTER%': escapedRegistryAdapter,
|
|
220
|
+
'%HANDOFF_REGISTRY_DATABASE_URL_ENV%': escapedDatabaseUrlEnv,
|
|
221
|
+
'%HANDOFF_REGISTRY_API_TOKEN_ENV%': escapedApiTokenEnv,
|
|
222
|
+
'%HANDOFF_ASSET_STORAGE_ADAPTER%': escapedAssetStorageAdapter,
|
|
223
|
+
'%HANDOFF_ASSET_STORAGE_MODULE%': escapedAssetStorageModule,
|
|
224
|
+
'%HANDOFF_ASSET_STORAGE_TOKEN_ENV%': escapedAssetStorageTokenEnv,
|
|
225
|
+
'%HANDOFF_ASSET_STORAGE_MAX_INLINE_BYTES%': escapedAssetStorageMaxInline,
|
|
226
|
+
'%HANDOFF_ASSET_STORAGE_OPTIONS%': escapedAssetStorageOptions,
|
|
90
227
|
};
|
|
91
228
|
let nextConfigContent = await fs.readFile(nextConfigPath, 'utf-8');
|
|
92
229
|
for (const [placeholder, value] of Object.entries(placeholderValues)) {
|
|
93
230
|
nextConfigContent = nextConfigContent.split(placeholder).join(value);
|
|
94
231
|
}
|
|
95
232
|
await fs.writeFile(targetPath, nextConfigContent);
|
|
233
|
+
|
|
234
|
+
// Bake the markdown-driven navigation shell before `next build` so the docs read API can import it
|
|
235
|
+
// statically (it is unreadable at runtime in the Vercel registry function). Generated for every
|
|
236
|
+
// target; only registry-mode clients fetch it.
|
|
237
|
+
// Registry builds must not bake workspace pages into the shell — they come from the DB at runtime.
|
|
238
|
+
await generateNavShell(handoff, appPath, runtimeMode !== 'registry');
|
|
239
|
+
await generateDefaultPages(handoff, appPath);
|
|
240
|
+
|
|
96
241
|
return appPath;
|
|
97
242
|
};
|
|
98
243
|
|
|
99
244
|
/**
|
|
100
|
-
* Build the Next.js documentation application.
|
|
245
|
+
* Build the Next.js documentation application for the resolved target. The `static` target builds
|
|
246
|
+
* and exports the local workspace (default); the `registry` target packages the deployable dynamic
|
|
247
|
+
* registry app via {@link buildRegistryApp}.
|
|
101
248
|
*/
|
|
102
|
-
const buildApp = async (
|
|
249
|
+
const buildApp = async (
|
|
250
|
+
handoff: Handoff,
|
|
251
|
+
target: BuildTarget = DEFAULT_BUILD_TARGET,
|
|
252
|
+
skipComponents?: boolean,
|
|
253
|
+
buildPackage?: BuildPackage
|
|
254
|
+
): Promise<void> => {
|
|
255
|
+
// Resolve + validate the (target, package) pair once, before any work.
|
|
256
|
+
const resolvedPackage = resolveBuildPackage(target, buildPackage);
|
|
257
|
+
|
|
258
|
+
if (target === 'registry') {
|
|
259
|
+
await buildRegistryApp(handoff, resolvedPackage);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// A registry-only runtime has no local workspace to export — fail clearly before any work.
|
|
264
|
+
assertStaticBuildAllowed(handoff);
|
|
265
|
+
|
|
103
266
|
skipComponents = skipComponents ?? false;
|
|
104
267
|
// Perform cleanup
|
|
105
268
|
await cleanupAppDirectory(handoff);
|
|
@@ -115,13 +278,18 @@ const buildApp = async (handoff: Handoff, skipComponents?: boolean): Promise<voi
|
|
|
115
278
|
|
|
116
279
|
await persistClientConfig(handoff);
|
|
117
280
|
|
|
118
|
-
//
|
|
281
|
+
// Fail before export if generated HTML references a required artifact that cannot be materialized.
|
|
282
|
+
validateReferencedArtifacts(handoff);
|
|
283
|
+
|
|
284
|
+
// Build app. The static target drives Next's `output: 'export'` via HANDOFF_BUILD_TARGET so the
|
|
285
|
+
// export gate is tied to the resolved target, not to NODE_ENV.
|
|
119
286
|
const buildResult = spawn.sync('npx', ['next', 'build'], {
|
|
120
287
|
cwd: appPath,
|
|
121
288
|
stdio: ['inherit', 'pipe', 'pipe'],
|
|
122
289
|
env: {
|
|
123
290
|
...process.env,
|
|
124
291
|
NODE_ENV: 'production',
|
|
292
|
+
HANDOFF_BUILD_TARGET: target,
|
|
125
293
|
},
|
|
126
294
|
});
|
|
127
295
|
|
|
@@ -136,6 +304,19 @@ const buildApp = async (handoff: Handoff, skipComponents?: boolean): Promise<voi
|
|
|
136
304
|
throw new Error(errorMsg);
|
|
137
305
|
}
|
|
138
306
|
|
|
307
|
+
// Reproduce the docs read API (`/api/docs/*`) as route-shaped static files in the export output,
|
|
308
|
+
// since `output: 'export'` disables the live API routes.
|
|
309
|
+
const exportDir = path.resolve(appPath, 'out');
|
|
310
|
+
await materializeDocsReadModel(handoff, exportDir);
|
|
311
|
+
|
|
312
|
+
// Final assembly branches on the resolved package. `vercel` lays the materialized export
|
|
313
|
+
// under `.vercel/output/static/` at the repo root (not the sites directory) — a hard Vercel
|
|
314
|
+
// constraint; `standalone` keeps writing the `out/<projectId>` export.
|
|
315
|
+
if (resolvedPackage === 'vercel') {
|
|
316
|
+
await writeStaticVercelOutput(handoff, exportDir);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
139
320
|
// Ensure output root directory exists
|
|
140
321
|
const outputRoot = path.resolve(handoff.workingPath, handoff.sitesDirectory);
|
|
141
322
|
await fs.ensureDir(outputRoot);
|
|
@@ -147,7 +328,363 @@ const buildApp = async (handoff: Handoff, skipComponents?: boolean): Promise<voi
|
|
|
147
328
|
}
|
|
148
329
|
|
|
149
330
|
// Copy the build files into the project output directory
|
|
150
|
-
await fs.copy(
|
|
331
|
+
await fs.copy(exportDir, output);
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Recursively locate the directory containing the standalone `server.js` entry, used as a fallback
|
|
336
|
+
* when the deterministic (tracing-root-relative) location does not resolve — e.g. if Next changes
|
|
337
|
+
* the standalone layout. Returns the first match (breadth-first), or `null` when none is found.
|
|
338
|
+
*/
|
|
339
|
+
const findStandaloneServerDir = (root: string): string | null => {
|
|
340
|
+
if (!fs.existsSync(root)) {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
const queue: string[] = [root];
|
|
344
|
+
while (queue.length > 0) {
|
|
345
|
+
const current = queue.shift()!;
|
|
346
|
+
const entries = fs.readdirSync(current, { withFileTypes: true });
|
|
347
|
+
if (entries.some((entry) => entry.isFile() && entry.name === 'server.js')) {
|
|
348
|
+
return current;
|
|
349
|
+
}
|
|
350
|
+
for (const entry of entries) {
|
|
351
|
+
if (entry.isDirectory() && entry.name !== 'node_modules') {
|
|
352
|
+
queue.push(path.join(current, entry.name));
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return null;
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Write a deployment guide next to the packaged registry app describing how to run it on Vercel,
|
|
361
|
+
* a custom Node server, or in a container, and which env vars it requires at deploy time.
|
|
362
|
+
*/
|
|
363
|
+
const writeRegistryDeploymentReadme = async (
|
|
364
|
+
outputRoot: string,
|
|
365
|
+
entryRelativePath: string,
|
|
366
|
+
databaseUrlEnv: string
|
|
367
|
+
): Promise<void> => {
|
|
368
|
+
const readme = `# Handoff Registry App
|
|
369
|
+
|
|
370
|
+
A self-contained, deployable Next.js **standalone** build produced by
|
|
371
|
+
\`handoff-app build --target registry\`. It serves the docs read API (\`/api/docs/*\`) and the
|
|
372
|
+
registry API from a PostgreSQL database and the published docs read-model artifacts — it never
|
|
373
|
+
builds or depends on local workspace component/pattern source, and never runs a static export.
|
|
374
|
+
|
|
375
|
+
This is the [Next.js standalone output](https://nextjs.org/docs/app/api-reference/config/next-config-js/output):
|
|
376
|
+
\`server.js\` and the trimmed \`node_modules\` it needs sit at this directory's root, with \`public/\`
|
|
377
|
+
and \`.next/static/\` already copied alongside so the server serves them.
|
|
378
|
+
|
|
379
|
+
## Required environment variables (supplied at deploy time, never baked in)
|
|
380
|
+
|
|
381
|
+
- \`${databaseUrlEnv}\` — PostgreSQL/Neon connection string, read at request time.
|
|
382
|
+
- \`HANDOFF_REGISTRY_API_TOKEN\` — bearer token for registry management mutations (if used).
|
|
383
|
+
|
|
384
|
+
## Database migrations
|
|
385
|
+
|
|
386
|
+
Apply migrations from the \`handoff-app\` CLI as a controlled release step (e.g. in your CI/CD
|
|
387
|
+
pipeline) before deploying new code:
|
|
388
|
+
|
|
389
|
+
\`\`\`bash
|
|
390
|
+
${databaseUrlEnv}="postgres://…" handoff-app db:migrate
|
|
391
|
+
\`\`\`
|
|
392
|
+
|
|
393
|
+
\`db:migrate\` reads your project config + DB env vars, resolves the same database adapter the app was
|
|
394
|
+
built with, and applies the package-owned migration set. It runs independently of \`build\` and of
|
|
395
|
+
starting the server, so run it as a release/one-shot job.
|
|
396
|
+
|
|
397
|
+
## Run it (self-hosting)
|
|
398
|
+
|
|
399
|
+
Standalone is the documented artifact for self-hosting on a Node.js server, VPS, Docker, or
|
|
400
|
+
Kubernetes. Start the bundled server from this directory:
|
|
401
|
+
|
|
402
|
+
\`\`\`bash
|
|
403
|
+
${databaseUrlEnv}="postgres://…" node ${entryRelativePath}
|
|
404
|
+
\`\`\`
|
|
405
|
+
|
|
406
|
+
The server listens on \`PORT\` (default \`3000\`) and \`HOSTNAME\` (default \`localhost\`). **In a
|
|
407
|
+
container, set \`HOSTNAME=0.0.0.0\`** so the server is reachable from outside the container:
|
|
408
|
+
|
|
409
|
+
\`\`\`bash
|
|
410
|
+
PORT=3000 HOSTNAME=0.0.0.0 ${databaseUrlEnv}="postgres://…" node ${entryRelativePath}
|
|
411
|
+
\`\`\`
|
|
412
|
+
|
|
413
|
+
### Docker (sketch)
|
|
414
|
+
|
|
415
|
+
\`\`\`dockerfile
|
|
416
|
+
FROM node:20-alpine
|
|
417
|
+
WORKDIR /app
|
|
418
|
+
COPY . .
|
|
419
|
+
ENV NODE_ENV=production HOSTNAME=0.0.0.0 PORT=3000
|
|
420
|
+
EXPOSE 3000
|
|
421
|
+
CMD ["node", "${entryRelativePath}"]
|
|
422
|
+
\`\`\`
|
|
423
|
+
|
|
424
|
+
For multi-instance self-hosting, front \`/_next/static\` with a CDN and configure a shared
|
|
425
|
+
\`cacheHandler\` if you rely on ISR.
|
|
426
|
+
|
|
427
|
+
## Deploying to Vercel
|
|
428
|
+
|
|
429
|
+
Vercel does **not** use \`output: 'standalone'\` — it ignores that setting and builds with its own
|
|
430
|
+
adapter, so this pre-built bundle is **not** the artifact you deploy there. For Vercel, deploy the
|
|
431
|
+
registry app via Vercel's Git/CLI source build (it produces and serves the dynamic app itself) and
|
|
432
|
+
supply the same environment variables above as project env vars. Use this standalone bundle for
|
|
433
|
+
containers, custom Node servers, and other non-Vercel hosts.
|
|
434
|
+
`;
|
|
435
|
+
await fs.writeFile(path.join(outputRoot, 'README.md'), readme);
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Resolve the set of npm packages the packaged registry app must be able to `require`/`import` at
|
|
440
|
+
* runtime, given the configured database adapter. `next`/`react`/`react-dom` run the server and React
|
|
441
|
+
* runtime; `drizzle-orm` backs both the request-time DB client and the migration runner; the driver
|
|
442
|
+
* is adapter-specific (the Neon serverless driver also needs `ws` for its Node WebSocket transport).
|
|
443
|
+
*/
|
|
444
|
+
const getRequiredRegistryRuntimeModules = (handoff: Handoff): string[] => {
|
|
445
|
+
const base = ['next', 'react', 'react-dom', 'drizzle-orm'];
|
|
446
|
+
const adapter = resolveRegistryAdapter(handoff.config);
|
|
447
|
+
const driver = adapter === 'neon' ? ['@neondatabase/serverless', 'ws'] : ['pg'];
|
|
448
|
+
// Asset-storage SDKs the deployed registry must be able to load at request time. The pre-packaged
|
|
449
|
+
// Vercel Blob adapter needs `@vercel/blob`; a custom adapter may declare its installed SDK
|
|
450
|
+
// package(s) via `assetStorage.options.sdkModules` so the trace/assertion covers them.
|
|
451
|
+
const assetStorage = resolveAssetStorageFromConfig(handoff.config);
|
|
452
|
+
const storage: string[] = [];
|
|
453
|
+
if (assetStorage.adapterKind === 'vercel-blob') {
|
|
454
|
+
storage.push('@vercel/blob');
|
|
455
|
+
}
|
|
456
|
+
const sdkModules = assetStorage.options?.sdkModules;
|
|
457
|
+
if (Array.isArray(sdkModules)) {
|
|
458
|
+
storage.push(...sdkModules.filter((mod): mod is string => typeof mod === 'string'));
|
|
459
|
+
}
|
|
460
|
+
return [...base, ...driver, ...storage];
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Build-time guard against the empty-`node_modules` regression (the artifact would otherwise boot-fail
|
|
465
|
+
* with `Cannot find module 'next'` only on a real, isolated deploy). After assembly, assert every
|
|
466
|
+
* required runtime module resolves under `<entryDir>/node_modules`. `fs.existsSync` follows symlinks,
|
|
467
|
+
* so this also catches a dangling pnpm symlink (the hoisted link traced without its `.pnpm` target),
|
|
468
|
+
* not just an absent directory.
|
|
469
|
+
*
|
|
470
|
+
* `handoff-app` itself is intentionally not asserted: the app inlines it via the `@handoff` alias and
|
|
471
|
+
* `transpilePackages`, so it is not required by its bare specifier at runtime and may be absent.
|
|
472
|
+
*/
|
|
473
|
+
const assertRegistryRuntimeDepsTraced = (handoff: Handoff, entryDir: string): void => {
|
|
474
|
+
const required = getRequiredRegistryRuntimeModules(handoff);
|
|
475
|
+
const missing = required.filter((mod) => !fs.existsSync(path.join(entryDir, 'node_modules', ...mod.split('/'), 'package.json')));
|
|
476
|
+
if (missing.length === 0) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
throw new HandoffBuildError(
|
|
480
|
+
`The packaged registry app is missing required runtime dependencies in its bundled node_modules: ` +
|
|
481
|
+
`${missing.join(', ')}. The Next file trace did not capture them, so the artifact would fail at ` +
|
|
482
|
+
`startup with "Cannot find module '${missing[0]}'".\n` +
|
|
483
|
+
`This usually means the runtime dependencies are installed *above* the build's tracing root ` +
|
|
484
|
+
`(\`outputFileTracingRoot\`, currently the project root "${handoff.workingPath}"). In a ` +
|
|
485
|
+
`workspace/monorepo the package manager may hoist them to the monorepo root; the tracing root ` +
|
|
486
|
+
`must be the nearest ancestor of *both* the staged app and the resolved node_modules. Ensure ` +
|
|
487
|
+
`${missing.join(', ')} are installed and resolvable from "${handoff.workingPath}".`
|
|
488
|
+
);
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Lay the Next `standalone` build out as the documented standalone layout under `entryDir`: copy the
|
|
493
|
+
* traced bundle, flatten the package-rooted server entry up to `entryDir`, and copy the untraced
|
|
494
|
+
* static assets + public tree + resolved runtime config beside the server entry. Shared by both
|
|
495
|
+
* registry deliverables — the `standalone` package emits this as `out/registry`, and the `vercel`
|
|
496
|
+
* package emits it inside the Build Output API function directory.
|
|
497
|
+
*
|
|
498
|
+
* @returns the absolute path of the server entry (`<entryDir>/server.js`).
|
|
499
|
+
*/
|
|
500
|
+
const assembleRegistryStandalone = async (
|
|
501
|
+
handoff: Handoff,
|
|
502
|
+
appPath: string,
|
|
503
|
+
standaloneRoot: string,
|
|
504
|
+
entryDir: string
|
|
505
|
+
): Promise<string> => {
|
|
506
|
+
await fs.copy(standaloneRoot, entryDir, { overwrite: true });
|
|
507
|
+
|
|
508
|
+
// Locate the server entry within the assembled package. Standalone tracing is rooted at the
|
|
509
|
+
// consumer project root (`workingPath`, see next.config.mjs), so every traced file is laid out at
|
|
510
|
+
// its path *relative to `workingPath`*. The staged app lives at `<workingPath>/node_modules/
|
|
511
|
+
// handoff-app/.handoff/<projectId>`, so the server entry lands nested at that same relative path
|
|
512
|
+
// *underneath* the traced `node_modules`. Resolve it deterministically; the recursive fallback
|
|
513
|
+
// skips `node_modules` (so it cannot find this nested entry) and only covers a hypothetical
|
|
514
|
+
// non-nested layout.
|
|
515
|
+
const relAppDir = path.relative(handoff.workingPath, appPath);
|
|
516
|
+
let serverDir = path.resolve(entryDir, relAppDir);
|
|
517
|
+
if (!fs.existsSync(path.join(serverDir, 'server.js'))) {
|
|
518
|
+
const located = findStandaloneServerDir(entryDir);
|
|
519
|
+
if (!located) {
|
|
520
|
+
throw new Error(
|
|
521
|
+
`Could not locate the standalone "server.js" in the packaged registry app at "${entryDir}". ` +
|
|
522
|
+
'The Next standalone layout may have changed.'
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
serverDir = located;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// The project id (and the `.handoff` staging nesting) are irrelevant to the deployable artifact, so
|
|
529
|
+
// flatten the server entry up to `entryDir` — the traced `node_modules` already live there.
|
|
530
|
+
if (path.resolve(serverDir) !== path.resolve(entryDir)) {
|
|
531
|
+
for (const entry of fs.readdirSync(serverDir)) {
|
|
532
|
+
await fs.move(path.join(serverDir, entry), path.join(entryDir, entry), { overwrite: true });
|
|
533
|
+
}
|
|
534
|
+
// Drop the now-empty staging nesting. The entry nests under `node_modules/handoff-app/.handoff`,
|
|
535
|
+
// so we must NOT remove the first path segment (`node_modules`) — that would delete every traced
|
|
536
|
+
// dependency. Remove only the `.handoff` staging container (the parent of the staged app dir),
|
|
537
|
+
// leaving `node_modules/` and the real `node_modules/handoff-app/` package intact.
|
|
538
|
+
const stagingContainer = path.resolve(entryDir, path.relative(handoff.workingPath, path.dirname(appPath)));
|
|
539
|
+
if (path.basename(stagingContainer) === '.handoff' && fs.existsSync(stagingContainer)) {
|
|
540
|
+
await fs.remove(stagingContainer);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Static assets and the app's public tree are not traced into standalone — copy them next to the
|
|
545
|
+
// server entry where the Next standalone server expects them.
|
|
546
|
+
const staticSrc = path.resolve(appPath, '.next', 'static');
|
|
547
|
+
if (fs.existsSync(staticSrc)) {
|
|
548
|
+
await fs.copy(staticSrc, path.join(entryDir, '.next', 'static'), { overwrite: true });
|
|
549
|
+
}
|
|
550
|
+
const publicSrc = path.resolve(appPath, 'public');
|
|
551
|
+
if (fs.existsSync(publicSrc)) {
|
|
552
|
+
await fs.copy(publicSrc, path.join(entryDir, 'public'), { overwrite: true });
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Ship the resolved runtime config beside the entry so the deployed server (cwd = entry dir) can
|
|
556
|
+
// read the full client config; runtime mode + DB env-var name are also baked into the bundle.
|
|
557
|
+
for (const configFile of ['client.config.json', 'runtime.server.json']) {
|
|
558
|
+
const src = path.resolve(appPath, configFile);
|
|
559
|
+
if (fs.existsSync(src)) {
|
|
560
|
+
await fs.copy(src, path.join(entryDir, configFile), { overwrite: true });
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Fail loudly at build time if the trace did not capture the runtime deps, rather than shipping a
|
|
565
|
+
// broken artifact that only crashes on an isolated deploy. Covers both deliverables (this is the
|
|
566
|
+
// shared assembly step for `standalone` and `vercel`).
|
|
567
|
+
assertRegistryRuntimeDepsTraced(handoff, entryDir);
|
|
568
|
+
|
|
569
|
+
return path.join(entryDir, 'server.js');
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Package the deployable dynamic registry app.
|
|
574
|
+
*
|
|
575
|
+
* Unlike the static target this never builds or bundles workspace component/pattern source and the
|
|
576
|
+
* resulting artifact has no workspace-source runtime dependency — it ships the app/runtime needed to
|
|
577
|
+
* serve the docs read API and registry API from the database and published docs read-model
|
|
578
|
+
* artifacts. Next always runs a `standalone` build (no static export); the divergence is the
|
|
579
|
+
* final-assembly step driven by `buildPackage`:
|
|
580
|
+
* - `standalone` (default) → the documented Next.js standalone layout under the sites output
|
|
581
|
+
* directory (`<project>/<sitesOutputDirectory>/registry`, default `out/registry`) — a
|
|
582
|
+
* self-hosting artifact for containers, custom Node servers, and VPS, started with
|
|
583
|
+
* `node server.js`.
|
|
584
|
+
* - `vercel` → the Vercel Build Output API directory (`.vercel/output`) at the repo root: the
|
|
585
|
+
* traced bundle wrapped as a Node function plus CDN static assets and a route table.
|
|
586
|
+
*
|
|
587
|
+
* Mode is forced to `registry`; the database adapter is honored from config (single-sourced so build
|
|
588
|
+
* and `db:migrate` never diverge). No connection-string value is ever baked — only env-var *names*.
|
|
589
|
+
*/
|
|
590
|
+
const buildRegistryApp = async (handoff: Handoff, buildPackage: BuildPackage = 'standalone'): Promise<void> => {
|
|
591
|
+
// The registry build target *defines* a registry deployment, so the packaged artifact always runs
|
|
592
|
+
// in registry mode regardless of the source project's `runtime.mode`. Mode stays config-only at
|
|
593
|
+
// runtime — it is fixed here by the build target, never inferred from the deploy environment — so
|
|
594
|
+
// a workspace project can produce a registry app without flipping its own runtime mode.
|
|
595
|
+
const sourceMode = handoff.config?.runtime?.mode ?? 'workspace';
|
|
596
|
+
if (sourceMode !== 'registry') {
|
|
597
|
+
Logger.info(`Source runtime.mode is "${sourceMode}"; forcing "registry" mode in the packaged registry app.`);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// The adapter stays single-sourced from config (never flipped by the package flag), so build and
|
|
601
|
+
// `db:migrate` always resolve the same one. `pg` uses long-lived TCP connections that pool poorly on
|
|
602
|
+
// serverless; warn (non-fatally) so the operator points DATABASE_URL at a pooled endpoint or selects
|
|
603
|
+
// the Neon adapter. The connection-string value is not available at build time, so this cannot be
|
|
604
|
+
// validated — it is guidance only and never fails the build.
|
|
605
|
+
if (buildPackage === 'vercel' && resolveRegistryAdapter(handoff.config) === 'pg') {
|
|
606
|
+
Logger.warn(
|
|
607
|
+
'Registry Vercel build uses the "pg" adapter (standard TCP connections), which pools poorly on ' +
|
|
608
|
+
'serverless and can exhaust the database connection limit. Point DATABASE_URL at a pooled endpoint ' +
|
|
609
|
+
'(PgBouncer / transaction pooling), or set runtime.registry.database.adapter: "neon" for a ' +
|
|
610
|
+
'serverless-native HTTP/WebSocket driver. The build will continue.'
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Stage the app without any workspace-derived artifacts — the registry serves from the database.
|
|
615
|
+
await cleanupAppDirectory(handoff);
|
|
616
|
+
const appPath = await initializeProjectApp(handoff, {
|
|
617
|
+
includeWorkspaceArtifacts: false,
|
|
618
|
+
runtimeModeOverride: 'registry',
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
// Persist the resolved client/server runtime config so the deployed app reports registry mode and
|
|
622
|
+
// resolves its DB env-var name (the connection string itself is read from that env var at runtime).
|
|
623
|
+
await persistClientConfig(handoff, { runtimeModeOverride: 'registry' });
|
|
624
|
+
|
|
625
|
+
// Remove prior registry outputs *before* tracing. Both deliverables (`out/registry` and
|
|
626
|
+
// `.vercel/output`) live inside the trace root (the project root), and after this fix each contains
|
|
627
|
+
// a full bundled `node_modules`. A stale prior output left in place could be swept into the new
|
|
628
|
+
// trace (bundle bloat / recursive nesting), so clear both up front. The per-deliverable cleanup in
|
|
629
|
+
// the assembly step remains.
|
|
630
|
+
await fs.remove(getRegistryBuildOutputPath(handoff));
|
|
631
|
+
await fs.remove(getVercelOutputPath(handoff));
|
|
632
|
+
|
|
633
|
+
// Build the dynamic app. `output: 'standalone'` (driven by HANDOFF_BUILD_TARGET) traces the runtime
|
|
634
|
+
// and the selected Postgres/Neon driver into a self-contained bundle — never a static export.
|
|
635
|
+
const buildResult = spawn.sync('npx', ['next', 'build'], {
|
|
636
|
+
cwd: appPath,
|
|
637
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
638
|
+
env: {
|
|
639
|
+
...process.env,
|
|
640
|
+
NODE_ENV: 'production',
|
|
641
|
+
HANDOFF_BUILD_TARGET: 'registry',
|
|
642
|
+
},
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
Logger.childProcessBuffer(buildResult.stdout);
|
|
646
|
+
Logger.childProcessBuffer(buildResult.stderr);
|
|
647
|
+
|
|
648
|
+
if (buildResult.status !== 0) {
|
|
649
|
+
let errorMsg = `Registry app build failed with exit code ${buildResult.status}`;
|
|
650
|
+
if (buildResult.error) {
|
|
651
|
+
errorMsg += `\nSpawn error: ${buildResult.error.message}`;
|
|
652
|
+
}
|
|
653
|
+
throw new Error(errorMsg);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Assemble the deployable package. Standalone tracing is rooted at the package (modulePath), so the
|
|
657
|
+
// app's traced files land under `<relAppDir>` inside the standalone tree; static assets and public
|
|
658
|
+
// files are not traced and must be copied alongside the server entry.
|
|
659
|
+
const standaloneRoot = path.resolve(appPath, '.next', 'standalone');
|
|
660
|
+
if (!fs.existsSync(standaloneRoot)) {
|
|
661
|
+
throw new Error(
|
|
662
|
+
`Registry build did not produce a standalone bundle at "${standaloneRoot}". Ensure the registry ` +
|
|
663
|
+
'build target sets Next `output: "standalone"`.'
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Final assembly branches on the resolved package. Both lay out
|
|
668
|
+
// the same traced standalone bundle; they differ only in where it lands and how it is wrapped.
|
|
669
|
+
if (buildPackage === 'vercel') {
|
|
670
|
+
await writeRegistryVercelOutput(handoff, { appPath, standaloneRoot, assembleStandalone: assembleRegistryStandalone });
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Standalone bundle (default): the documented self-hosting layout under the sites output directory.
|
|
675
|
+
const output = getRegistryBuildOutputPath(handoff);
|
|
676
|
+
await fs.remove(output);
|
|
677
|
+
await fs.ensureDir(output);
|
|
678
|
+
const serverEntry = await assembleRegistryStandalone(handoff, appPath, standaloneRoot, output);
|
|
679
|
+
|
|
680
|
+
// Migrations are intentionally not bundled into the artifact: they are applied from the
|
|
681
|
+
// `handoff-app` source/CLI (`handoff-app db:migrate`) as a controlled release step (e.g. CI/CD),
|
|
682
|
+
// never from the deployed bundle. See the generated README for the migrate-then-deploy flow.
|
|
683
|
+
|
|
684
|
+
const entryRelativePath = path.relative(output, serverEntry).split(path.sep).join('/');
|
|
685
|
+
await writeRegistryDeploymentReadme(output, entryRelativePath, resolveDatabaseUrlEnv(handoff.config));
|
|
686
|
+
|
|
687
|
+
Logger.success(`Packaged registry app at ${output} (start: \`node ${entryRelativePath}\`, migrate: \`handoff-app db:migrate\`).`);
|
|
151
688
|
};
|
|
152
689
|
|
|
153
690
|
/**
|
|
@@ -155,11 +692,12 @@ const buildApp = async (handoff: Handoff, skipComponents?: boolean): Promise<voi
|
|
|
155
692
|
* Starts a custom dev server with Handoff-specific watchers and hot-reloading.
|
|
156
693
|
*/
|
|
157
694
|
export const watchApp = async (handoff: Handoff): Promise<void> => {
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
|
|
695
|
+
// Build the shared/global artifacts first so component/pattern preview HTML references them only
|
|
696
|
+
// when present, then process components with caching enabled (which skips
|
|
697
|
+
// rebuilding components whose source files haven't changed).
|
|
161
698
|
await buildMainJS(handoff);
|
|
162
699
|
await buildMainCss(handoff);
|
|
700
|
+
await processComponents(handoff, undefined, undefined, { useCache: true });
|
|
163
701
|
|
|
164
702
|
// Build patterns after components are ready
|
|
165
703
|
await buildPatterns(handoff);
|
|
@@ -190,6 +728,8 @@ export const watchApp = async (handoff: Handoff): Promise<void> => {
|
|
|
190
728
|
env: {
|
|
191
729
|
...process.env,
|
|
192
730
|
NODE_ENV: 'development',
|
|
731
|
+
// Workspace dev is never a static export, even if a static target leaked into the environment.
|
|
732
|
+
HANDOFF_BUILD_TARGET: '',
|
|
193
733
|
},
|
|
194
734
|
});
|
|
195
735
|
Logger.pipeChildStreams(nextProcess.stdout, nextProcess.stderr);
|
|
@@ -224,12 +764,14 @@ export const watchApp = async (handoff: Handoff): Promise<void> => {
|
|
|
224
764
|
runtimeComponentsWatcher: null,
|
|
225
765
|
runtimeConfigurationWatcher: null,
|
|
226
766
|
componentDirectoriesWatcher: null,
|
|
767
|
+
patternDirectoriesWatcher: null,
|
|
227
768
|
};
|
|
228
769
|
|
|
229
770
|
watchPublicDirectory(handoff, wss, state, chokidarConfig);
|
|
230
771
|
watchRuntimeComponents(handoff, state, getRuntimeComponentsPathsToWatch(handoff));
|
|
231
772
|
watchRuntimeConfiguration(handoff, state);
|
|
232
773
|
watchComponentDirectories(handoff, state, chokidarConfig);
|
|
774
|
+
watchPatternDirectories(handoff, state, chokidarConfig);
|
|
233
775
|
watchGlobalEntries(handoff, state, chokidarConfig);
|
|
234
776
|
watchPages(handoff, chokidarConfig);
|
|
235
777
|
};
|
|
@@ -260,6 +802,8 @@ export const devApp = async (handoff: Handoff): Promise<void> => {
|
|
|
260
802
|
env: {
|
|
261
803
|
...process.env,
|
|
262
804
|
NODE_ENV: 'development',
|
|
805
|
+
// Workspace dev is never a static export, even if a static target leaked into the environment.
|
|
806
|
+
HANDOFF_BUILD_TARGET: '',
|
|
263
807
|
},
|
|
264
808
|
});
|
|
265
809
|
|