handoff-app 2.0.0-8 → 2.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 → CHANGELOG.md} +85 -1
- package/{License.md → LICENSE.md} +1 -1
- package/README.md +816 -393
- package/RECIPES.md +975 -0
- package/UPGRADE.md +57 -9
- package/components.json +20 -20
- package/config/docs/foundations/colors/guidelines.md +0 -1
- package/config/docs/foundations/colors.md +0 -1
- package/config/docs/foundations/effects/guidelines.md +0 -1
- package/config/docs/foundations/effects/resources.md +0 -1
- package/config/docs/foundations/effects.md +0 -1
- package/config/docs/foundations/fonts.md +0 -1
- package/config/docs/foundations/grid.md +0 -1
- package/config/docs/foundations/icon.md +0 -1
- package/config/docs/foundations/icons/guidelines.md +0 -1
- package/config/docs/foundations/icons.md +0 -1
- package/config/docs/foundations/images/guidelines.md +0 -1
- package/config/docs/foundations/images/resources.md +0 -1
- package/config/docs/foundations/images.md +0 -1
- package/config/docs/foundations/logo/resources.md +0 -1
- package/config/docs/foundations/logo.md +0 -1
- package/config/docs/foundations/typography/guidelines.md +0 -1
- package/config/docs/foundations/typography/resources.md +0 -1
- package/config/docs/foundations/typography.md +0 -1
- package/config/docs/foundations.md +0 -1
- package/config/docs/guidelines.md +0 -1
- package/config/docs/index.md +0 -1
- package/config/docs/system/tokens/foundations/colors.md +0 -4
- package/config/docs/system/tokens/foundations/effects.md +0 -4
- package/config/docs/system/tokens/foundations/typography.md +0 -2
- package/config/docs/system.md +0 -2
- package/config/templates/page.md +2 -1
- package/config/templates/page.mdx +2 -1
- package/dist/ai/connections.d.ts +66 -0
- package/dist/ai/connections.js +149 -0
- package/dist/ai/define.d.ts +10 -0
- package/dist/ai/define.js +13 -0
- package/dist/ai/types.d.ts +26 -0
- package/dist/ai/types.js +10 -0
- package/dist/app-builder/build.d.ts +14 -7
- package/dist/app-builder/build.js +195 -66
- package/dist/app-builder/client-config.js +3 -2
- package/dist/app-builder/paths.d.ts +0 -12
- package/dist/app-builder/paths.js +3 -20
- package/dist/app-builder/static-export.js +1 -1
- package/dist/app-builder/vercel-output.d.ts +1 -1
- package/dist/app-builder/vercel-output.js +5 -4
- package/dist/cache/build-cache.d.ts +27 -4
- package/dist/cache/build-cache.js +114 -47
- package/dist/cache/build-inputs.d.ts +13 -0
- package/dist/cache/build-inputs.js +19 -0
- package/dist/cache/index.d.ts +2 -1
- package/dist/cache/index.js +5 -1
- package/dist/catalog/normalize.js +3 -0
- package/dist/catalog/previews.js +5 -4
- package/dist/catalog/types.d.ts +8 -0
- package/dist/cli/create.js +1 -2
- package/dist/commands/build/index.d.ts +1 -1
- package/dist/commands/build/index.js +1 -1
- package/dist/commands/utils.d.ts +3 -3
- package/dist/config/defaults.js +7 -4
- package/dist/config/from-env.d.ts +4 -0
- package/dist/config/from-env.js +38 -1
- package/dist/config/helpers.js +2 -2
- package/dist/config/index.d.ts +0 -1
- package/dist/config/index.js +1 -4
- package/dist/config/loader.js +28 -2
- package/dist/config/resolve-env.d.ts +2 -1
- package/dist/config/resolve-env.js +28 -12
- package/dist/config/runtime.js +9 -11
- package/dist/handlebars.d.ts +10 -2
- package/dist/index.d.ts +12 -7
- package/dist/index.js +18 -22
- package/dist/nav/index.d.ts +2 -2
- package/dist/nav/registry.d.ts +3 -3
- package/dist/nav/registry.js +1 -1
- package/dist/nav/workspace.d.ts +3 -3
- package/dist/nav/workspace.js +1 -1
- package/dist/pipeline/figma.d.ts +13 -2
- package/dist/pipeline/figma.js +86 -46
- package/dist/pipeline/styles.js +1 -1
- package/dist/pipeline/validation.d.ts +1 -1
- package/dist/pipeline/validation.js +3 -3
- package/dist/providers.d.ts +10 -0
- package/dist/providers.js +13 -0
- package/dist/registry/asset-storage/define.d.ts +5 -5
- package/dist/registry/asset-storage/define.js +4 -4
- package/dist/registry/asset-storage/resolve.d.ts +9 -19
- package/dist/registry/asset-storage/resolve.js +33 -21
- package/dist/registry/asset-storage/types.d.ts +37 -17
- package/dist/registry/asset-storage/types.js +5 -5
- package/dist/registry/auth/ai-keys.d.ts +32 -0
- package/dist/registry/auth/ai-keys.js +69 -0
- package/dist/registry/auth/crypto.d.ts +17 -0
- package/dist/registry/auth/crypto.js +61 -1
- package/dist/registry/auth/index.d.ts +2 -1
- package/dist/registry/auth/index.js +11 -2
- package/dist/registry/checkout/index.d.ts +2 -1
- package/dist/registry/checkout/index.js +57 -127
- package/dist/registry/client.js +53 -17
- package/dist/registry/db/driver.js +3 -3
- package/dist/registry/db/schema.d.ts +261 -3
- package/dist/registry/db/schema.js +29 -4
- package/dist/registry/email/define.d.ts +10 -0
- package/dist/registry/email/define.js +13 -0
- package/dist/registry/email/providers/smtp.d.ts +10 -0
- package/dist/registry/email/providers/smtp.js +73 -0
- package/dist/registry/email/resolve.d.ts +28 -0
- package/dist/registry/email/resolve.js +74 -0
- package/dist/registry/email/types.d.ts +24 -0
- package/dist/registry/email/types.js +6 -0
- package/dist/registry/path.d.ts +2 -0
- package/dist/registry/path.js +11 -1
- package/dist/registry/publish/package.js +8 -0
- package/dist/registry/transfer.d.ts +4 -0
- package/dist/store/registry.d.ts +3 -3
- package/dist/store/registry.js +4 -4
- package/dist/transformers/plugins/ssr-render.js +40 -15
- package/dist/transformers/preview/component/builder.d.ts +1 -9
- package/dist/transformers/preview/component/builder.js +17 -17
- package/dist/transformers/preview/component/css.d.ts +3 -1
- package/dist/transformers/preview/component/css.js +92 -51
- package/dist/transformers/preview/component/html.d.ts +3 -1
- package/dist/transformers/preview/component/html.js +6 -2
- package/dist/transformers/preview/component/javascript.d.ts +2 -1
- package/dist/transformers/preview/component/javascript.js +10 -4
- package/dist/transformers/preview/pattern/html.js +3 -1
- package/dist/transformers/preview/types.d.ts +13 -0
- package/dist/transformers/utils/build.d.ts +24 -0
- package/dist/transformers/utils/build.js +100 -1
- package/dist/transformers/utils/module.js +2 -1
- package/dist/types/config.d.ts +135 -18
- package/dist/utils/path.d.ts +1 -5
- package/dist/utils/path.js +6 -23
- package/docs/api.md +6 -4
- package/docs/cli.md +275 -54
- package/drizzle/0007_add_ai_keys.sql +11 -0
- package/drizzle/0008_add_source_dir.sql +2 -0
- package/drizzle/meta/0007_snapshot.json +2013 -0
- package/drizzle/meta/0008_snapshot.json +2025 -0
- package/drizzle/meta/_journal.json +14 -0
- package/package.json +19 -8
- package/src/ai/README.md +42 -0
- package/src/ai/connections.ts +189 -0
- package/src/ai/define.ts +15 -0
- package/src/ai/types.ts +30 -0
- package/src/app/components/AiAssistant/AiAssistantPanel.tsx +144 -0
- package/src/app/components/AiAssistant/AiAssistantProvider.tsx +215 -0
- package/src/app/components/AiAssistant/AiConversation.tsx +737 -0
- package/src/app/components/AiAssistant/AiToolResult.tsx +99 -0
- package/src/app/components/AiAssistant/dockState.ts +86 -0
- package/src/app/components/AiAssistant/toolLabel.ts +24 -0
- package/src/app/components/AiAssistant/useAiChat.ts +147 -0
- package/src/app/components/AiAssistant/useAiConnections.ts +86 -0
- package/src/app/components/AiAssistant/usePageContext.ts +63 -0
- package/src/app/components/Auth/AccountLayout.tsx +58 -19
- package/src/app/components/Auth/AuthShell.tsx +27 -33
- package/src/app/components/Auth/RegistryInstallGate.tsx +4 -8
- package/src/app/components/Auth/installStatus.ts +29 -0
- package/src/app/components/Component/PageSliceResolver.tsx +1 -0
- package/src/app/components/Component/Preview.tsx +383 -189
- package/src/app/components/Component/slices/ComponentDisplaySlice.tsx +4 -0
- package/src/app/components/Foundations/ColorGrid.tsx +13 -14
- package/src/app/components/Layout/Header.tsx +21 -15
- package/src/app/components/Layout/Main.tsx +2 -2
- package/src/app/components/Markdown/MarkdownComponents.tsx +30 -32
- package/src/app/components/McpIntegration/McpConfigDialog.tsx +12 -51
- package/src/app/components/Navigation/MobileNav.tsx +2 -2
- package/src/app/components/Navigation/SideNav.tsx +1 -15
- package/src/app/components/context/NavProvider.tsx +7 -3
- package/src/app/components/ui/attachment.tsx +50 -0
- package/src/app/components/ui/bubble.tsx +57 -0
- package/src/app/components/ui/button.tsx +3 -1
- package/src/app/components/ui/confirm-dialog.tsx +87 -0
- package/src/app/components/ui/dialog.tsx +7 -2
- package/src/app/components/ui/marker.tsx +56 -0
- package/src/app/components/ui/sidebar.tsx +3 -1
- package/src/app/components/util/index.ts +3 -12
- package/src/app/css/index.css +64 -0
- package/src/app/lib/ai/auth.ts +33 -0
- package/src/app/lib/ai/context.ts +99 -0
- package/src/app/lib/ai/model.ts +79 -0
- package/src/app/lib/ai/prompt.ts +36 -0
- package/src/app/lib/ai/resolve.ts +97 -0
- package/src/app/lib/ai/tools.ts +120 -0
- package/src/app/lib/api/methods.ts +13 -0
- package/src/app/lib/asset-storage.ts +35 -36
- package/src/app/lib/auth/api.ts +4 -12
- package/src/app/lib/auth/email.ts +96 -23
- package/src/app/lib/docs-api/page-rendering.ts +1 -1
- package/src/app/lib/docs-api/registry.ts +2 -3
- package/src/app/lib/docs-api/runtime-config.ts +47 -21
- package/src/app/lib/mcp/components.ts +523 -0
- package/src/app/lib/mcp/page-url.ts +24 -0
- package/src/app/lib/mcp/pages.ts +84 -0
- package/src/app/lib/mcp/result.ts +33 -0
- package/src/app/lib/mcp/server.ts +39 -318
- package/src/app/lib/mcp/tokens.ts +334 -0
- package/src/app/lib/mcp/tool.ts +19 -0
- package/src/app/lib/registry-api/asset-transfer.ts +119 -11
- package/src/app/lib/registry-api/errors.ts +2 -0
- package/src/app/lib/registry-api/handler.ts +7 -1
- package/src/app/lib/registry-api/transfer.ts +28 -6
- package/src/app/lib/server-module.ts +34 -0
- package/src/app/lib/utils.ts +20 -0
- package/src/app/next.config.mjs +26 -24
- package/src/app/pages/[...slug]/index.tsx +2 -1
- package/src/app/pages/_app.tsx +6 -2
- package/src/app/pages/_document.tsx +6 -0
- package/src/app/pages/account/ai.tsx +183 -0
- package/src/app/pages/account/index.tsx +68 -68
- package/src/app/pages/account/tokens.tsx +37 -7
- package/src/app/pages/account/users.tsx +75 -15
- package/src/app/pages/api/account/ai/keys/[connectionId].ts +60 -0
- package/src/app/pages/api/account/ai/keys/index.ts +20 -0
- package/src/app/pages/api/admin/users/[id]/[action].ts +80 -11
- package/src/app/pages/api/admin/users/index.ts +2 -2
- package/src/app/pages/api/ai/chat.ts +115 -0
- package/src/app/pages/api/ai/connections.ts +23 -0
- package/src/app/pages/api/auth/request-reset.ts +14 -16
- package/src/app/pages/api/docs/nav.json.ts +1 -1
- package/src/app/pages/api/install.ts +3 -3
- package/src/app/pages/api/mcp.ts +6 -0
- package/src/app/pages/api/registry/transfer/assets/blobs/[hash]/complete.ts +11 -0
- package/src/app/pages/api/registry/transfer/assets/blobs/[hash]/upload.ts +12 -0
- package/src/app/pages/api/registry/transfer/assets/blobs/[hash].ts +1 -1
- package/src/app/pages/cli/device.tsx +10 -5
- package/src/app/pages/foundations/colors.tsx +9 -8
- package/src/app/pages/foundations/index.tsx +3 -3
- package/src/app/pages/index.tsx +2 -2
- package/src/app/pages/install.tsx +7 -2
- package/src/app/pages/login.tsx +32 -27
- package/src/app/pages/reset-password.tsx +21 -20
- package/src/app/pages/system/component/[component]/index.tsx +2 -5
- package/src/app/pages/system/index.tsx +11 -11
- package/src/app/pages/system/pattern/[pattern]/index.tsx +2 -2
- package/src/app/pages/system/tokens/components/[component]/index.tsx +0 -4
- package/src/app-builder/README.md +2 -1
- package/src/app-builder/build.ts +218 -73
- package/src/app-builder/client-config.ts +3 -2
- package/src/app-builder/paths.ts +3 -22
- package/src/app-builder/static-export.ts +1 -1
- package/src/app-builder/vercel-output.ts +5 -4
- package/src/cache/README.md +4 -1
- package/src/cache/build-cache.ts +128 -43
- package/src/cache/build-inputs.ts +28 -0
- package/src/cache/index.ts +4 -0
- package/src/catalog/README.md +3 -0
- package/src/catalog/normalize.ts +2 -0
- package/src/catalog/previews.ts +6 -3
- package/src/catalog/types.ts +8 -0
- package/src/cli/create.ts +1 -2
- package/src/commands/build/index.ts +1 -1
- package/src/config/README.md +10 -9
- package/src/config/defaults.ts +7 -4
- package/src/config/from-env.ts +33 -0
- package/src/config/helpers.ts +1 -3
- package/src/config/index.ts +0 -3
- package/src/config/loader.ts +27 -3
- package/src/config/resolve-env.ts +28 -15
- package/src/config/runtime.ts +10 -5
- package/src/handlebars.ts +8 -2
- package/src/index.ts +33 -29
- package/src/nav/index.ts +2 -2
- package/src/nav/registry.ts +3 -3
- package/src/nav/workspace.ts +3 -3
- package/src/pipeline/README.md +1 -1
- package/src/pipeline/figma.ts +98 -49
- package/src/pipeline/styles.ts +1 -1
- package/src/pipeline/validation.ts +3 -3
- package/src/providers.ts +18 -0
- package/src/registry/asset-storage/define.ts +6 -6
- package/src/registry/asset-storage/resolve.ts +38 -37
- package/src/registry/asset-storage/types.ts +39 -17
- package/src/registry/auth/ai-keys.ts +62 -0
- package/src/registry/auth/crypto.ts +63 -1
- package/src/registry/auth/index.ts +11 -1
- package/src/registry/checkout/index.ts +60 -138
- package/src/registry/client.ts +60 -20
- package/src/registry/db/driver.ts +2 -2
- package/src/registry/db/schema.ts +49 -22
- package/src/registry/email/define.ts +15 -0
- package/src/registry/email/providers/smtp.ts +39 -0
- package/src/registry/email/resolve.ts +94 -0
- package/src/registry/email/types.ts +28 -0
- package/src/registry/path.ts +11 -0
- package/src/registry/publish/package.ts +9 -0
- package/src/registry/transfer.ts +4 -0
- package/src/store/registry.ts +7 -7
- package/src/transformers/plugins/ssr-render.ts +53 -16
- package/src/transformers/preview/component/builder.ts +19 -25
- package/src/transformers/preview/component/css.ts +101 -53
- package/src/transformers/preview/component/html.ts +6 -2
- package/src/transformers/preview/component/javascript.ts +14 -4
- package/src/transformers/preview/pattern/html.ts +3 -1
- package/src/transformers/preview/types.ts +14 -0
- package/src/transformers/utils/build.ts +93 -0
- package/src/transformers/utils/module.ts +4 -2
- package/src/types/config.ts +148 -20
- package/src/utils/README.md +1 -1
- package/src/utils/path.ts +6 -29
- package/templates/default/handoff.config.js.tpl +10 -2
- package/templates/default/handoff.config.ts.tpl +10 -2
- package/templates/default/handoff.config.with-examples.js.tpl +10 -2
- package/templates/default/handoff.config.with-examples.ts.tpl +10 -2
- package/templates/default/package.json.tpl +2 -2
- package/.editorconfig +0 -19
- package/.eslintrc.json +0 -4
- package/.prettierignore +0 -0
- package/.prettierrc.json +0 -8
- package/.vscode/settings.json +0 -17
- package/dist/config/validator.d.ts +0 -9
- package/dist/config/validator.js +0 -24
- package/dist/registry/asset-storage/adapters/vercel-blob.d.ts +0 -17
- package/dist/registry/asset-storage/adapters/vercel-blob.js +0 -87
- package/src/app/components/old/Header.tsx +0 -89
- package/src/app/lib/mcp/shape.ts +0 -596
- package/src/app/pages/assets/fonts.tsx +0 -129
- package/src/app/pages/assets/index.tsx +0 -91
- package/src/app/pages/assets/logos.tsx +0 -75
- package/src/config/validator.ts +0 -22
- package/src/registry/asset-storage/adapters/vercel-blob.ts +0 -51
package/src/app-builder/build.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import * as p from '@clack/prompts';
|
|
2
|
+
import { nodeFileTrace } from '@vercel/nft';
|
|
2
3
|
import spawn from 'cross-spawn';
|
|
3
4
|
import fs from 'fs-extra';
|
|
4
5
|
import path from 'path';
|
|
5
6
|
import Handoff from '..';
|
|
7
|
+
import { resolveAiFromConfig, type AiSettings } from '../ai/connections';
|
|
6
8
|
import { isMcpEnabled } from '../config';
|
|
9
|
+
import { envReferenceNames } from '../config/from-env';
|
|
7
10
|
import { buildComponents } from '../pipeline/components';
|
|
8
11
|
import { buildPatterns } from '../pipeline/patterns';
|
|
9
|
-
import { resolveAssetStorageFromConfig } from '../registry/asset-storage/resolve';
|
|
12
|
+
import { resolveAssetStorageFromConfig, type ResolvedAssetStorage } from '../registry/asset-storage/resolve';
|
|
10
13
|
import { resolveDatabaseUrlEnv, resolveRegistryDriver } from '../registry/db/driver';
|
|
14
|
+
import { resolveEmailFromConfig, type EmailSettings } from '../registry/email/resolve';
|
|
11
15
|
import processComponents from '../transformers/preview/component/builder';
|
|
12
16
|
import { buildMainCss } from '../transformers/preview/component/css';
|
|
13
17
|
import { buildMainJS } from '../transformers/preview/component/javascript';
|
|
@@ -44,7 +48,7 @@ export const DEFAULT_BUILD_TARGET: BuildTarget = 'static';
|
|
|
44
48
|
/**
|
|
45
49
|
* Resolved packaging axis — orthogonal to {@link BuildTarget}. It
|
|
46
50
|
* selects *how* the build is packaged, not *what* is built:
|
|
47
|
-
* - `standalone` → the
|
|
51
|
+
* - `standalone` → the sites-directory deliverable (`out/static` for static, the
|
|
48
52
|
* Node standalone bundle `out/registry` for registry).
|
|
49
53
|
* - `vercel` → the Vercel Build Output API directory (`.vercel/output`) at the repo root.
|
|
50
54
|
*
|
|
@@ -71,7 +75,7 @@ export class HandoffBuildError extends Error {
|
|
|
71
75
|
* Resolve and validate the effective `(target, package)` pair.
|
|
72
76
|
*
|
|
73
77
|
* `--package` is optional and never implies a target. When omitted the effective package is
|
|
74
|
-
* `standalone` for both targets
|
|
78
|
+
* `standalone` for both targets (`out/static` / `out/registry`).
|
|
75
79
|
* The one rejected combination is `static + standalone`: a static snapshot has no server to package
|
|
76
80
|
* as a Node standalone bundle.
|
|
77
81
|
*/
|
|
@@ -162,16 +166,51 @@ const runNextBuild = async (appPath: string, target: BuildTarget, errorLabel: st
|
|
|
162
166
|
});
|
|
163
167
|
};
|
|
164
168
|
|
|
165
|
-
/**
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
* deliverable — both build targets emit to one discoverable, gitignored output root, keeping the
|
|
169
|
-
* package's `.handoff` for internal build/staging only. The registry artifact is a single
|
|
170
|
-
* deployable, so it is not keyed by project id.
|
|
171
|
-
*/
|
|
169
|
+
/** Fixed names, not project ids, so deploy paths are the same on every machine. */
|
|
170
|
+
export const getStaticBuildOutputPath = (handoff: Handoff): string => path.resolve(handoff.workingPath, handoff.sitesDirectory, 'static');
|
|
171
|
+
|
|
172
172
|
export const getRegistryBuildOutputPath = (handoff: Handoff): string =>
|
|
173
173
|
path.resolve(handoff.workingPath, handoff.sitesDirectory, 'registry');
|
|
174
174
|
|
|
175
|
+
const commonAncestorPath = (paths: string[]): string => {
|
|
176
|
+
const [first, ...rest] = paths.map((entry) => path.resolve(entry).split(path.sep));
|
|
177
|
+
let shared = first;
|
|
178
|
+
for (const candidate of rest) {
|
|
179
|
+
const next: string[] = [];
|
|
180
|
+
for (let index = 0; index < Math.min(shared.length, candidate.length) && shared[index] === candidate[index]; index += 1) {
|
|
181
|
+
next.push(shared[index]);
|
|
182
|
+
}
|
|
183
|
+
shared = next;
|
|
184
|
+
}
|
|
185
|
+
return shared.join(path.sep) || path.sep;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Root for Next's file tracing. Every traced path in the standalone bundle is relative to this root.
|
|
190
|
+
*
|
|
191
|
+
* The tracer copies only the files inside this root. It must contain the staged app (see
|
|
192
|
+
* {@link getAppPath}), or Next emits no standalone bundle. It must also contain every
|
|
193
|
+
* `node_modules` that the required runtime dependencies resolve from, or the bundle cannot start
|
|
194
|
+
* (see {@link assertRegistryRuntimeDepsTraced}).
|
|
195
|
+
*
|
|
196
|
+
* `workingPath` satisfies both conditions only when handoff-app is installed in the same package as
|
|
197
|
+
* the config, and nothing is hoisted above it.
|
|
198
|
+
*/
|
|
199
|
+
export const resolveRegistryTracingRoot = (handoff: Handoff): string => {
|
|
200
|
+
const anchors = [path.resolve(handoff.workingPath), path.resolve(handoff.modulePath)];
|
|
201
|
+
// Resolve each module instead of assuming its location. A hoisted or linked dependency otherwise
|
|
202
|
+
// drops out of the trace.
|
|
203
|
+
for (const moduleName of getRequiredRegistryRuntimeModules(handoff)) {
|
|
204
|
+
try {
|
|
205
|
+
const resolved = require.resolve(`${moduleName}/package.json`, { paths: [handoff.modulePath, handoff.workingPath] });
|
|
206
|
+
anchors.push(fs.realpathSync(path.dirname(resolved)));
|
|
207
|
+
} catch {
|
|
208
|
+
// The assertion after assembly reports a module that does not resolve.
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return commonAncestorPath(anchors);
|
|
212
|
+
};
|
|
213
|
+
|
|
175
214
|
/**
|
|
176
215
|
* Performs cleanup of the application directory by removing the existing app directory if it exists.
|
|
177
216
|
*/
|
|
@@ -243,7 +282,7 @@ const initializeProjectApp = async (handoff: Handoff, options: InitializeProject
|
|
|
243
282
|
const handoffAppBasePath = handoff.config.app.base_path ?? '';
|
|
244
283
|
const handoffWorkingPath = path.resolve(handoff.workingPath);
|
|
245
284
|
const handoffModulePath = path.resolve(handoff.modulePath);
|
|
246
|
-
const handoffExportPath =
|
|
285
|
+
const handoffExportPath = handoff.getOutputPath();
|
|
247
286
|
const nextConfigPath = path.resolve(srcPath, 'next.config.mjs');
|
|
248
287
|
const targetPath = path.resolve(appPath, 'next.config.mjs');
|
|
249
288
|
const handoffWebsocketPort = handoff.config.app.ports?.websocket ?? 3001;
|
|
@@ -251,6 +290,8 @@ const initializeProjectApp = async (handoff: Handoff, options: InitializeProject
|
|
|
251
290
|
const escapedProjectId = escapeForSingleQuotedJsString(handoffProjectId);
|
|
252
291
|
const escapedWorkingPath = escapeForSingleQuotedJsString(handoffWorkingPath);
|
|
253
292
|
const escapedModulePath = escapeForSingleQuotedJsString(handoffModulePath);
|
|
293
|
+
// Baked here so the template and the assembly step use the same root.
|
|
294
|
+
const escapedTracingRoot = escapeForSingleQuotedJsString(resolveRegistryTracingRoot(handoff));
|
|
254
295
|
const escapedExportPath = escapeForSingleQuotedJsString(handoffExportPath);
|
|
255
296
|
const escapedWebsocketPort = escapeForSingleQuotedJsString(String(handoffWebsocketPort));
|
|
256
297
|
// Resolved runtime mode + registry connection inputs (names only) baked into the Next bundle so the
|
|
@@ -259,29 +300,47 @@ const initializeProjectApp = async (handoff: Handoff, options: InitializeProject
|
|
|
259
300
|
const escapedMcpEnabled = escapeForSingleQuotedJsString(String(isMcpEnabled(handoff.config)));
|
|
260
301
|
const escapedRegistryDriver = escapeForSingleQuotedJsString(resolveRegistryDriver(handoff.config));
|
|
261
302
|
const escapedDatabaseUrlEnv = escapeForSingleQuotedJsString(resolveDatabaseUrlEnv(handoff.config));
|
|
262
|
-
// Asset storage selection baked (provider + module +
|
|
303
|
+
// Asset storage selection baked (provider + module + options JSON with `{ $env }` references).
|
|
263
304
|
const assetStorage = resolveAssetStorageFromConfig(handoff.config);
|
|
264
|
-
const
|
|
305
|
+
const escapedAssetStorageProvider = escapeForSingleQuotedJsString(assetStorage.provider);
|
|
265
306
|
const escapedAssetStorageModule = escapeForSingleQuotedJsString(assetStorage.module ?? '');
|
|
266
|
-
const escapedAssetStorageTokenEnv = escapeForSingleQuotedJsString(assetStorage.tokenEnv);
|
|
267
307
|
const escapedAssetStorageMaxInline = escapeForSingleQuotedJsString(String(assetStorage.maxInlineBytes));
|
|
268
308
|
const escapedAssetStorageOptions = escapeForSingleQuotedJsString(JSON.stringify(assetStorage.options ?? {}));
|
|
309
|
+
const email = resolveEmailFromConfig(handoff.config);
|
|
310
|
+
const escapedEmailProvider = escapeForSingleQuotedJsString(email.provider ?? '');
|
|
311
|
+
const escapedEmailFrom = escapeForSingleQuotedJsString(email.from ?? '');
|
|
312
|
+
const escapedEmailModule = escapeForSingleQuotedJsString(email.module ?? '');
|
|
313
|
+
const escapedEmailOptions = escapeForSingleQuotedJsString(JSON.stringify(email.options));
|
|
314
|
+
// AI assistant selection baked (enabled flag + declared connections + default model). Connections
|
|
315
|
+
// carry env-var names and non-secret options only; keys are read from their named env var at
|
|
316
|
+
// request time, and a deployment can extend or replace the list through HANDOFF_AI_CONNECTIONS.
|
|
317
|
+
const ai = resolveAiFromConfig(handoff.config);
|
|
318
|
+
const escapedAiEnabled = escapeForSingleQuotedJsString(String(ai.enabled));
|
|
319
|
+
const escapedAiConnections = escapeForSingleQuotedJsString(JSON.stringify(ai.connections));
|
|
320
|
+
const escapedAiDefaultModel = escapeForSingleQuotedJsString(ai.defaultModel ?? '');
|
|
269
321
|
const placeholderValues: Record<string, string> = {
|
|
270
322
|
'%HANDOFF_PROJECT_ID%': escapedProjectId,
|
|
271
323
|
'%HANDOFF_APP_BASE_PATH%': escapedAppBasePath,
|
|
272
324
|
'%HANDOFF_WORKING_PATH%': escapedWorkingPath,
|
|
273
325
|
'%HANDOFF_MODULE_PATH%': escapedModulePath,
|
|
326
|
+
'%HANDOFF_TRACING_ROOT%': escapedTracingRoot,
|
|
274
327
|
'%HANDOFF_EXPORT_PATH%': escapedExportPath,
|
|
275
328
|
'%HANDOFF_WEBSOCKET_PORT%': escapedWebsocketPort,
|
|
276
329
|
'%HANDOFF_RUNTIME_MODE%': escapedRuntimeMode,
|
|
277
330
|
'%HANDOFF_MCP_ENABLED%': escapedMcpEnabled,
|
|
278
331
|
'%HANDOFF_REGISTRY_DRIVER%': escapedRegistryDriver,
|
|
279
332
|
'%HANDOFF_REGISTRY_DATABASE_URL_ENV%': escapedDatabaseUrlEnv,
|
|
280
|
-
'%
|
|
333
|
+
'%HANDOFF_ASSET_STORAGE_PROVIDER%': escapedAssetStorageProvider,
|
|
281
334
|
'%HANDOFF_ASSET_STORAGE_MODULE%': escapedAssetStorageModule,
|
|
282
|
-
'%HANDOFF_ASSET_STORAGE_TOKEN_ENV%': escapedAssetStorageTokenEnv,
|
|
283
335
|
'%HANDOFF_ASSET_STORAGE_MAX_INLINE_BYTES%': escapedAssetStorageMaxInline,
|
|
284
336
|
'%HANDOFF_ASSET_STORAGE_OPTIONS%': escapedAssetStorageOptions,
|
|
337
|
+
'%HANDOFF_EMAIL_PROVIDER%': escapedEmailProvider,
|
|
338
|
+
'%HANDOFF_EMAIL_FROM%': escapedEmailFrom,
|
|
339
|
+
'%HANDOFF_EMAIL_MODULE%': escapedEmailModule,
|
|
340
|
+
'%HANDOFF_EMAIL_OPTIONS%': escapedEmailOptions,
|
|
341
|
+
'%HANDOFF_AI_ENABLED%': escapedAiEnabled,
|
|
342
|
+
'%HANDOFF_AI_BAKED_CONNECTIONS%': escapedAiConnections,
|
|
343
|
+
'%HANDOFF_AI_DEFAULT_MODEL%': escapedAiDefaultModel,
|
|
285
344
|
};
|
|
286
345
|
let nextConfigContent = await fs.readFile(nextConfigPath, 'utf-8');
|
|
287
346
|
for (const [placeholder, value] of Object.entries(placeholderValues)) {
|
|
@@ -350,23 +409,14 @@ const buildApp = async (
|
|
|
350
409
|
|
|
351
410
|
// Final assembly branches on the resolved package. `vercel` lays the materialized export
|
|
352
411
|
// under `.vercel/output/static/` at the repo root (not the sites directory) — a hard Vercel
|
|
353
|
-
// constraint; `standalone` keeps writing the `out
|
|
412
|
+
// constraint; `standalone` keeps writing the `out/static` export.
|
|
354
413
|
if (resolvedPackage === 'vercel') {
|
|
355
414
|
await writeStaticVercelOutput(handoff, exportDir);
|
|
356
415
|
return;
|
|
357
416
|
}
|
|
358
417
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
await fs.ensureDir(outputRoot);
|
|
362
|
-
|
|
363
|
-
// Clean the project output directory (if exists)
|
|
364
|
-
const output = path.resolve(outputRoot, handoff.getProjectId());
|
|
365
|
-
if (fs.existsSync(output)) {
|
|
366
|
-
await fs.remove(output);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// Copy the build files into the project output directory
|
|
418
|
+
const output = getStaticBuildOutputPath(handoff);
|
|
419
|
+
await fs.remove(output);
|
|
370
420
|
await fs.copy(exportDir, output);
|
|
371
421
|
};
|
|
372
422
|
|
|
@@ -402,8 +452,36 @@ const findStandaloneServerDir = (root: string): string | null => {
|
|
|
402
452
|
const writeRegistryDeploymentReadme = async (
|
|
403
453
|
outputRoot: string,
|
|
404
454
|
entryRelativePath: string,
|
|
405
|
-
databaseUrlEnv: string
|
|
455
|
+
databaseUrlEnv: string,
|
|
456
|
+
ai: AiSettings,
|
|
457
|
+
email: EmailSettings,
|
|
458
|
+
assetStorage: ResolvedAssetStorage
|
|
406
459
|
): Promise<void> => {
|
|
460
|
+
const serviceKeyEnvs = [...new Set(ai.connections.map((connection) => connection.apiKeyEnv).filter(Boolean))];
|
|
461
|
+
const aiOptionEnvs = [...new Set(ai.connections.flatMap((connection) => envReferenceNames(connection.options ?? {})))];
|
|
462
|
+
const needsKeySecret = ai.connections.some((connection) => connection.credential === 'user');
|
|
463
|
+
const aiSection = !ai.enabled
|
|
464
|
+
? ''
|
|
465
|
+
: `
|
|
466
|
+
## AI assistant
|
|
467
|
+
|
|
468
|
+
This build serves the docs assistant at \`/api/ai/*\`.
|
|
469
|
+
|
|
470
|
+
${serviceKeyEnvs.map((name) => `- \`${name}\` — API key for a declared AI connection, read at request time.`).join('\n') || '- No service-key connection is declared, so no provider key is required here.'}
|
|
471
|
+
${aiOptionEnvs.map((name) => `- \`${name}\` — read by a custom AI provider at request time.\n`).join('')}${needsKeySecret ? '- `HANDOFF_AI_KEY_SECRET` — a long, random secret that encrypts the API keys readers save for themselves. Readers cannot save a key without it.\n' : ''}
|
|
472
|
+
\`HANDOFF_AI_CONNECTIONS\` optionally carries a JSON array of connections, read at request time and
|
|
473
|
+
merged over the list this build baked in, keyed by \`id\`. Use it to add or repoint a provider
|
|
474
|
+
without rebuilding.
|
|
475
|
+
`;
|
|
476
|
+
|
|
477
|
+
const assetStorageEnvs = assetStorage.provider === 'custom' ? envReferenceNames(assetStorage.options) : [];
|
|
478
|
+
const emailEnvs = envReferenceNames(email.options);
|
|
479
|
+
const emailSection = !email.from
|
|
480
|
+
? 'Email delivery is off because `runtime.registry.email.from` is not set. Invitation links are shown once to an administrator for manual delivery.'
|
|
481
|
+
: `Invitation and password-reset emails are sent from \`${email.from}\` through the \`${email.provider}\` provider.${
|
|
482
|
+
emailEnvs.length ? ` The provider reads ${emailEnvs.map((name) => `\`${name}\``).join(', ')} at request time.` : ''
|
|
483
|
+
} Until the provider's settings have values, invitation links are shown once to an administrator for manual delivery.`;
|
|
484
|
+
|
|
407
485
|
const readme = `# Handoff Registry App
|
|
408
486
|
|
|
409
487
|
A self-contained, deployable Next.js **standalone** build produced by
|
|
@@ -420,10 +498,9 @@ and \`.next/static/\` already copied alongside so the server serves them.
|
|
|
420
498
|
- \`${databaseUrlEnv}\` — PostgreSQL/Neon connection string, read at request time.
|
|
421
499
|
- \`AUTH_SECRET\` — a long, random secret used to sign browser sessions.
|
|
422
500
|
- \`AUTH_URL\` — the canonical public registry URL, including the configured base path.
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
501
|
+
${assetStorageEnvs.map((name) => `- \`${name}\` — read by the custom asset storage provider at request time.\n`).join('')}
|
|
502
|
+
${emailSection}
|
|
503
|
+
${aiSection}
|
|
427
504
|
## Database migrations
|
|
428
505
|
|
|
429
506
|
Apply migrations from the \`handoff-app\` CLI as a controlled release step (e.g. in your CI/CD
|
|
@@ -492,8 +569,8 @@ For multi-instance self-hosting, front \`/_next/static\` with a CDN and configur
|
|
|
492
569
|
|
|
493
570
|
## Deploying to Vercel
|
|
494
571
|
|
|
495
|
-
Vercel does **not** use \`output: 'standalone'\` — it ignores that setting and
|
|
496
|
-
|
|
572
|
+
Vercel does **not** use \`output: 'standalone'\` — it ignores that setting and runs its
|
|
573
|
+
own build, so this pre-built bundle is **not** the artifact you deploy there. For Vercel, deploy the
|
|
497
574
|
registry app via Vercel's Git/CLI source build (it produces and serves the dynamic app itself) and
|
|
498
575
|
supply the same environment variables above as project env vars. Use this standalone bundle for
|
|
499
576
|
containers, custom Node servers, and other non-Vercel hosts.
|
|
@@ -505,29 +582,25 @@ containers, custom Node servers, and other non-Vercel hosts.
|
|
|
505
582
|
* Resolve the set of npm packages the packaged registry app must be able to `require`/`import` at
|
|
506
583
|
* runtime, given the configured database driver. `next`/`react`/`react-dom` run the server and React
|
|
507
584
|
* runtime; `drizzle-orm` backs both the request-time DB client and the migration runner; the MCP SDK
|
|
508
|
-
*
|
|
509
|
-
* driver-specific (the Neon serverless driver also needs `ws` for its Node WebSocket transport)
|
|
585
|
+
* is only needed when `/api/mcp/` is enabled; the driver package is
|
|
586
|
+
* driver-specific (the Neon serverless driver also needs `ws` for its Node WebSocket transport); the
|
|
587
|
+
* SMTP email provider needs `nodemailer`.
|
|
510
588
|
*/
|
|
511
589
|
const getRequiredRegistryRuntimeModules = (handoff: Handoff): string[] => {
|
|
512
590
|
const base = ['next', 'next-auth', 'react', 'react-dom', 'drizzle-orm'];
|
|
513
|
-
|
|
514
|
-
|
|
591
|
+
const ai = resolveAiFromConfig(handoff.config);
|
|
592
|
+
// The assistant runs the MCP tools in-process, so it needs the MCP SDK whether or not the HTTP
|
|
593
|
+
// endpoint is served.
|
|
594
|
+
if (isMcpEnabled(handoff.config) || ai.enabled) {
|
|
595
|
+
base.push('@modelcontextprotocol/sdk');
|
|
596
|
+
}
|
|
597
|
+
if (ai.enabled) {
|
|
598
|
+
base.push('ai', '@ai-sdk/openai-compatible');
|
|
515
599
|
}
|
|
516
600
|
const driver = resolveRegistryDriver(handoff.config);
|
|
517
601
|
const driverModules = driver === 'neon' ? ['@neondatabase/serverless', 'ws'] : ['pg'];
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
// package(s) via `assetStorage.options.sdkModules` so the trace/assertion covers them.
|
|
521
|
-
const assetStorage = resolveAssetStorageFromConfig(handoff.config);
|
|
522
|
-
const storage: string[] = [];
|
|
523
|
-
if (assetStorage.adapterKind === 'vercel-blob') {
|
|
524
|
-
storage.push('@vercel/blob');
|
|
525
|
-
}
|
|
526
|
-
const sdkModules = assetStorage.options?.sdkModules;
|
|
527
|
-
if (Array.isArray(sdkModules)) {
|
|
528
|
-
storage.push(...sdkModules.filter((mod): mod is string => typeof mod === 'string'));
|
|
529
|
-
}
|
|
530
|
-
return [...base, ...driverModules, ...storage];
|
|
602
|
+
const email = resolveEmailFromConfig(handoff.config).provider === 'smtp' ? ['nodemailer'] : [];
|
|
603
|
+
return [...base, ...driverModules, ...email];
|
|
531
604
|
};
|
|
532
605
|
|
|
533
606
|
/**
|
|
@@ -558,6 +631,64 @@ const assertRegistryRuntimeDepsTraced = (handoff: Handoff, entryDir: string): vo
|
|
|
558
631
|
);
|
|
559
632
|
};
|
|
560
633
|
|
|
634
|
+
const getCustomServerModules = (handoff: Handoff): string[] => {
|
|
635
|
+
const assetStorage = resolveAssetStorageFromConfig(handoff.config);
|
|
636
|
+
const email = resolveEmailFromConfig(handoff.config);
|
|
637
|
+
const modules = [
|
|
638
|
+
assetStorage.provider === 'custom' ? assetStorage.module : undefined,
|
|
639
|
+
email.provider === 'custom' ? email.module : undefined,
|
|
640
|
+
...resolveAiFromConfig(handoff.config).connections.map((connection) => connection.module),
|
|
641
|
+
];
|
|
642
|
+
return [...new Set(modules.filter((module): module is string => Boolean(module)))];
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Copy the custom server modules and every file they import into the bundle. The Next trace cannot
|
|
647
|
+
* follow their variable dynamic import.
|
|
648
|
+
*/
|
|
649
|
+
const copyCustomServerModules = async (handoff: Handoff, tracingRoot: string, entryDir: string): Promise<void> => {
|
|
650
|
+
const modules = getCustomServerModules(handoff);
|
|
651
|
+
if (modules.length === 0) return;
|
|
652
|
+
|
|
653
|
+
const missing = modules.filter((module) => !fs.existsSync(path.resolve(handoff.workingPath, module)));
|
|
654
|
+
if (missing.length > 0) {
|
|
655
|
+
throw new HandoffBuildError(`A custom server module does not exist: ${missing.map((module) => `"${module}"`).join(', ')}.`);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const files = new Set<string>();
|
|
659
|
+
for (const module of modules) {
|
|
660
|
+
const { fileList } = await nodeFileTrace([path.resolve(handoff.workingPath, module)], { base: tracingRoot });
|
|
661
|
+
const outside = [...fileList].filter((file) => file.startsWith('..'));
|
|
662
|
+
if (outside.length > 0) {
|
|
663
|
+
throw new HandoffBuildError(
|
|
664
|
+
`The custom server module "${module}" imports files outside the build's tracing root "${tracingRoot}": ${outside.join(', ')}. ` +
|
|
665
|
+
`Install the packages it imports so that they resolve from "${handoff.workingPath}".`
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
if ([...fileList].some((file) => file.split(path.sep).join('/').endsWith('node_modules/handoff-app/dist/index.js'))) {
|
|
669
|
+
throw new HandoffBuildError(
|
|
670
|
+
`The custom server module "${module}" imports "handoff-app", which loads the whole CLI. Import the define helpers from "handoff-app/providers".`
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
fileList.forEach((file) => files.add(file));
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
for (const file of files) {
|
|
677
|
+
const source = path.join(tracingRoot, file);
|
|
678
|
+
const target = path.join(entryDir, file);
|
|
679
|
+
if (await fs.lstat(target).then(() => true, () => false)) continue;
|
|
680
|
+
const stat = await fs.lstat(source);
|
|
681
|
+
await fs.ensureDir(path.dirname(target));
|
|
682
|
+
if (stat.isSymbolicLink()) {
|
|
683
|
+
// Point the link at the bundle copy of its target, not at the build machine.
|
|
684
|
+
const linked = path.join(entryDir, path.relative(tracingRoot, await fs.realpath(source)));
|
|
685
|
+
await fs.symlink(path.relative(path.dirname(target), linked), target);
|
|
686
|
+
} else if (stat.isFile()) {
|
|
687
|
+
await fs.copy(source, target);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
|
|
561
692
|
/**
|
|
562
693
|
* Lay the Next `standalone` build out as the documented standalone layout under `entryDir`: copy the
|
|
563
694
|
* traced bundle, flatten the package-rooted server entry up to `entryDir`, and copy the untraced
|
|
@@ -567,22 +698,14 @@ const assertRegistryRuntimeDepsTraced = (handoff: Handoff, entryDir: string): vo
|
|
|
567
698
|
*
|
|
568
699
|
* @returns the absolute path of the server entry (`<entryDir>/server.js`).
|
|
569
700
|
*/
|
|
570
|
-
const assembleRegistryStandalone = async (
|
|
571
|
-
handoff: Handoff,
|
|
572
|
-
appPath: string,
|
|
573
|
-
standaloneRoot: string,
|
|
574
|
-
entryDir: string
|
|
575
|
-
): Promise<string> => {
|
|
701
|
+
const assembleRegistryStandalone = async (handoff: Handoff, appPath: string, standaloneRoot: string, entryDir: string): Promise<string> => {
|
|
576
702
|
await fs.copy(standaloneRoot, entryDir, { overwrite: true });
|
|
577
703
|
|
|
578
|
-
// Locate the server entry
|
|
579
|
-
//
|
|
580
|
-
//
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
// skips `node_modules` (so it cannot find this nested entry) and only covers a hypothetical
|
|
584
|
-
// non-nested layout.
|
|
585
|
-
const relAppDir = path.relative(handoff.workingPath, appPath);
|
|
704
|
+
// Locate the server entry in the assembled package. Traced files keep their path relative to the
|
|
705
|
+
// tracing root, so the server entry nests under the traced `node_modules`. The recursive fallback
|
|
706
|
+
// skips `node_modules`, so it covers only a non-nested layout.
|
|
707
|
+
const tracingRoot = resolveRegistryTracingRoot(handoff);
|
|
708
|
+
const relAppDir = path.relative(tracingRoot, appPath);
|
|
586
709
|
let serverDir = path.resolve(entryDir, relAppDir);
|
|
587
710
|
if (!fs.existsSync(path.join(serverDir, 'server.js'))) {
|
|
588
711
|
const located = findStandaloneServerDir(entryDir);
|
|
@@ -605,7 +728,7 @@ const assembleRegistryStandalone = async (
|
|
|
605
728
|
// so we must NOT remove the first path segment (`node_modules`) — that would delete every traced
|
|
606
729
|
// dependency. Remove only the `.handoff` staging container (the parent of the staged app dir),
|
|
607
730
|
// leaving `node_modules/` and the real `node_modules/handoff-app/` package intact.
|
|
608
|
-
const stagingContainer = path.resolve(entryDir, path.relative(
|
|
731
|
+
const stagingContainer = path.resolve(entryDir, path.relative(tracingRoot, path.dirname(appPath)));
|
|
609
732
|
if (path.basename(stagingContainer) === '.handoff' && fs.existsSync(stagingContainer)) {
|
|
610
733
|
await fs.remove(stagingContainer);
|
|
611
734
|
}
|
|
@@ -631,6 +754,19 @@ const assembleRegistryStandalone = async (
|
|
|
631
754
|
}
|
|
632
755
|
}
|
|
633
756
|
|
|
757
|
+
// Next copies the `package.json` of the tracing root into the bundle without change. The generated
|
|
758
|
+
// `server.js` is CommonJS, so an ESM manifest stops the bundle from starting. That manifest also
|
|
759
|
+
// names scripts and dev dependencies that the bundle does not ship.
|
|
760
|
+
const manifestPath = path.join(entryDir, 'package.json');
|
|
761
|
+
const inheritedManifest: Record<string, unknown> = (await fs.pathExists(manifestPath))
|
|
762
|
+
? await fs.readJson(manifestPath).catch(() => ({}))
|
|
763
|
+
: {};
|
|
764
|
+
delete inheritedManifest.scripts;
|
|
765
|
+
delete inheritedManifest.devDependencies;
|
|
766
|
+
await fs.writeJson(manifestPath, { ...inheritedManifest, type: 'commonjs' }, { spaces: 2 });
|
|
767
|
+
|
|
768
|
+
await copyCustomServerModules(handoff, tracingRoot, entryDir);
|
|
769
|
+
|
|
634
770
|
// Fail loudly at build time if the trace did not capture the runtime deps, rather than shipping a
|
|
635
771
|
// broken artifact that only crashes on an isolated deploy. Covers both deliverables (this is the
|
|
636
772
|
// shared assembly step for `standalone` and `vercel`).
|
|
@@ -710,8 +846,10 @@ const buildRegistryApp = async (handoff: Handoff, buildPackage: BuildPackage = '
|
|
|
710
846
|
const standaloneRoot = path.resolve(appPath, '.next', 'standalone');
|
|
711
847
|
if (!fs.existsSync(standaloneRoot)) {
|
|
712
848
|
throw new Error(
|
|
713
|
-
`Registry build did not produce a standalone bundle at "${standaloneRoot}".
|
|
714
|
-
|
|
849
|
+
`Registry build did not produce a standalone bundle at "${standaloneRoot}". Next emits one only ` +
|
|
850
|
+
`when the app being built sits inside \`outputFileTracingRoot\` (resolved here to ` +
|
|
851
|
+
`"${resolveRegistryTracingRoot(handoff)}"), so check that the staged app at "${appPath}" is ` +
|
|
852
|
+
'inside that root, and that the registry build target still sets Next `output: "standalone"`.'
|
|
715
853
|
);
|
|
716
854
|
}
|
|
717
855
|
|
|
@@ -733,7 +871,14 @@ const buildRegistryApp = async (handoff: Handoff, buildPackage: BuildPackage = '
|
|
|
733
871
|
// never from the deployed bundle. See the generated README for the migrate-then-deploy flow.
|
|
734
872
|
|
|
735
873
|
const entryRelativePath = path.relative(output, serverEntry).split(path.sep).join('/');
|
|
736
|
-
await writeRegistryDeploymentReadme(
|
|
874
|
+
await writeRegistryDeploymentReadme(
|
|
875
|
+
output,
|
|
876
|
+
entryRelativePath,
|
|
877
|
+
resolveDatabaseUrlEnv(handoff.config),
|
|
878
|
+
resolveAiFromConfig(handoff.config),
|
|
879
|
+
resolveEmailFromConfig(handoff.config),
|
|
880
|
+
resolveAssetStorageFromConfig(handoff.config)
|
|
881
|
+
);
|
|
737
882
|
|
|
738
883
|
Logger.success(`Packaged registry app at ${output} (start: \`node ${entryRelativePath}\`, migrate: \`handoff-app db:migrate\`).`);
|
|
739
884
|
};
|
|
@@ -744,11 +889,11 @@ const buildRegistryApp = async (handoff: Handoff, buildPackage: BuildPackage = '
|
|
|
744
889
|
*/
|
|
745
890
|
export const watchApp = async (handoff: Handoff): Promise<void> => {
|
|
746
891
|
// Build the shared/global artifacts first so component/pattern preview HTML references them only
|
|
747
|
-
// when present, then process components
|
|
748
|
-
//
|
|
892
|
+
// when present, then process components. The build cache skips the components whose files did not
|
|
893
|
+
// change.
|
|
749
894
|
await buildMainJS(handoff);
|
|
750
895
|
await buildMainCss(handoff);
|
|
751
|
-
await processComponents(handoff
|
|
896
|
+
await processComponents(handoff);
|
|
752
897
|
|
|
753
898
|
// Build patterns after components are ready
|
|
754
899
|
await buildPatterns(handoff);
|
|
@@ -5,6 +5,7 @@ import { getClientConfig, isMcpEnabled } from '../config';
|
|
|
5
5
|
import { resolveAssetStorageFromConfig } from '../registry/asset-storage/resolve';
|
|
6
6
|
import { resolveAuthenticatedRegistryConnection } from '../registry/connection';
|
|
7
7
|
import { resolveDatabaseUrlEnv, resolveRegistryDriver } from '../registry/db/driver';
|
|
8
|
+
import { resolveEmailFromConfig } from '../registry/email/resolve';
|
|
8
9
|
import type { ResolvedConfig, RuntimeMode } from '../types/config';
|
|
9
10
|
import { getAppPath } from './paths';
|
|
10
11
|
|
|
@@ -63,12 +64,12 @@ const buildServerRuntimeConfig = (config: ResolvedConfig, modeOverride?: Runtime
|
|
|
63
64
|
databaseUrlEnv: resolveDatabaseUrlEnv(config),
|
|
64
65
|
},
|
|
65
66
|
assetStorage: {
|
|
66
|
-
|
|
67
|
+
provider: assetStorage.provider,
|
|
67
68
|
module: assetStorage.module,
|
|
68
|
-
tokenEnv: assetStorage.tokenEnv,
|
|
69
69
|
maxInlineBytes: assetStorage.maxInlineBytes,
|
|
70
70
|
options: assetStorage.options,
|
|
71
71
|
},
|
|
72
|
+
email: resolveEmailFromConfig(config),
|
|
72
73
|
};
|
|
73
74
|
};
|
|
74
75
|
|
package/src/app-builder/paths.ts
CHANGED
|
@@ -2,30 +2,11 @@ import fs from 'fs-extra';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import Handoff from '..';
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* Gets the working public directory path for a given handoff instance.
|
|
7
|
-
* Checks for both project-specific and default public directories.
|
|
8
|
-
*
|
|
9
|
-
* @param handoff - The handoff instance containing working path and figma project configuration
|
|
10
|
-
* @returns The resolved path to the public directory if it exists, null otherwise
|
|
11
|
-
*/
|
|
12
5
|
export const getWorkingPublicPath = (handoff: Handoff): string | null => {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
for (const p of paths) {
|
|
16
|
-
if (fs.existsSync(p)) {
|
|
17
|
-
return p;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return null;
|
|
6
|
+
const publicPath = path.resolve(handoff.workingPath, 'public');
|
|
7
|
+
return fs.existsSync(publicPath) ? publicPath : null;
|
|
22
8
|
};
|
|
23
9
|
|
|
24
|
-
/**
|
|
25
|
-
* Gets the application path for a given handoff instance.
|
|
26
|
-
* @param handoff - The handoff instance containing module path and figma project configuration
|
|
27
|
-
* @returns The resolved path to the application directory
|
|
28
|
-
*/
|
|
29
10
|
export const getAppPath = (handoff: Handoff): string => {
|
|
30
11
|
return path.resolve(handoff.modulePath, '.handoff', `${handoff.getProjectId()}`);
|
|
31
12
|
};
|
|
@@ -42,7 +23,7 @@ const mirrorDirectory = async (sourcePath: string, destinationPath: string): Pro
|
|
|
42
23
|
};
|
|
43
24
|
|
|
44
25
|
/**
|
|
45
|
-
* Copy the fetch-produced asset bundles (`exported
|
|
26
|
+
* Copy the fetch-produced asset bundles (`exported/{collection}.zip`) into the app's public
|
|
46
27
|
* root, where the docs read API serves them at `/api/docs/assets/{collection}/{collection}.zip`.
|
|
47
28
|
* `fetch` writes these bundles, but `build`/`start` cleans and re-stages `.handoff` without them, so
|
|
48
29
|
* the download links 404 until the next fetch. Copying them here keeps the route working in workspace
|
|
@@ -220,7 +220,7 @@ export const materializeDocsReadModel = async (handoff: Handoff, outDir: string)
|
|
|
220
220
|
* Write the whole-collection asset bundles to the asset route
|
|
221
221
|
* (`api/docs/assets/{collection}/{collection}.zip`), the same URL the docs read API serves in
|
|
222
222
|
* registry mode, so the "Download" links work identically on a static host. The bundles come from the
|
|
223
|
-
* fetch output (`exported
|
|
223
|
+
* fetch output (`exported/{collection}.zip`); `next build` never re-creates them and the
|
|
224
224
|
* fetch-time copy into `public/` is wiped by the build's clean+restage, so we copy them here again.
|
|
225
225
|
* Projects with no logos/icons have no bundles, so those are skipped rather than treated as an error.
|
|
226
226
|
*/
|
|
@@ -40,7 +40,7 @@ const resolveBasePath = (rawBasePath: string | undefined): string => {
|
|
|
40
40
|
* no functions, no database.
|
|
41
41
|
*
|
|
42
42
|
* @param exportDir Absolute path of the materialized static export (the same content the static
|
|
43
|
-
* target writes to `out
|
|
43
|
+
* target writes to `out/static`).
|
|
44
44
|
*/
|
|
45
45
|
export const writeStaticVercelOutput = async (handoff: Handoff, exportDir: string): Promise<void> => {
|
|
46
46
|
const outputPath = getVercelOutputPath(handoff);
|
|
@@ -69,10 +69,11 @@ export const writeStaticVercelOutput = async (handoff: Handoff, exportDir: strin
|
|
|
69
69
|
/**
|
|
70
70
|
* Node.js runtime identifier baked into the registry function's `.vc-config.json`. The `pg` driver
|
|
71
71
|
* (and the Neon serverless driver in its Node fallback) need the Node.js runtime, not the edge
|
|
72
|
-
* runtime
|
|
73
|
-
* version within this major; pinning the major keeps
|
|
72
|
+
* runtime. The major matches the package `engines` minimum, and the Vercel project's Node.js setting
|
|
73
|
+
* does not override it. Vercel resolves the patch version within this major; pinning the major keeps
|
|
74
|
+
* the artifact stable.
|
|
74
75
|
*/
|
|
75
|
-
const VERCEL_NODE_RUNTIME = '
|
|
76
|
+
const VERCEL_NODE_RUNTIME = 'nodejs22.x';
|
|
76
77
|
|
|
77
78
|
/** Name (sans `.func`) of the single registry function. Served at `/` and the catch-all route. */
|
|
78
79
|
const REGISTRY_FUNCTION_NAME = 'index';
|
package/src/cache/README.md
CHANGED
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
Incremental build cache for component previews. Tracks file states to skip rebuilding unchanged components.
|
|
4
4
|
|
|
5
|
+
Every full component build uses the cache (`start`, `build`, `build:components` without an id, and bulk `publish`), unless `--force` is set. The cache file is `<workingPath>/.handoff/.cache/build-cache.json`. A component entry holds its declared files and every file that its last build read: Rollup watch files from the Vite builds (Sass partials included) and the esbuild graph of each script entry. The lockfile state in the global dependencies covers the files under `node_modules`.
|
|
6
|
+
|
|
5
7
|
## Files
|
|
6
8
|
|
|
7
9
|
| File | Purpose |
|
|
8
10
|
|------|---------|
|
|
9
11
|
| `file-state.ts` | `computeFileState()`, `computeDirectoryState()`, `statesMatch()`, `directoryStatesMatch()` — file-level change detection |
|
|
10
|
-
| `build-cache.ts` | `loadBuildCache()`, `saveBuildCache()`, `hasComponentChanged()`, `computeGlobalDepsState()`, `haveGlobalDepsChanged()`, `computeComponentFileStates()` — component-level and global dependency cache management |
|
|
12
|
+
| `build-cache.ts` | `loadBuildCache()`, `saveBuildCache()`, `hasComponentChanged()`, `computeGlobalDepsState()`, `haveGlobalDepsChanged()`, `computeComponentFileStates()`, `computeBuildInputStates()` — component-level and global dependency cache management |
|
|
13
|
+
| `build-inputs.ts` | `createBuildInputs()`, `withBuildInputs()` — collects the files that the Vite builds of a component read |
|
|
11
14
|
| `index.ts` | Barrel re-exports |
|