handoff-app 2.0.0-9 → 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 +813 -406
- 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/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 +26 -2
- package/dist/config/resolve-env.d.ts +2 -1
- package/dist/config/resolve-env.js +28 -12
- package/dist/config/runtime.js +1 -1
- 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/preview/component/builder.d.ts +1 -9
- package/dist/transformers/preview/component/builder.js +17 -17
- package/dist/transformers/preview/component/css.d.ts +2 -1
- package/dist/transformers/preview/component/css.js +8 -5
- 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/utils/build.d.ts +5 -0
- package/dist/transformers/utils/build.js +21 -4
- package/dist/types/config.d.ts +129 -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/Preview.tsx +5 -2
- 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/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 +25 -3
- package/src/config/resolve-env.ts +28 -15
- package/src/config/runtime.ts +1 -1
- 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/preview/component/builder.ts +19 -25
- package/src/transformers/preview/component/css.ts +9 -4
- package/src/transformers/preview/component/html.ts +6 -2
- package/src/transformers/preview/component/javascript.ts +14 -4
- package/src/transformers/utils/build.ts +16 -3
- package/src/types/config.ts +142 -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
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-only AI provider contract for connections that are not OpenAI-compatible.
|
|
3
|
+
*
|
|
4
|
+
* Every built-in connection speaks the OpenAI-compatible `/chat/completions` API and needs no
|
|
5
|
+
* custom module — Ollama, LiteLLM, OpenRouter, vLLM, LM Studio and Azure OpenAI all serve it, and each of
|
|
6
|
+
* them fronts Anthropic, Google and xAI models. A provider that fits nothing else names a
|
|
7
|
+
* {@link defineAiProvider} module instead of a base URL.
|
|
8
|
+
*/
|
|
9
|
+
import type { LanguageModel } from 'ai';
|
|
10
|
+
/** Context passed to a custom provider factory. */
|
|
11
|
+
export interface AiProviderContext {
|
|
12
|
+
/** The connection's `options`, with each `fromEnv()` reference replaced by its value. */
|
|
13
|
+
options: Record<string, unknown>;
|
|
14
|
+
env: NodeJS.ProcessEnv;
|
|
15
|
+
/**
|
|
16
|
+
* The resolved key for this request: the deployment's own key for a service connection, or the
|
|
17
|
+
* reader's saved key for a `credential: 'user'` connection. Undefined when neither applies.
|
|
18
|
+
*/
|
|
19
|
+
apiKey?: string;
|
|
20
|
+
}
|
|
21
|
+
/** The pluggable provider contract: resolve one of the connection's declared models. */
|
|
22
|
+
export interface AiProvider {
|
|
23
|
+
languageModel(modelId: string): LanguageModel;
|
|
24
|
+
}
|
|
25
|
+
/** A custom provider may export the provider directly or a factory that builds it from its context. */
|
|
26
|
+
export type AiProviderFactory = (context: AiProviderContext) => AiProvider | Promise<AiProvider>;
|
package/dist/ai/types.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Server-only AI provider contract for connections that are not OpenAI-compatible.
|
|
4
|
+
*
|
|
5
|
+
* Every built-in connection speaks the OpenAI-compatible `/chat/completions` API and needs no
|
|
6
|
+
* custom module — Ollama, LiteLLM, OpenRouter, vLLM, LM Studio and Azure OpenAI all serve it, and each of
|
|
7
|
+
* them fronts Anthropic, Google and xAI models. A provider that fits nothing else names a
|
|
8
|
+
* {@link defineAiProvider} module instead of a base URL.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -10,7 +10,7 @@ export declare const DEFAULT_BUILD_TARGET: BuildTarget;
|
|
|
10
10
|
/**
|
|
11
11
|
* Resolved packaging axis — orthogonal to {@link BuildTarget}. It
|
|
12
12
|
* selects *how* the build is packaged, not *what* is built:
|
|
13
|
-
* - `standalone` → the
|
|
13
|
+
* - `standalone` → the sites-directory deliverable (`out/static` for static, the
|
|
14
14
|
* Node standalone bundle `out/registry` for registry).
|
|
15
15
|
* - `vercel` → the Vercel Build Output API directory (`.vercel/output`) at the repo root.
|
|
16
16
|
*
|
|
@@ -28,14 +28,21 @@ export type BuildPackage = 'standalone' | 'vercel';
|
|
|
28
28
|
export declare class HandoffBuildError extends Error {
|
|
29
29
|
constructor(message: string);
|
|
30
30
|
}
|
|
31
|
+
/** Fixed names, not project ids, so deploy paths are the same on every machine. */
|
|
32
|
+
export declare const getStaticBuildOutputPath: (handoff: Handoff) => string;
|
|
33
|
+
export declare const getRegistryBuildOutputPath: (handoff: Handoff) => string;
|
|
31
34
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
35
|
+
* Root for Next's file tracing. Every traced path in the standalone bundle is relative to this root.
|
|
36
|
+
*
|
|
37
|
+
* The tracer copies only the files inside this root. It must contain the staged app (see
|
|
38
|
+
* {@link getAppPath}), or Next emits no standalone bundle. It must also contain every
|
|
39
|
+
* `node_modules` that the required runtime dependencies resolve from, or the bundle cannot start
|
|
40
|
+
* (see {@link assertRegistryRuntimeDepsTraced}).
|
|
41
|
+
*
|
|
42
|
+
* `workingPath` satisfies both conditions only when handoff-app is installed in the same package as
|
|
43
|
+
* the config, and nothing is hoisted above it.
|
|
37
44
|
*/
|
|
38
|
-
export declare const
|
|
45
|
+
export declare const resolveRegistryTracingRoot: (handoff: Handoff) => string;
|
|
39
46
|
/**
|
|
40
47
|
* Build the Next.js documentation application for the resolved target. The `static` target builds
|
|
41
48
|
* and exports the local workspace (default); the `registry` target packages the deployable dynamic
|
|
@@ -35,16 +35,20 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
35
35
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
36
36
|
};
|
|
37
37
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
|
-
exports.devApp = exports.watchApp = exports.getRegistryBuildOutputPath = exports.HandoffBuildError = exports.DEFAULT_BUILD_TARGET = void 0;
|
|
38
|
+
exports.devApp = exports.watchApp = exports.resolveRegistryTracingRoot = exports.getRegistryBuildOutputPath = exports.getStaticBuildOutputPath = exports.HandoffBuildError = exports.DEFAULT_BUILD_TARGET = void 0;
|
|
39
39
|
const p = __importStar(require("@clack/prompts"));
|
|
40
|
+
const nft_1 = require("@vercel/nft");
|
|
40
41
|
const cross_spawn_1 = __importDefault(require("cross-spawn"));
|
|
41
42
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
42
43
|
const path_1 = __importDefault(require("path"));
|
|
44
|
+
const connections_1 = require("../ai/connections");
|
|
43
45
|
const config_1 = require("../config");
|
|
46
|
+
const from_env_1 = require("../config/from-env");
|
|
44
47
|
const components_1 = require("../pipeline/components");
|
|
45
48
|
const patterns_1 = require("../pipeline/patterns");
|
|
46
49
|
const resolve_1 = require("../registry/asset-storage/resolve");
|
|
47
50
|
const driver_1 = require("../registry/db/driver");
|
|
51
|
+
const resolve_2 = require("../registry/email/resolve");
|
|
48
52
|
const builder_1 = __importDefault(require("../transformers/preview/component/builder"));
|
|
49
53
|
const css_1 = require("../transformers/preview/component/css");
|
|
50
54
|
const javascript_1 = require("../transformers/preview/component/javascript");
|
|
@@ -75,7 +79,7 @@ exports.HandoffBuildError = HandoffBuildError;
|
|
|
75
79
|
* Resolve and validate the effective `(target, package)` pair.
|
|
76
80
|
*
|
|
77
81
|
* `--package` is optional and never implies a target. When omitted the effective package is
|
|
78
|
-
* `standalone` for both targets
|
|
82
|
+
* `standalone` for both targets (`out/static` / `out/registry`).
|
|
79
83
|
* The one rejected combination is `static + standalone`: a static snapshot has no server to package
|
|
80
84
|
* as a Node standalone bundle.
|
|
81
85
|
*/
|
|
@@ -148,15 +152,50 @@ const runNextBuild = (appPath, target, errorLabel) => __awaiter(void 0, void 0,
|
|
|
148
152
|
});
|
|
149
153
|
});
|
|
150
154
|
});
|
|
151
|
-
/**
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
* deliverable — both build targets emit to one discoverable, gitignored output root, keeping the
|
|
155
|
-
* package's `.handoff` for internal build/staging only. The registry artifact is a single
|
|
156
|
-
* deployable, so it is not keyed by project id.
|
|
157
|
-
*/
|
|
155
|
+
/** Fixed names, not project ids, so deploy paths are the same on every machine. */
|
|
156
|
+
const getStaticBuildOutputPath = (handoff) => path_1.default.resolve(handoff.workingPath, handoff.sitesDirectory, 'static');
|
|
157
|
+
exports.getStaticBuildOutputPath = getStaticBuildOutputPath;
|
|
158
158
|
const getRegistryBuildOutputPath = (handoff) => path_1.default.resolve(handoff.workingPath, handoff.sitesDirectory, 'registry');
|
|
159
159
|
exports.getRegistryBuildOutputPath = getRegistryBuildOutputPath;
|
|
160
|
+
const commonAncestorPath = (paths) => {
|
|
161
|
+
const [first, ...rest] = paths.map((entry) => path_1.default.resolve(entry).split(path_1.default.sep));
|
|
162
|
+
let shared = first;
|
|
163
|
+
for (const candidate of rest) {
|
|
164
|
+
const next = [];
|
|
165
|
+
for (let index = 0; index < Math.min(shared.length, candidate.length) && shared[index] === candidate[index]; index += 1) {
|
|
166
|
+
next.push(shared[index]);
|
|
167
|
+
}
|
|
168
|
+
shared = next;
|
|
169
|
+
}
|
|
170
|
+
return shared.join(path_1.default.sep) || path_1.default.sep;
|
|
171
|
+
};
|
|
172
|
+
/**
|
|
173
|
+
* Root for Next's file tracing. Every traced path in the standalone bundle is relative to this root.
|
|
174
|
+
*
|
|
175
|
+
* The tracer copies only the files inside this root. It must contain the staged app (see
|
|
176
|
+
* {@link getAppPath}), or Next emits no standalone bundle. It must also contain every
|
|
177
|
+
* `node_modules` that the required runtime dependencies resolve from, or the bundle cannot start
|
|
178
|
+
* (see {@link assertRegistryRuntimeDepsTraced}).
|
|
179
|
+
*
|
|
180
|
+
* `workingPath` satisfies both conditions only when handoff-app is installed in the same package as
|
|
181
|
+
* the config, and nothing is hoisted above it.
|
|
182
|
+
*/
|
|
183
|
+
const resolveRegistryTracingRoot = (handoff) => {
|
|
184
|
+
const anchors = [path_1.default.resolve(handoff.workingPath), path_1.default.resolve(handoff.modulePath)];
|
|
185
|
+
// Resolve each module instead of assuming its location. A hoisted or linked dependency otherwise
|
|
186
|
+
// drops out of the trace.
|
|
187
|
+
for (const moduleName of getRequiredRegistryRuntimeModules(handoff)) {
|
|
188
|
+
try {
|
|
189
|
+
const resolved = require.resolve(`${moduleName}/package.json`, { paths: [handoff.modulePath, handoff.workingPath] });
|
|
190
|
+
anchors.push(fs_extra_1.default.realpathSync(path_1.default.dirname(resolved)));
|
|
191
|
+
}
|
|
192
|
+
catch (_a) {
|
|
193
|
+
// The assertion after assembly reports a module that does not resolve.
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return commonAncestorPath(anchors);
|
|
197
|
+
};
|
|
198
|
+
exports.resolveRegistryTracingRoot = resolveRegistryTracingRoot;
|
|
160
199
|
/**
|
|
161
200
|
* Performs cleanup of the application directory by removing the existing app directory if it exists.
|
|
162
201
|
*/
|
|
@@ -173,7 +212,7 @@ const cleanupAppDirectory = (handoff) => __awaiter(void 0, void 0, void 0, funct
|
|
|
173
212
|
* @returns The path to the prepared application directory
|
|
174
213
|
*/
|
|
175
214
|
const initializeProjectApp = (handoff_1, ...args_1) => __awaiter(void 0, [handoff_1, ...args_1], void 0, function* (handoff, options = {}) {
|
|
176
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
215
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
|
|
177
216
|
const includeWorkspaceArtifacts = (_a = options.includeWorkspaceArtifacts) !== null && _a !== void 0 ? _a : true;
|
|
178
217
|
const runtimeMode = (_e = (_b = options.runtimeModeOverride) !== null && _b !== void 0 ? _b : (_d = (_c = handoff.config) === null || _c === void 0 ? void 0 : _c.runtime) === null || _d === void 0 ? void 0 : _d.mode) !== null && _e !== void 0 ? _e : 'workspace';
|
|
179
218
|
const srcPath = path_1.default.resolve(handoff.modulePath, 'src', 'app');
|
|
@@ -204,7 +243,7 @@ const initializeProjectApp = (handoff_1, ...args_1) => __awaiter(void 0, [handof
|
|
|
204
243
|
const handoffAppBasePath = (_f = handoff.config.app.base_path) !== null && _f !== void 0 ? _f : '';
|
|
205
244
|
const handoffWorkingPath = path_1.default.resolve(handoff.workingPath);
|
|
206
245
|
const handoffModulePath = path_1.default.resolve(handoff.modulePath);
|
|
207
|
-
const handoffExportPath =
|
|
246
|
+
const handoffExportPath = handoff.getOutputPath();
|
|
208
247
|
const nextConfigPath = path_1.default.resolve(srcPath, 'next.config.mjs');
|
|
209
248
|
const targetPath = path_1.default.resolve(appPath, 'next.config.mjs');
|
|
210
249
|
const handoffWebsocketPort = (_h = (_g = handoff.config.app.ports) === null || _g === void 0 ? void 0 : _g.websocket) !== null && _h !== void 0 ? _h : 3001;
|
|
@@ -212,6 +251,8 @@ const initializeProjectApp = (handoff_1, ...args_1) => __awaiter(void 0, [handof
|
|
|
212
251
|
const escapedProjectId = escapeForSingleQuotedJsString(handoffProjectId);
|
|
213
252
|
const escapedWorkingPath = escapeForSingleQuotedJsString(handoffWorkingPath);
|
|
214
253
|
const escapedModulePath = escapeForSingleQuotedJsString(handoffModulePath);
|
|
254
|
+
// Baked here so the template and the assembly step use the same root.
|
|
255
|
+
const escapedTracingRoot = escapeForSingleQuotedJsString((0, exports.resolveRegistryTracingRoot)(handoff));
|
|
215
256
|
const escapedExportPath = escapeForSingleQuotedJsString(handoffExportPath);
|
|
216
257
|
const escapedWebsocketPort = escapeForSingleQuotedJsString(String(handoffWebsocketPort));
|
|
217
258
|
// Resolved runtime mode + registry connection inputs (names only) baked into the Next bundle so the
|
|
@@ -220,29 +261,47 @@ const initializeProjectApp = (handoff_1, ...args_1) => __awaiter(void 0, [handof
|
|
|
220
261
|
const escapedMcpEnabled = escapeForSingleQuotedJsString(String((0, config_1.isMcpEnabled)(handoff.config)));
|
|
221
262
|
const escapedRegistryDriver = escapeForSingleQuotedJsString((0, driver_1.resolveRegistryDriver)(handoff.config));
|
|
222
263
|
const escapedDatabaseUrlEnv = escapeForSingleQuotedJsString((0, driver_1.resolveDatabaseUrlEnv)(handoff.config));
|
|
223
|
-
// Asset storage selection baked (provider + module +
|
|
264
|
+
// Asset storage selection baked (provider + module + options JSON with `{ $env }` references).
|
|
224
265
|
const assetStorage = (0, resolve_1.resolveAssetStorageFromConfig)(handoff.config);
|
|
225
|
-
const
|
|
266
|
+
const escapedAssetStorageProvider = escapeForSingleQuotedJsString(assetStorage.provider);
|
|
226
267
|
const escapedAssetStorageModule = escapeForSingleQuotedJsString((_j = assetStorage.module) !== null && _j !== void 0 ? _j : '');
|
|
227
|
-
const escapedAssetStorageTokenEnv = escapeForSingleQuotedJsString(assetStorage.tokenEnv);
|
|
228
268
|
const escapedAssetStorageMaxInline = escapeForSingleQuotedJsString(String(assetStorage.maxInlineBytes));
|
|
229
269
|
const escapedAssetStorageOptions = escapeForSingleQuotedJsString(JSON.stringify((_k = assetStorage.options) !== null && _k !== void 0 ? _k : {}));
|
|
270
|
+
const email = (0, resolve_2.resolveEmailFromConfig)(handoff.config);
|
|
271
|
+
const escapedEmailProvider = escapeForSingleQuotedJsString((_l = email.provider) !== null && _l !== void 0 ? _l : '');
|
|
272
|
+
const escapedEmailFrom = escapeForSingleQuotedJsString((_m = email.from) !== null && _m !== void 0 ? _m : '');
|
|
273
|
+
const escapedEmailModule = escapeForSingleQuotedJsString((_o = email.module) !== null && _o !== void 0 ? _o : '');
|
|
274
|
+
const escapedEmailOptions = escapeForSingleQuotedJsString(JSON.stringify(email.options));
|
|
275
|
+
// AI assistant selection baked (enabled flag + declared connections + default model). Connections
|
|
276
|
+
// carry env-var names and non-secret options only; keys are read from their named env var at
|
|
277
|
+
// request time, and a deployment can extend or replace the list through HANDOFF_AI_CONNECTIONS.
|
|
278
|
+
const ai = (0, connections_1.resolveAiFromConfig)(handoff.config);
|
|
279
|
+
const escapedAiEnabled = escapeForSingleQuotedJsString(String(ai.enabled));
|
|
280
|
+
const escapedAiConnections = escapeForSingleQuotedJsString(JSON.stringify(ai.connections));
|
|
281
|
+
const escapedAiDefaultModel = escapeForSingleQuotedJsString((_p = ai.defaultModel) !== null && _p !== void 0 ? _p : '');
|
|
230
282
|
const placeholderValues = {
|
|
231
283
|
'%HANDOFF_PROJECT_ID%': escapedProjectId,
|
|
232
284
|
'%HANDOFF_APP_BASE_PATH%': escapedAppBasePath,
|
|
233
285
|
'%HANDOFF_WORKING_PATH%': escapedWorkingPath,
|
|
234
286
|
'%HANDOFF_MODULE_PATH%': escapedModulePath,
|
|
287
|
+
'%HANDOFF_TRACING_ROOT%': escapedTracingRoot,
|
|
235
288
|
'%HANDOFF_EXPORT_PATH%': escapedExportPath,
|
|
236
289
|
'%HANDOFF_WEBSOCKET_PORT%': escapedWebsocketPort,
|
|
237
290
|
'%HANDOFF_RUNTIME_MODE%': escapedRuntimeMode,
|
|
238
291
|
'%HANDOFF_MCP_ENABLED%': escapedMcpEnabled,
|
|
239
292
|
'%HANDOFF_REGISTRY_DRIVER%': escapedRegistryDriver,
|
|
240
293
|
'%HANDOFF_REGISTRY_DATABASE_URL_ENV%': escapedDatabaseUrlEnv,
|
|
241
|
-
'%
|
|
294
|
+
'%HANDOFF_ASSET_STORAGE_PROVIDER%': escapedAssetStorageProvider,
|
|
242
295
|
'%HANDOFF_ASSET_STORAGE_MODULE%': escapedAssetStorageModule,
|
|
243
|
-
'%HANDOFF_ASSET_STORAGE_TOKEN_ENV%': escapedAssetStorageTokenEnv,
|
|
244
296
|
'%HANDOFF_ASSET_STORAGE_MAX_INLINE_BYTES%': escapedAssetStorageMaxInline,
|
|
245
297
|
'%HANDOFF_ASSET_STORAGE_OPTIONS%': escapedAssetStorageOptions,
|
|
298
|
+
'%HANDOFF_EMAIL_PROVIDER%': escapedEmailProvider,
|
|
299
|
+
'%HANDOFF_EMAIL_FROM%': escapedEmailFrom,
|
|
300
|
+
'%HANDOFF_EMAIL_MODULE%': escapedEmailModule,
|
|
301
|
+
'%HANDOFF_EMAIL_OPTIONS%': escapedEmailOptions,
|
|
302
|
+
'%HANDOFF_AI_ENABLED%': escapedAiEnabled,
|
|
303
|
+
'%HANDOFF_AI_BAKED_CONNECTIONS%': escapedAiConnections,
|
|
304
|
+
'%HANDOFF_AI_DEFAULT_MODEL%': escapedAiDefaultModel,
|
|
246
305
|
};
|
|
247
306
|
let nextConfigContent = yield fs_extra_1.default.readFile(nextConfigPath, 'utf-8');
|
|
248
307
|
for (const [placeholder, value] of Object.entries(placeholderValues)) {
|
|
@@ -293,20 +352,13 @@ const buildApp = (handoff_1, ...args_1) => __awaiter(void 0, [handoff_1, ...args
|
|
|
293
352
|
yield (0, static_export_1.materializeDocsReadModel)(handoff, exportDir);
|
|
294
353
|
// Final assembly branches on the resolved package. `vercel` lays the materialized export
|
|
295
354
|
// under `.vercel/output/static/` at the repo root (not the sites directory) — a hard Vercel
|
|
296
|
-
// constraint; `standalone` keeps writing the `out
|
|
355
|
+
// constraint; `standalone` keeps writing the `out/static` export.
|
|
297
356
|
if (resolvedPackage === 'vercel') {
|
|
298
357
|
yield (0, vercel_output_1.writeStaticVercelOutput)(handoff, exportDir);
|
|
299
358
|
return;
|
|
300
359
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
yield fs_extra_1.default.ensureDir(outputRoot);
|
|
304
|
-
// Clean the project output directory (if exists)
|
|
305
|
-
const output = path_1.default.resolve(outputRoot, handoff.getProjectId());
|
|
306
|
-
if (fs_extra_1.default.existsSync(output)) {
|
|
307
|
-
yield fs_extra_1.default.remove(output);
|
|
308
|
-
}
|
|
309
|
-
// Copy the build files into the project output directory
|
|
360
|
+
const output = (0, exports.getStaticBuildOutputPath)(handoff);
|
|
361
|
+
yield fs_extra_1.default.remove(output);
|
|
310
362
|
yield fs_extra_1.default.copy(exportDir, output);
|
|
311
363
|
});
|
|
312
364
|
/**
|
|
@@ -337,7 +389,28 @@ const findStandaloneServerDir = (root) => {
|
|
|
337
389
|
* Write a deployment guide next to the packaged registry app describing how to run it on Vercel,
|
|
338
390
|
* a custom Node server, or in a container, and which env vars it requires at deploy time.
|
|
339
391
|
*/
|
|
340
|
-
const writeRegistryDeploymentReadme = (outputRoot, entryRelativePath, databaseUrlEnv) => __awaiter(void 0, void 0, void 0, function* () {
|
|
392
|
+
const writeRegistryDeploymentReadme = (outputRoot, entryRelativePath, databaseUrlEnv, ai, email, assetStorage) => __awaiter(void 0, void 0, void 0, function* () {
|
|
393
|
+
const serviceKeyEnvs = [...new Set(ai.connections.map((connection) => connection.apiKeyEnv).filter(Boolean))];
|
|
394
|
+
const aiOptionEnvs = [...new Set(ai.connections.flatMap((connection) => { var _a; return (0, from_env_1.envReferenceNames)((_a = connection.options) !== null && _a !== void 0 ? _a : {}); }))];
|
|
395
|
+
const needsKeySecret = ai.connections.some((connection) => connection.credential === 'user');
|
|
396
|
+
const aiSection = !ai.enabled
|
|
397
|
+
? ''
|
|
398
|
+
: `
|
|
399
|
+
## AI assistant
|
|
400
|
+
|
|
401
|
+
This build serves the docs assistant at \`/api/ai/*\`.
|
|
402
|
+
|
|
403
|
+
${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.'}
|
|
404
|
+
${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' : ''}
|
|
405
|
+
\`HANDOFF_AI_CONNECTIONS\` optionally carries a JSON array of connections, read at request time and
|
|
406
|
+
merged over the list this build baked in, keyed by \`id\`. Use it to add or repoint a provider
|
|
407
|
+
without rebuilding.
|
|
408
|
+
`;
|
|
409
|
+
const assetStorageEnvs = assetStorage.provider === 'custom' ? (0, from_env_1.envReferenceNames)(assetStorage.options) : [];
|
|
410
|
+
const emailEnvs = (0, from_env_1.envReferenceNames)(email.options);
|
|
411
|
+
const emailSection = !email.from
|
|
412
|
+
? 'Email delivery is off because `runtime.registry.email.from` is not set. Invitation links are shown once to an administrator for manual delivery.'
|
|
413
|
+
: `Invitation and password-reset emails are sent from \`${email.from}\` through the \`${email.provider}\` provider.${emailEnvs.length ? ` The provider reads ${emailEnvs.map((name) => `\`${name}\``).join(', ')} at request time.` : ''} Until the provider's settings have values, invitation links are shown once to an administrator for manual delivery.`;
|
|
341
414
|
const readme = `# Handoff Registry App
|
|
342
415
|
|
|
343
416
|
A self-contained, deployable Next.js **standalone** build produced by
|
|
@@ -354,10 +427,9 @@ and \`.next/static/\` already copied alongside so the server serves them.
|
|
|
354
427
|
- \`${databaseUrlEnv}\` — PostgreSQL/Neon connection string, read at request time.
|
|
355
428
|
- \`AUTH_SECRET\` — a long, random secret used to sign browser sessions.
|
|
356
429
|
- \`AUTH_URL\` — the canonical public registry URL, including the configured base path.
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
430
|
+
${assetStorageEnvs.map((name) => `- \`${name}\` — read by the custom asset storage provider at request time.\n`).join('')}
|
|
431
|
+
${emailSection}
|
|
432
|
+
${aiSection}
|
|
361
433
|
## Database migrations
|
|
362
434
|
|
|
363
435
|
Apply migrations from the \`handoff-app\` CLI as a controlled release step (e.g. in your CI/CD
|
|
@@ -426,8 +498,8 @@ For multi-instance self-hosting, front \`/_next/static\` with a CDN and configur
|
|
|
426
498
|
|
|
427
499
|
## Deploying to Vercel
|
|
428
500
|
|
|
429
|
-
Vercel does **not** use \`output: 'standalone'\` — it ignores that setting and
|
|
430
|
-
|
|
501
|
+
Vercel does **not** use \`output: 'standalone'\` — it ignores that setting and runs its
|
|
502
|
+
own build, so this pre-built bundle is **not** the artifact you deploy there. For Vercel, deploy the
|
|
431
503
|
registry app via Vercel's Git/CLI source build (it produces and serves the dynamic app itself) and
|
|
432
504
|
supply the same environment variables above as project env vars. Use this standalone bundle for
|
|
433
505
|
containers, custom Node servers, and other non-Vercel hosts.
|
|
@@ -438,30 +510,25 @@ containers, custom Node servers, and other non-Vercel hosts.
|
|
|
438
510
|
* Resolve the set of npm packages the packaged registry app must be able to `require`/`import` at
|
|
439
511
|
* runtime, given the configured database driver. `next`/`react`/`react-dom` run the server and React
|
|
440
512
|
* runtime; `drizzle-orm` backs both the request-time DB client and the migration runner; the MCP SDK
|
|
441
|
-
*
|
|
442
|
-
* driver-specific (the Neon serverless driver also needs `ws` for its Node WebSocket transport)
|
|
513
|
+
* is only needed when `/api/mcp/` is enabled; the driver package is
|
|
514
|
+
* driver-specific (the Neon serverless driver also needs `ws` for its Node WebSocket transport); the
|
|
515
|
+
* SMTP email provider needs `nodemailer`.
|
|
443
516
|
*/
|
|
444
517
|
const getRequiredRegistryRuntimeModules = (handoff) => {
|
|
445
|
-
var _a;
|
|
446
518
|
const base = ['next', 'next-auth', 'react', 'react-dom', 'drizzle-orm'];
|
|
447
|
-
|
|
448
|
-
|
|
519
|
+
const ai = (0, connections_1.resolveAiFromConfig)(handoff.config);
|
|
520
|
+
// The assistant runs the MCP tools in-process, so it needs the MCP SDK whether or not the HTTP
|
|
521
|
+
// endpoint is served.
|
|
522
|
+
if ((0, config_1.isMcpEnabled)(handoff.config) || ai.enabled) {
|
|
523
|
+
base.push('@modelcontextprotocol/sdk');
|
|
524
|
+
}
|
|
525
|
+
if (ai.enabled) {
|
|
526
|
+
base.push('ai', '@ai-sdk/openai-compatible');
|
|
449
527
|
}
|
|
450
528
|
const driver = (0, driver_1.resolveRegistryDriver)(handoff.config);
|
|
451
529
|
const driverModules = driver === 'neon' ? ['@neondatabase/serverless', 'ws'] : ['pg'];
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
// package(s) via `assetStorage.options.sdkModules` so the trace/assertion covers them.
|
|
455
|
-
const assetStorage = (0, resolve_1.resolveAssetStorageFromConfig)(handoff.config);
|
|
456
|
-
const storage = [];
|
|
457
|
-
if (assetStorage.adapterKind === 'vercel-blob') {
|
|
458
|
-
storage.push('@vercel/blob');
|
|
459
|
-
}
|
|
460
|
-
const sdkModules = (_a = assetStorage.options) === null || _a === void 0 ? void 0 : _a.sdkModules;
|
|
461
|
-
if (Array.isArray(sdkModules)) {
|
|
462
|
-
storage.push(...sdkModules.filter((mod) => typeof mod === 'string'));
|
|
463
|
-
}
|
|
464
|
-
return [...base, ...driverModules, ...storage];
|
|
530
|
+
const email = (0, resolve_2.resolveEmailFromConfig)(handoff.config).provider === 'smtp' ? ['nodemailer'] : [];
|
|
531
|
+
return [...base, ...driverModules, ...email];
|
|
465
532
|
};
|
|
466
533
|
/**
|
|
467
534
|
* Build-time guard against the empty-`node_modules` regression (the artifact would otherwise boot-fail
|
|
@@ -488,6 +555,58 @@ const assertRegistryRuntimeDepsTraced = (handoff, entryDir) => {
|
|
|
488
555
|
`must be the nearest ancestor of *both* the staged app and the resolved node_modules. Ensure ` +
|
|
489
556
|
`${missing.join(', ')} are installed and resolvable from "${handoff.workingPath}".`);
|
|
490
557
|
};
|
|
558
|
+
const getCustomServerModules = (handoff) => {
|
|
559
|
+
const assetStorage = (0, resolve_1.resolveAssetStorageFromConfig)(handoff.config);
|
|
560
|
+
const email = (0, resolve_2.resolveEmailFromConfig)(handoff.config);
|
|
561
|
+
const modules = [
|
|
562
|
+
assetStorage.provider === 'custom' ? assetStorage.module : undefined,
|
|
563
|
+
email.provider === 'custom' ? email.module : undefined,
|
|
564
|
+
...(0, connections_1.resolveAiFromConfig)(handoff.config).connections.map((connection) => connection.module),
|
|
565
|
+
];
|
|
566
|
+
return [...new Set(modules.filter((module) => Boolean(module)))];
|
|
567
|
+
};
|
|
568
|
+
/**
|
|
569
|
+
* Copy the custom server modules and every file they import into the bundle. The Next trace cannot
|
|
570
|
+
* follow their variable dynamic import.
|
|
571
|
+
*/
|
|
572
|
+
const copyCustomServerModules = (handoff, tracingRoot, entryDir) => __awaiter(void 0, void 0, void 0, function* () {
|
|
573
|
+
const modules = getCustomServerModules(handoff);
|
|
574
|
+
if (modules.length === 0)
|
|
575
|
+
return;
|
|
576
|
+
const missing = modules.filter((module) => !fs_extra_1.default.existsSync(path_1.default.resolve(handoff.workingPath, module)));
|
|
577
|
+
if (missing.length > 0) {
|
|
578
|
+
throw new HandoffBuildError(`A custom server module does not exist: ${missing.map((module) => `"${module}"`).join(', ')}.`);
|
|
579
|
+
}
|
|
580
|
+
const files = new Set();
|
|
581
|
+
for (const module of modules) {
|
|
582
|
+
const { fileList } = yield (0, nft_1.nodeFileTrace)([path_1.default.resolve(handoff.workingPath, module)], { base: tracingRoot });
|
|
583
|
+
const outside = [...fileList].filter((file) => file.startsWith('..'));
|
|
584
|
+
if (outside.length > 0) {
|
|
585
|
+
throw new HandoffBuildError(`The custom server module "${module}" imports files outside the build's tracing root "${tracingRoot}": ${outside.join(', ')}. ` +
|
|
586
|
+
`Install the packages it imports so that they resolve from "${handoff.workingPath}".`);
|
|
587
|
+
}
|
|
588
|
+
if ([...fileList].some((file) => file.split(path_1.default.sep).join('/').endsWith('node_modules/handoff-app/dist/index.js'))) {
|
|
589
|
+
throw new HandoffBuildError(`The custom server module "${module}" imports "handoff-app", which loads the whole CLI. Import the define helpers from "handoff-app/providers".`);
|
|
590
|
+
}
|
|
591
|
+
fileList.forEach((file) => files.add(file));
|
|
592
|
+
}
|
|
593
|
+
for (const file of files) {
|
|
594
|
+
const source = path_1.default.join(tracingRoot, file);
|
|
595
|
+
const target = path_1.default.join(entryDir, file);
|
|
596
|
+
if (yield fs_extra_1.default.lstat(target).then(() => true, () => false))
|
|
597
|
+
continue;
|
|
598
|
+
const stat = yield fs_extra_1.default.lstat(source);
|
|
599
|
+
yield fs_extra_1.default.ensureDir(path_1.default.dirname(target));
|
|
600
|
+
if (stat.isSymbolicLink()) {
|
|
601
|
+
// Point the link at the bundle copy of its target, not at the build machine.
|
|
602
|
+
const linked = path_1.default.join(entryDir, path_1.default.relative(tracingRoot, yield fs_extra_1.default.realpath(source)));
|
|
603
|
+
yield fs_extra_1.default.symlink(path_1.default.relative(path_1.default.dirname(target), linked), target);
|
|
604
|
+
}
|
|
605
|
+
else if (stat.isFile()) {
|
|
606
|
+
yield fs_extra_1.default.copy(source, target);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
});
|
|
491
610
|
/**
|
|
492
611
|
* Lay the Next `standalone` build out as the documented standalone layout under `entryDir`: copy the
|
|
493
612
|
* traced bundle, flatten the package-rooted server entry up to `entryDir`, and copy the untraced
|
|
@@ -499,14 +618,11 @@ const assertRegistryRuntimeDepsTraced = (handoff, entryDir) => {
|
|
|
499
618
|
*/
|
|
500
619
|
const assembleRegistryStandalone = (handoff, appPath, standaloneRoot, entryDir) => __awaiter(void 0, void 0, void 0, function* () {
|
|
501
620
|
yield fs_extra_1.default.copy(standaloneRoot, entryDir, { overwrite: true });
|
|
502
|
-
// Locate the server entry
|
|
503
|
-
//
|
|
504
|
-
//
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
// skips `node_modules` (so it cannot find this nested entry) and only covers a hypothetical
|
|
508
|
-
// non-nested layout.
|
|
509
|
-
const relAppDir = path_1.default.relative(handoff.workingPath, appPath);
|
|
621
|
+
// Locate the server entry in the assembled package. Traced files keep their path relative to the
|
|
622
|
+
// tracing root, so the server entry nests under the traced `node_modules`. The recursive fallback
|
|
623
|
+
// skips `node_modules`, so it covers only a non-nested layout.
|
|
624
|
+
const tracingRoot = (0, exports.resolveRegistryTracingRoot)(handoff);
|
|
625
|
+
const relAppDir = path_1.default.relative(tracingRoot, appPath);
|
|
510
626
|
let serverDir = path_1.default.resolve(entryDir, relAppDir);
|
|
511
627
|
if (!fs_extra_1.default.existsSync(path_1.default.join(serverDir, 'server.js'))) {
|
|
512
628
|
const located = findStandaloneServerDir(entryDir);
|
|
@@ -526,7 +642,7 @@ const assembleRegistryStandalone = (handoff, appPath, standaloneRoot, entryDir)
|
|
|
526
642
|
// so we must NOT remove the first path segment (`node_modules`) — that would delete every traced
|
|
527
643
|
// dependency. Remove only the `.handoff` staging container (the parent of the staged app dir),
|
|
528
644
|
// leaving `node_modules/` and the real `node_modules/handoff-app/` package intact.
|
|
529
|
-
const stagingContainer = path_1.default.resolve(entryDir, path_1.default.relative(
|
|
645
|
+
const stagingContainer = path_1.default.resolve(entryDir, path_1.default.relative(tracingRoot, path_1.default.dirname(appPath)));
|
|
530
646
|
if (path_1.default.basename(stagingContainer) === '.handoff' && fs_extra_1.default.existsSync(stagingContainer)) {
|
|
531
647
|
yield fs_extra_1.default.remove(stagingContainer);
|
|
532
648
|
}
|
|
@@ -549,6 +665,17 @@ const assembleRegistryStandalone = (handoff, appPath, standaloneRoot, entryDir)
|
|
|
549
665
|
yield fs_extra_1.default.copy(src, path_1.default.join(entryDir, configFile), { overwrite: true });
|
|
550
666
|
}
|
|
551
667
|
}
|
|
668
|
+
// Next copies the `package.json` of the tracing root into the bundle without change. The generated
|
|
669
|
+
// `server.js` is CommonJS, so an ESM manifest stops the bundle from starting. That manifest also
|
|
670
|
+
// names scripts and dev dependencies that the bundle does not ship.
|
|
671
|
+
const manifestPath = path_1.default.join(entryDir, 'package.json');
|
|
672
|
+
const inheritedManifest = (yield fs_extra_1.default.pathExists(manifestPath))
|
|
673
|
+
? yield fs_extra_1.default.readJson(manifestPath).catch(() => ({}))
|
|
674
|
+
: {};
|
|
675
|
+
delete inheritedManifest.scripts;
|
|
676
|
+
delete inheritedManifest.devDependencies;
|
|
677
|
+
yield fs_extra_1.default.writeJson(manifestPath, Object.assign(Object.assign({}, inheritedManifest), { type: 'commonjs' }), { spaces: 2 });
|
|
678
|
+
yield copyCustomServerModules(handoff, tracingRoot, entryDir);
|
|
552
679
|
// Fail loudly at build time if the trace did not capture the runtime deps, rather than shipping a
|
|
553
680
|
// broken artifact that only crashes on an isolated deploy. Covers both deliverables (this is the
|
|
554
681
|
// shared assembly step for `standalone` and `vercel`).
|
|
@@ -618,8 +745,10 @@ const buildRegistryApp = (handoff_1, ...args_1) => __awaiter(void 0, [handoff_1,
|
|
|
618
745
|
// files are not traced and must be copied alongside the server entry.
|
|
619
746
|
const standaloneRoot = path_1.default.resolve(appPath, '.next', 'standalone');
|
|
620
747
|
if (!fs_extra_1.default.existsSync(standaloneRoot)) {
|
|
621
|
-
throw new Error(`Registry build did not produce a standalone bundle at "${standaloneRoot}".
|
|
622
|
-
|
|
748
|
+
throw new Error(`Registry build did not produce a standalone bundle at "${standaloneRoot}". Next emits one only ` +
|
|
749
|
+
`when the app being built sits inside \`outputFileTracingRoot\` (resolved here to ` +
|
|
750
|
+
`"${(0, exports.resolveRegistryTracingRoot)(handoff)}"), so check that the staged app at "${appPath}" is ` +
|
|
751
|
+
'inside that root, and that the registry build target still sets Next `output: "standalone"`.');
|
|
623
752
|
}
|
|
624
753
|
// Final assembly branches on the resolved package. Both lay out
|
|
625
754
|
// the same traced standalone bundle; they differ only in where it lands and how it is wrapped.
|
|
@@ -636,7 +765,7 @@ const buildRegistryApp = (handoff_1, ...args_1) => __awaiter(void 0, [handoff_1,
|
|
|
636
765
|
// `handoff-app` source/CLI (`handoff-app db:migrate`) as a controlled release step (e.g. CI/CD),
|
|
637
766
|
// never from the deployed bundle. See the generated README for the migrate-then-deploy flow.
|
|
638
767
|
const entryRelativePath = path_1.default.relative(output, serverEntry).split(path_1.default.sep).join('/');
|
|
639
|
-
yield writeRegistryDeploymentReadme(output, entryRelativePath, (0, driver_1.resolveDatabaseUrlEnv)(handoff.config));
|
|
768
|
+
yield writeRegistryDeploymentReadme(output, entryRelativePath, (0, driver_1.resolveDatabaseUrlEnv)(handoff.config), (0, connections_1.resolveAiFromConfig)(handoff.config), (0, resolve_2.resolveEmailFromConfig)(handoff.config), (0, resolve_1.resolveAssetStorageFromConfig)(handoff.config));
|
|
640
769
|
logger_1.Logger.success(`Packaged registry app at ${output} (start: \`node ${entryRelativePath}\`, migrate: \`handoff-app db:migrate\`).`);
|
|
641
770
|
});
|
|
642
771
|
/**
|
|
@@ -646,11 +775,11 @@ const buildRegistryApp = (handoff_1, ...args_1) => __awaiter(void 0, [handoff_1,
|
|
|
646
775
|
const watchApp = (handoff) => __awaiter(void 0, void 0, void 0, function* () {
|
|
647
776
|
var _a, _b, _c, _d;
|
|
648
777
|
// Build the shared/global artifacts first so component/pattern preview HTML references them only
|
|
649
|
-
// when present, then process components
|
|
650
|
-
//
|
|
778
|
+
// when present, then process components. The build cache skips the components whose files did not
|
|
779
|
+
// change.
|
|
651
780
|
yield (0, javascript_1.buildMainJS)(handoff);
|
|
652
781
|
yield (0, css_1.buildMainCss)(handoff);
|
|
653
|
-
yield (0, builder_1.default)(handoff
|
|
782
|
+
yield (0, builder_1.default)(handoff);
|
|
654
783
|
// Build patterns after components are ready
|
|
655
784
|
yield (0, patterns_1.buildPatterns)(handoff);
|
|
656
785
|
const appPath = yield initializeProjectApp(handoff);
|
|
@@ -19,6 +19,7 @@ const config_1 = require("../config");
|
|
|
19
19
|
const resolve_1 = require("../registry/asset-storage/resolve");
|
|
20
20
|
const connection_1 = require("../registry/connection");
|
|
21
21
|
const driver_1 = require("../registry/db/driver");
|
|
22
|
+
const resolve_2 = require("../registry/email/resolve");
|
|
22
23
|
const paths_1 = require("./paths");
|
|
23
24
|
/**
|
|
24
25
|
* Publishes the tokens API files to the public directory.
|
|
@@ -71,12 +72,12 @@ const buildServerRuntimeConfig = (config, modeOverride) => {
|
|
|
71
72
|
databaseUrlEnv: (0, driver_1.resolveDatabaseUrlEnv)(config),
|
|
72
73
|
},
|
|
73
74
|
assetStorage: {
|
|
74
|
-
|
|
75
|
+
provider: assetStorage.provider,
|
|
75
76
|
module: assetStorage.module,
|
|
76
|
-
tokenEnv: assetStorage.tokenEnv,
|
|
77
77
|
maxInlineBytes: assetStorage.maxInlineBytes,
|
|
78
78
|
options: assetStorage.options,
|
|
79
79
|
},
|
|
80
|
+
email: (0, resolve_2.resolveEmailFromConfig)(config),
|
|
80
81
|
};
|
|
81
82
|
};
|
|
82
83
|
/**
|
|
@@ -1,17 +1,5 @@
|
|
|
1
1
|
import Handoff from '..';
|
|
2
|
-
/**
|
|
3
|
-
* Gets the working public directory path for a given handoff instance.
|
|
4
|
-
* Checks for both project-specific and default public directories.
|
|
5
|
-
*
|
|
6
|
-
* @param handoff - The handoff instance containing working path and figma project configuration
|
|
7
|
-
* @returns The resolved path to the public directory if it exists, null otherwise
|
|
8
|
-
*/
|
|
9
2
|
export declare const getWorkingPublicPath: (handoff: Handoff) => string | null;
|
|
10
|
-
/**
|
|
11
|
-
* Gets the application path for a given handoff instance.
|
|
12
|
-
* @param handoff - The handoff instance containing module path and figma project configuration
|
|
13
|
-
* @returns The resolved path to the application directory
|
|
14
|
-
*/
|
|
15
3
|
export declare const getAppPath: (handoff: Handoff) => string;
|
|
16
4
|
/**
|
|
17
5
|
* Copy the public dir from the working dir to the module dir.
|
|
@@ -15,28 +15,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
15
15
|
exports.syncPublicFiles = exports.getAppPath = exports.getWorkingPublicPath = void 0;
|
|
16
16
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
17
17
|
const path_1 = __importDefault(require("path"));
|
|
18
|
-
/**
|
|
19
|
-
* Gets the working public directory path for a given handoff instance.
|
|
20
|
-
* Checks for both project-specific and default public directories.
|
|
21
|
-
*
|
|
22
|
-
* @param handoff - The handoff instance containing working path and figma project configuration
|
|
23
|
-
* @returns The resolved path to the public directory if it exists, null otherwise
|
|
24
|
-
*/
|
|
25
18
|
const getWorkingPublicPath = (handoff) => {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
if (fs_extra_1.default.existsSync(p)) {
|
|
29
|
-
return p;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return null;
|
|
19
|
+
const publicPath = path_1.default.resolve(handoff.workingPath, 'public');
|
|
20
|
+
return fs_extra_1.default.existsSync(publicPath) ? publicPath : null;
|
|
33
21
|
};
|
|
34
22
|
exports.getWorkingPublicPath = getWorkingPublicPath;
|
|
35
|
-
/**
|
|
36
|
-
* Gets the application path for a given handoff instance.
|
|
37
|
-
* @param handoff - The handoff instance containing module path and figma project configuration
|
|
38
|
-
* @returns The resolved path to the application directory
|
|
39
|
-
*/
|
|
40
23
|
const getAppPath = (handoff) => {
|
|
41
24
|
return path_1.default.resolve(handoff.modulePath, '.handoff', `${handoff.getProjectId()}`);
|
|
42
25
|
};
|
|
@@ -51,7 +34,7 @@ const mirrorDirectory = (sourcePath, destinationPath) => __awaiter(void 0, void
|
|
|
51
34
|
yield fs_extra_1.default.copy(sourcePath, destinationPath, { overwrite: true });
|
|
52
35
|
});
|
|
53
36
|
/**
|
|
54
|
-
* Copy the fetch-produced asset bundles (`exported
|
|
37
|
+
* Copy the fetch-produced asset bundles (`exported/{collection}.zip`) into the app's public
|
|
55
38
|
* root, where the docs read API serves them at `/api/docs/assets/{collection}/{collection}.zip`.
|
|
56
39
|
* `fetch` writes these bundles, but `build`/`start` cleans and re-stages `.handoff` without them, so
|
|
57
40
|
* the download links 404 until the next fetch. Copying them here keeps the route working in workspace
|
|
@@ -184,7 +184,7 @@ exports.materializeDocsReadModel = materializeDocsReadModel;
|
|
|
184
184
|
* Write the whole-collection asset bundles to the asset route
|
|
185
185
|
* (`api/docs/assets/{collection}/{collection}.zip`), the same URL the docs read API serves in
|
|
186
186
|
* registry mode, so the "Download" links work identically on a static host. The bundles come from the
|
|
187
|
-
* fetch output (`exported
|
|
187
|
+
* fetch output (`exported/{collection}.zip`); `next build` never re-creates them and the
|
|
188
188
|
* fetch-time copy into `public/` is wiped by the build's clean+restage, so we copy them here again.
|
|
189
189
|
* Projects with no logos/icons have no bundles, so those are skipped rather than treated as an error.
|
|
190
190
|
*/
|
|
@@ -7,7 +7,7 @@ export declare const getVercelOutputPath: (handoff: Handoff) => string;
|
|
|
7
7
|
* no functions, no database.
|
|
8
8
|
*
|
|
9
9
|
* @param exportDir Absolute path of the materialized static export (the same content the static
|
|
10
|
-
* target writes to `out
|
|
10
|
+
* target writes to `out/static`).
|
|
11
11
|
*/
|
|
12
12
|
export declare const writeStaticVercelOutput: (handoff: Handoff, exportDir: string) => Promise<void>;
|
|
13
13
|
/**
|