1/**
2 * `@alchemy.run/frontend-frameworks/waku/node` — the Node container deploy
3 * target for the Waku integration.
4 *
5 * Waku's Node story is a plain-Node waku build with waku's own `node`
6 * adapter selected via `unstable_adapter`. The finishing pass writes a
7 * Node HTTP program that serves `clientDirectory` first (extensionless
8 * HTML: `/about` → `about/index.html`), then falls through to
9 * `INTERNAL_runFetch` on `PORT` (default 3000), and answers `GET /health`.
10 *
11 * - **`adapter`** — the project's `waku/adapters/node`.
12 * - **`vitePlugins`** — none (plain Node; no workerd plugin).
13 * - **`bundle`** — Node resolve conditions (no `workerd`, no `@aws-sdk/`).
14 */
15import * as Effect from "effect/Effect";
16import * as FileSystem from "effect/FileSystem";
17import * as Path from "effect/Path";
18import * as NodePath from "node:path";
19import type { Config as WakuConfig } from "waku/config";
20import { runBuildChild } from "../core/BuildChild.ts";
21import {
22 NODE_BUNDLE_CONDITIONS,
23 NODE_SERVE_ENTRY_FILE_NAME,
24 relativeClientDirExpression,
25 writeNodeServeEntry,
26} from "../core/NodeServe.ts";
27import { DeployTargetError, makeDeployTarget } from "../core/index.ts";
28import { make, type WakuTarget, type WakuTargetBuildContext } from "./Waku.ts";
29
30export type { WakuTarget, WakuTargetContext } from "./Waku.ts";
31
32export interface WakuNodeTargetConfig {
33 readonly main?: string | undefined;
34}
35
36const fail = (message: string) => (cause?: unknown) =>
37 new DeployTargetError({ platform: "node", message, cause });
38
39const makeNodeAdapterTarget = (
40 config?: WakuNodeTargetConfig,
41): WakuTarget<WakuNodeTargetConfig | undefined> =>
42 makeDeployTarget({
43 platform: "node",
44 config,
45 entry: config?.main !== undefined ? { main: config.main } : undefined,
46 bundle: {
47 conditions: [...NODE_BUNDLE_CONDITIONS],
48 },
49 adapter: (context) =>
50 Effect.succeed(
51 NodePath.join(context.wakuDirectory, "dist/adapters/node.js"),
52 ),
53 vitePlugins: () => Effect.sync(() => []),
54 finish: (output, _context) =>
55 Effect.gen(function* () {
56 const fs = yield* FileSystem.FileSystem;
57 const path = yield* Path.Path;
58 if (output.distDirectory === undefined) {
59 return yield* Effect.fail(
60 fail("The waku build produced no dist directory")(undefined),
61 );
62 }
63 if (output.clientDirectory === undefined) {
64 return yield* Effect.fail(
65 fail(
66 "The waku build produced no client directory for the Node serve entry",
67 )(undefined),
68 );
69 }
70 const serverDir = path.join(output.distDirectory, "server");
71 const serverIndex = path.join(serverDir, "index.js");
72 const hasServerIndex = yield* fs
73 .exists(serverIndex)
74 .pipe(
75 Effect.mapError(fail("Failed to probe the built server entry")),
76 );
77 if (!hasServerIndex) {
78 return yield* Effect.fail(
79 fail(`The waku build produced no server entry at ${serverIndex}`)(
80 undefined,
81 ),
82 );
83 }
84 yield* fs
85 .writeFileString(
86 path.join(serverDir, "package.json"),
87 `${JSON.stringify({ type: "module" }, null, 2)}\n`,
88 )
89 .pipe(
90 Effect.mapError(fail("Failed to write dist/server/package.json")),
91 );
92 const servePath = path.join(serverDir, NODE_SERVE_ENTRY_FILE_NAME);
93 return yield* writeNodeServeEntry({
94 output,
95 servePath,
96 serveModuleName: path
97 .join("server", NODE_SERVE_ENTRY_FILE_NAME)
98 .replaceAll("\\", "/"),
99 clientDirExpression: relativeClientDirExpression(
100 servePath,
101 output.clientDirectory,
102 ),
103 handler: {
104 kind: "fetch",
105 imports: `import { INTERNAL_runFetch } from "./index.js";`,
106 expr: "(req, ...args) => INTERNAL_runFetch(process.env, req, ...args)",
107 },
108 htmlHandling: "drop-trailing-slash",
109 platform: "node",
110 });
111 }),
112 });
113
114export interface WakuNodeBuildChildConfig {
115 readonly rootDir: string;
116 readonly config: WakuNodeTargetConfig | undefined;
117 readonly waku: WakuConfig | undefined;
118}
119
120export const buildInChild = (config: WakuNodeBuildChildConfig) =>
121 Effect.gen(function* () {
122 const framework = yield* make({
123 root: config.rootDir,
124 target: makeNodeAdapterTarget(config.config),
125 waku: config.waku,
126 });
127 return yield* framework.build({ root: config.rootDir });
128 });
129
130export const makeNodeTarget = (
131 config?: WakuNodeTargetConfig,
132): WakuTarget<WakuNodeTargetConfig | undefined> => ({
133 ...makeNodeAdapterTarget(config),
134 build: (context: WakuTargetBuildContext) =>
135 runBuildChild({
136 runtime: "node",
137 module: import.meta.url,
138 rootDir: context.root,
139 env: context.env,
140 framework: "waku",
141 config: {
142 rootDir: context.root,
143 config,
144 waku: context.waku,
145 } satisfies WakuNodeBuildChildConfig,
146 }).pipe(Effect.mapError((error) => fail(error.message)(error.cause))),
147});
148
149export const target = makeNodeTarget;
150
151export default makeNodeTarget;