1/**
2 * `@alchemy.run/frontend-frameworks/solidstart/node` — the Node container
3 * deploy target for the SolidStart integration.
4 *
5 * SolidStart'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 * - **`bundle`** — Node resolve conditions (no `workerd`, no `@aws-sdk/`).
15 *
16 * Do not use the `aws-lambda` preset: that emits a Lambda `handler`, not a
17 * Node listener.
18 */
19import * as Effect from "effect/Effect";
20import * as Path from "effect/Path";
21import { runBuildChild } from "../core/BuildChild.ts";
22import {
23 NODE_BUNDLE_CONDITIONS,
24 NODE_SERVE_ENTRY_FILE_NAME,
25 relativeClientDirExpression,
26 writeNodeServeEntry,
27} from "../core/NodeServe.ts";
28import { DeployTargetError, makeDeployTarget } from "../core/index.ts";
29import {
30 make,
31 type SolidStartTarget,
32 type SolidStartTargetConfig,
33} from "./SolidStart.ts";
34
35/** The nitro deployment preset this target builds with. */
36export const NITRO_PRESET = "node";
37
38/**
39 * The importable specifier of nitro's node-listener runtime handler — the
40 * module a USER entry re-exports to wrap the framework's handler.
41 */
42export const NITRO_HANDLER_SPECIFIER =
43 "nitropack/presets/node/runtime/node-listener";
44
45export interface SolidStartNodeTargetConfig extends SolidStartTargetConfig {}
46
47const fail = (message: string, cause?: unknown) =>
48 new DeployTargetError({ platform: "node", message, cause });
49
50const makeNodeAdapterTarget = (
51 config: SolidStartNodeTargetConfig = {},
52): SolidStartTarget =>
53 makeDeployTarget({
54 platform: "node",
55 config,
56 bundle: {
57 conditions: [...NODE_BUNDLE_CONDITIONS],
58 },
59 nitroPreset: NITRO_PRESET,
60 finish: (output) =>
61 Effect.gen(function* () {
62 const path = yield* Path.Path;
63 if (
64 output.distDirectory === undefined ||
65 output.clientDirectory === undefined
66 ) {
67 return yield* Effect.fail(
68 fail(
69 "The SolidStart build produced no .output directories for the Node serve entry",
70 ),
71 );
72 }
73 const servePath = path.join(
74 output.distDirectory,
75 "server",
76 NODE_SERVE_ENTRY_FILE_NAME,
77 );
78 return yield* writeNodeServeEntry({
79 output,
80 servePath,
81 serveModuleName: path
82 .join("server", NODE_SERVE_ENTRY_FILE_NAME)
83 .replaceAll("\\", "/"),
84 clientDirExpression: relativeClientDirExpression(
85 servePath,
86 output.clientDirectory,
87 ),
88 handler: {
89 kind: "node",
90 imports: `import { handler } from "./index.mjs";`,
91 expr: "handler",
92 },
93 platform: "node",
94 });
95 }),
96 });
97
98export interface SolidStartNodeBuildChildConfig {
99 readonly rootDir: string;
100 readonly config: SolidStartNodeTargetConfig;
101}
102
103export const buildInChild = (config: SolidStartNodeBuildChildConfig) =>
104 Effect.gen(function* () {
105 const framework = yield* make({
106 root: config.rootDir,
107 target: makeNodeAdapterTarget(config.config),
108 nitro: config.config.nitro,
109 });
110 return yield* framework.build({ root: config.rootDir });
111 });
112
113export const makeNodeTarget = (
114 config: SolidStartNodeTargetConfig = {},
115): SolidStartTarget => ({
116 ...makeNodeAdapterTarget(config),
117 build: (context) =>
118 runBuildChild({
119 module: import.meta.url,
120 rootDir: context.root,
121 env: context.env,
122 framework: "solidstart",
123 config: {
124 rootDir: context.root,
125 config,
126 } satisfies SolidStartNodeBuildChildConfig,
127 }).pipe(Effect.mapError((error) => fail(error.message, error.cause))),
128});
129
130export const target = makeNodeTarget;
131
132export default makeNodeTarget;