1/**
2 * `@alchemy.run/frontend-frameworks/vite/node` — the Node container deploy
3 * target for the plain Vite integration.
4 *
5 * A plain Vite build is assets-only (client bundle + `index.html`). The
6 * finishing pass writes a Node HTTP program (`serve-node.mjs`) into
7 * `clientDirectory` that serves those files, answers `GET /health`, and
8 * applies SPA / 404-page fallback. Platform composites deploy that file
9 * as-is (`isExternal`) — they do not generate a second static server.
10 *
11 * The wholesale `build` hook runs the project's `vite build` in a
12 * disposable child process whose working directory IS the project root
13 * (see `core/BuildChild.ts`).
14 */
15import * as Effect from "effect/Effect";
16import * as Path from "effect/Path";
17import { runBuildChild } from "../core/BuildChild.ts";
18import {
19 NODE_BUNDLE_CONDITIONS,
20 NODE_SERVE_ENTRY_FILE_NAME,
21 writeNodeServeEntry,
22} from "../core/NodeServe.ts";
23import { DeployTargetError, makeDeployTarget } from "../core/index.ts";
24import { make, type ViteTarget, type ViteTargetConfig } from "./Vite.ts";
25
26const fail = (message: string, cause?: unknown) =>
27 new DeployTargetError({ platform: "node", message, cause });
28
29/**
30 * The in-child target: no wholesale `build` hook, so the child runs the
31 * regular vite-build pipeline (no recursion) then this finish.
32 */
33const makeNodeChildTarget = (config: ViteTargetConfig = {}): ViteTarget =>
34 makeDeployTarget({
35 platform: "node",
36 config,
37 bundle: {
38 conditions: [...NODE_BUNDLE_CONDITIONS],
39 },
40 finish: (output) =>
41 Effect.gen(function* () {
42 const path = yield* Path.Path;
43 if (output.clientDirectory === undefined) {
44 return yield* Effect.fail(
45 fail(
46 "The Vite build produced no client directory for the Node serve entry",
47 ),
48 );
49 }
50 const servePath = path.join(
51 output.clientDirectory,
52 NODE_SERVE_ENTRY_FILE_NAME,
53 );
54 return yield* writeNodeServeEntry({
55 output,
56 servePath,
57 serveModuleName: NODE_SERVE_ENTRY_FILE_NAME,
58 clientDirExpression: `fileURLToPath(new URL("./", import.meta.url))`,
59 notFoundHandling: config.notFoundHandling ?? "spa",
60 htmlHandling: config.htmlHandling ?? "none",
61 platform: "node",
62 });
63 }),
64 });
65
66/** The runner's JSON payload for the build child (see `core/BuildChild.ts`). */
67export interface ViteNodeBuildChildConfig {
68 readonly rootDir: string;
69 /** The (JSON-serializable) target config the parent was created with. */
70 readonly config: ViteTargetConfig;
71}
72
73/** The pure in-child build entry the shared `BuildChildRunner` invokes. */
74export const buildInChild = (config: ViteNodeBuildChildConfig) =>
75 Effect.gen(function* () {
76 const framework = yield* make({
77 root: config.rootDir,
78 target: makeNodeChildTarget(config.config),
79 vite: config.config.vite,
80 });
81 return yield* framework.build({ root: config.rootDir });
82 });
83
84/**
85 * Create the Node {@link ViteTarget}: the assets-only target whose `build`
86 * spawns the vite build in a child process.
87 */
88export const makeNodeTarget = (config: ViteTargetConfig = {}): ViteTarget => ({
89 ...makeNodeChildTarget(config),
90 build: (context) =>
91 runBuildChild({
92 module: import.meta.url,
93 rootDir: context.root,
94 env: context.env,
95 framework: "vite",
96 config: {
97 rootDir: context.root,
98 config,
99 } satisfies ViteNodeBuildChildConfig,
100 }).pipe(Effect.mapError((error) => fail(error.message, error.cause))),
101});
102
103/**
104 * The deploy-target module contract (`resolveDeployTarget` accepts the
105 * default export — or the named `target` export — as a value or factory).
106 */
107export const target = makeNodeTarget;
108
109export default makeNodeTarget;