1/**
2 * Shared child-process ENTRY for framework dev servers.
3 *
4 * Executed only as a spawned process by `runDevChild` (`DevChild.ts`) —
5 * never imported. `argv[2]` carries the JSON {@link DevChildPayload}: the
6 * framework-integration module specifier, the options for its `make(...)`,
7 * and the dev options. The process's working directory IS the project root
8 * (the parent spawns it with `cwd = rootDir`), so toolchains that resolve
9 * from the cwd — or `process.chdir` around startup — are isolated here and
10 * never touch the process hosting other sites' dev servers.
11 *
12 * The parent sets `ALCHEMY_FRAMEWORK_DEV_CHILD=1` in this process's env, so
13 * the integration's `dev` takes its in-process path instead of recursing
14 * into another child.
15 *
16 * The runner prints the dev server's URL wrapped in the readiness marker,
17 * then serves until SIGTERM/SIGINT aborts the main fiber (closing the scope
18 * that owns the dev server).
19 */
20import * as NodeServices from "@effect/platform-node/NodeServices";
21import * as Cause from "effect/Cause";
22import * as Effect from "effect/Effect";
23import * as Exit from "effect/Exit";
24import type * as FileSystem from "effect/FileSystem";
25import type * as Path from "effect/Path";
26import type * as Scope from "effect/Scope";
27import { devChildUrlMarker, type DevChildPayload } from "./DevChild.ts";
28import type { FrameworkDevServer } from "./Framework.ts";
29
30/** The structural slice of a framework-integration module the runner drives. */
31interface FrameworkModule {
32 readonly make: (options: Record<string, unknown>) => Effect.Effect<
33 {
34 readonly dev: (options: {
35 readonly root?: string;
36 readonly port?: number;
37 readonly host?: string;
38 }) => Effect.Effect<FrameworkDevServer, unknown, Scope.Scope>;
39 },
40 unknown,
41 FileSystem.FileSystem | Path.Path
42 >;
43}
44
45const payload = JSON.parse(process.argv[2] ?? "{}") as DevChildPayload;
46
47const program = Effect.scoped(
48 Effect.gen(function* () {
49 const module = (yield* Effect.promise(
50 () => import(payload.module),
51 )) as Partial<FrameworkModule>;
52 if (typeof module.make !== "function") {
53 return yield* Effect.die(
54 new Error(
55 `Dev child module ${payload.module} does not export a make function`,
56 ),
57 );
58 }
59 const service = yield* module.make(payload.makeOptions);
60 const { url } = yield* service.dev(payload.devOptions);
61 // Through the real stdout: the parent scans lines for this marker.
62 process.stdout.write(`${devChildUrlMarker(url)}\n`);
63 // Serve until the parent stops the process; the abort below interrupts
64 // this fiber and the enclosing scope closes the dev server.
65 return yield* Effect.never;
66 }),
67).pipe(Effect.provide(NodeServices.layer));
68
69const controller = new AbortController();
70process.once("SIGTERM", () => controller.abort());
71process.once("SIGINT", () => controller.abort());
72
73void Effect.runPromiseExit(program, { signal: controller.signal }).then(
74 (exit) => {
75 const code = Exit.match(exit, {
76 onSuccess: () => 0,
77 onFailure: (cause) => {
78 if (Cause.hasInterrupts(cause)) {
79 return 0;
80 }
81 console.error(cause);
82 return 1;
83 },
84 });
85 // The macrotask hop lets buffered stdout/stderr drain before exiting.
86 setTimeout(() => process.exit(code), 0);
87 },
88);