Skip to content
Source
Sign inSign up

nodeRuntime.ts

92 lines3.7 KB

1import * as Effect from "effect/Effect";
2import * as FileSystem from "effect/FileSystem";
3import * as Path from "effect/Path";
4import * as Option from "effect/Option";
5import * as Schema from "effect/Schema";
6​
7import * as HostProcess from "./HostProcess.ts";
8import { CommandResolutionCache, resolveCommandPath } from "./shell.ts";
9​
10const NodeRuntimeFeature = Schema.Literals([
11 "Local device support",
12 "Device automation",
13 "Antigravity",
14 "Antigravity sign-in",
15]);
16​
17export const nodeRuntimeUnavailableMessage = (feature: typeof NodeRuntimeFeature.Type): string =>
18 `${feature} requires Node.js. Install Node.js and make sure node is on PATH, then retry.`;
19​
20export class NodeRuntimeUnavailableError extends Schema.TaggedError<NodeRuntimeUnavailableError>()(
21 "NodeRuntimeUnavailableError",
22 { feature: NodeRuntimeFeature, cause: Schema.optional(Schema.Defect()) },
23) {
24 override get message(): string {
25 return nodeRuntimeUnavailableMessage(this.feature);
26 }
28​
29export interface SelfInvocation {
30 readonly command: string;
31 /** Absolute script entrypoint; undefined for packaged binaries, which dispatch subcommands from argv[2]. */
32 readonly entrypoint: string | undefined;
34​
35/** Packaged binaries repeat the binary at argv[1]; callers must not assemble execPath/argv[1] themselves. */
36export const resolveSelfInvocation = Effect.fn("nodeRuntime.resolveSelfInvocation")(function* () {
37 const command = yield* HostProcess.ExecutablePath;
38 if (yield* HostProcess.IsExecutable)
39 return { command, entrypoint: undefined } satisfies SelfInvocation;
40 const path = yield* Path.Path;
41 const entry = (yield* HostProcess.Arguments)[1];
42 // Children spawn from their own working directory, so the script path must be absolute.
43 return {
44 command,
45 entrypoint: entry === undefined ? undefined : path.resolve(entry),
46 } satisfies SelfInvocation;
47});
48​
49export const selfInvocationArgs = (
50 invocation: SelfInvocation,
51 args: ReadonlyArray<string>,
52): ReadonlyArray<string> =>
53 invocation.entrypoint === undefined ? args : [invocation.entrypoint, ...args];
54​
55export const resolveNodeExecutable = Effect.fn("nodeRuntime.resolveNodeExecutable")(function* (
56 feature: typeof NodeRuntimeFeature.Type,
57 environment?: NodeJS.ProcessEnv,
58) {
59 const executablePath = yield* HostProcess.ExecutablePath;
60 if (!(yield* HostProcess.IsExecutable)) return executablePath;
61​
62 const fs = yield* FileSystem.FileSystem;
63 const path = yield* Path.Path;
64 const platform = yield* HostProcess.Platform;
65 const env = environment ?? (yield* HostProcess.Environment);
66 const resolvedNodePath = yield* resolveCommandPath(platform === "win32" ? "node.exe" : "node", {
67 env: platform === "win32" ? { ...env, PATHEXT: ".EXE" } : env,
68 }).pipe(Effect.provideService(CommandResolutionCache, new Map()));
69 if (resolvedNodePath === null) return yield* new NodeRuntimeUnavailableError({ feature });
70 const nodePath = path.resolve(resolvedNodePath);
71 const resolvedPath = yield* fs
72 .realPath(nodePath)
73 .pipe(Effect.mapError((cause) => new NodeRuntimeUnavailableError({ feature, cause })));
74 if (resolvedPath === executablePath) return yield* new NodeRuntimeUnavailableError({ feature });
75 const [hostInfo, nodeInfo] = yield* Effect.all([
76 fs.stat(executablePath).pipe(Effect.option),
77 fs.stat(nodePath).pipe(Effect.option),
78 ]);
79 if (
80 Option.isSome(hostInfo) &&
81 Option.isSome(nodeInfo) &&
82 hostInfo.value.dev === nodeInfo.value.dev &&
83 Option.isSome(hostInfo.value.ino) &&
84 Option.isSome(nodeInfo.value.ino) &&
85 hostInfo.value.ino.value > 0n &&
86 hostInfo.value.ino.value === nodeInfo.value.ino.value
87 ) {
88 return yield* new NodeRuntimeUnavailableError({ feature });
89 }
90 // Launchers such as Vite+ dispatch by argv[0]; keep the node name intact.
91 return nodePath;
92});