1/**
2 * `@alchemy.run/frontend-frameworks/nuxt/node` — the Node container deploy
3 * target for the Nuxt integration.
4 *
5 * Nuxt's Node story is nitro's `node` (node-listener) preset:
6 * `.output/server` is a self-contained Node ESM bundle whose entry exports
7 * a Node `(req, res)` `handler`, and `.output/public` holds the static
8 * assets. The finishing pass writes a Node HTTP program that serves
9 * `clientDirectory` first, then falls through to that handler on `PORT`
10 * (default 3000), and answers `GET /health`.
11 *
12 * - **`nitroPreset`** — `"node"`, enforced as the last word on the
13 * resolved nitro config. A user `nitro.preset` mismatch fails the build.
14 * - **`entry`** — surfaces `config.main` as the generic user-entry carriage.
15 * - **`bundle`** — Node resolve conditions (no `workerd`, no `@aws-sdk/`).
16 */
17import * as Effect from "effect/Effect";
18import * as Path from "effect/Path";
19import { runBuildChild } from "../core/BuildChild.ts";
20import {
21 NODE_BUNDLE_CONDITIONS,
22 NODE_SERVE_ENTRY_FILE_NAME,
23 relativeClientDirExpression,
24 writeNodeServeEntry,
25} from "../core/NodeServe.ts";
26import { DeployTargetError, makeDeployTarget } from "../core/index.ts";
27import { make, type NuxtTarget, type NuxtTargetConfig } from "./Nuxt.ts";
28
29/** The nitro deployment preset this target builds with. */
30export const NITRO_PRESET = "node";
31
32/**
33 * The importable specifier of nitro's node-listener runtime handler — the
34 * module a USER entry re-exports to wrap the framework's handler.
35 */
36export const NITRO_HANDLER_SPECIFIER =
37 "nitropack/presets/node/runtime/node-listener";
38
39export interface NuxtNodeTargetConfig extends NuxtTargetConfig {}
40
41const fail = (message: string, cause?: unknown) =>
42 new DeployTargetError({ platform: "node", message, cause });
43
44const makeNodeAdapterTarget = (config: NuxtNodeTargetConfig = {}): NuxtTarget =>
45 makeDeployTarget({
46 platform: "node",
47 config,
48 bundle: {
49 conditions: [...NODE_BUNDLE_CONDITIONS],
50 },
51 entry: config.main !== undefined ? { main: config.main } : undefined,
52 nitroPreset: NITRO_PRESET,
53 configureNitro: (_nitroConfig, _context) => {
54 // The user entry (context.entry) is deliberately NOT wired here —
55 // the framework package sets it on the nitro INSTANCE at `nitro:init`
56 // (a config-level entry would leak into the prerenderer's Node-preset
57 // clone). See the matching note in ./cloudflare.ts.
58 },
59 finish: (output) =>
60 Effect.gen(function* () {
61 const path = yield* Path.Path;
62 if (
63 output.distDirectory === undefined ||
64 output.clientDirectory === undefined
65 ) {
66 return yield* Effect.fail(
67 fail(
68 "The Nuxt build produced no .output directories for the Node serve entry",
69 ),
70 );
71 }
72 const servePath = path.join(
73 output.distDirectory,
74 "server",
75 NODE_SERVE_ENTRY_FILE_NAME,
76 );
77 return yield* writeNodeServeEntry({
78 output,
79 servePath,
80 serveModuleName: path
81 .join("server", NODE_SERVE_ENTRY_FILE_NAME)
82 .replaceAll("\\", "/"),
83 clientDirExpression: relativeClientDirExpression(
84 servePath,
85 output.clientDirectory,
86 ),
87 handler: {
88 kind: "node",
89 imports: `import { handler } from "./index.mjs";`,
90 expr: "handler",
91 },
92 platform: "node",
93 });
94 }),
95 });
96
97export interface NuxtNodeBuildChildConfig {
98 readonly rootDir: string;
99 readonly config: NuxtNodeTargetConfig;
100}
101
102export const buildInChild = (config: NuxtNodeBuildChildConfig) =>
103 Effect.gen(function* () {
104 const framework = yield* make({
105 root: config.rootDir,
106 target: makeNodeAdapterTarget(config.config),
107 compatibilityDate: config.config.compatibilityDate,
108 compatibilityFlags: config.config.compatibilityFlags,
109 main: config.config.main,
110 nuxt: config.config.nuxt,
111 });
112 return yield* framework.build({ root: config.rootDir });
113 });
114
115export const makeNodeTarget = (
116 config: NuxtNodeTargetConfig = {},
117): NuxtTarget => ({
118 ...makeNodeAdapterTarget(config),
119 build: (context) =>
120 runBuildChild({
121 module: import.meta.url,
122 rootDir: context.root,
123 env: context.env,
124 framework: "nuxt",
125 config: {
126 rootDir: context.root,
127 config,
128 } satisfies NuxtNodeBuildChildConfig,
129 }).pipe(Effect.mapError((error) => fail(error.message, error.cause))),
130});
131
132export const target = makeNodeTarget;
133
134export default makeNodeTarget;