mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-03 07:54:40 +08:00
Three shapes from pi's process/child helpers: 1. A cleanup arrow such as `() => process.off(signal, handler)` sits in @types/node's `Process`-returning slot. The lambda's ABI was decided from its inferred return type before the contextual void slot was consulted, so the arrow promised a `Process` value the lowering cannot produce and the registration fenced. `lambdaSignature` now takes the contextual void answer first; the adoption block further down already computes it, so every program that compiles today keeps its IR. 2. `child.off`/`removeListener` were unimplemented. They join the `child.on` family, are limited to the removable literals `spawn`, `exit`, `close` and `error`, and require the listener value that was registered — removal is by closure identity, scanning the registry from the end as Node does. New C runtime entry points `scr_child_off_spawn/_exit/_close/_error` release the entry and shift the tail. 3. `child.signalCode` reads `string | null` from the recorded exit signal, with `scr_child_has_signal_code`/`scr_child_signal_code` behind the same typed-union wrap as `spawnRes.signal`; the IR validator pins the `string | null` result. The process listener registration paths (stdin events, IPC message and disconnect, uncaughtException and uncaughtExceptionMonitor, unhandledRejection, warning, signals, exit) now accept any discarded expression, matching `child.on` — `resultIsDiscarded` rather than a bare expression statement — so `handlers.push(() => process.off(…))` compiles. Evidence: corpus 8412 (removal through `off` and `removeListener` for all four events, `signalCode` while running, after a normal exit, after a fatal signal and after a failed spawn, idempotent double removal) and the `node-types` fixture plus project-config test that pins pi's exact `() => process.off(...)` cleanup closure. Ambient declarations gain the `signalCode` property and the four `off`/`removeListener` overloads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
243 lines
12 KiB
TypeScript
243 lines
12 KiB
TypeScript
/* node:child_process — the synchronous slice: spawnSync(command, args?)
|
|
* runs the child to completion (posix_spawn + waitpid) with stdout/stderr
|
|
* captured as utf8 strings. `status` is the exit code, or null when the
|
|
* child died to a signal OR could not be spawned at all (nonexistent
|
|
* binary, permission denied) — Node reports spawn failure through the
|
|
* result's `error` property, which this surface does not carry, and types
|
|
* stdout/stderr null there where scriptc returns "" (SEMANTICS.md).
|
|
* No shell, no options object: the command runs directly (PATH-searched,
|
|
* like Node). */
|
|
declare module "child_process" {
|
|
export interface SpawnSyncReturns {
|
|
readonly status: number | null;
|
|
readonly stdout: string;
|
|
readonly stderr: string;
|
|
/* The termination signal's name ("SIGTERM", ...) when a signal
|
|
* killed the child (a timeout's killSignal included), null otherwise
|
|
* — Node's shape. */
|
|
readonly signal: string | null;
|
|
/* Node's spawn-failure carrier: a real Error ("spawnSync <file>
|
|
* ENOENT", `code` stamped) when the spawn itself failed — status is
|
|
* null and both outputs "" then — or ETIMEDOUT after a timeout kill;
|
|
* undefined otherwise. */
|
|
readonly error?: Error;
|
|
}
|
|
/* scriptc always captures utf8 strings; pass { encoding: "utf8" }
|
|
* whenever stdout/stderr is READ so plain Node agrees (without it Node
|
|
* hands Buffers). The other supported options: timeout (ms — sends
|
|
* killSignal at the deadline and the result carries error: ETIMEDOUT +
|
|
* the signal, never a throw), killSignal (a signal NAME literal),
|
|
* stdio ("ignore"/"inherit"/"pipe" or a 3-tuple of those — captures
|
|
* read "" for non-piped streams), env (replaces the child's
|
|
* environment), and windowsHide (a POSIX no-op). */
|
|
export function spawnSync(
|
|
command: string,
|
|
args?: string[],
|
|
options?: {
|
|
encoding?: "utf8" | "utf-8";
|
|
timeout?: number;
|
|
killSignal?: string;
|
|
stdio?: "pipe" | "ignore" | "inherit" | ("pipe" | "ignore" | "inherit")[];
|
|
env?: { [k: string]: string | undefined };
|
|
windowsHide?: boolean;
|
|
},
|
|
): SpawnSyncReturns;
|
|
|
|
/* The asynchronous slice: spawn with stdio "ignore"/"inherit"/fd/"pipe"
|
|
* tuples (piped stdin is writable through child.stdin; piped stdout/stderr
|
|
* deliver through child.stdout/stderr) and the terminal events. "exit"
|
|
* fires once with the exit code, or
|
|
* null when the child died to a signal; "error" fires ONLY when the
|
|
* child could not be spawned at all (Node's split: a spawn failure
|
|
* emits "error" and never "exit"). "close" fires after exit/error and
|
|
* every piped stdio handle reaches EOF. Exit/close listeners may take
|
|
* Node's code and signal parameters. `on`/`once` return void here (Node
|
|
* returns the child; chaining is fenced). The event loop keeps the process
|
|
* alive until every spawned child is reaped, like Node — reaping polls at
|
|
* loop quiescence (SEMANTICS.md documents the divergence). An "error"
|
|
* event with no registered listener prints the error and exits 1,
|
|
* exactly the unhandled-'error' EventEmitter behavior. */
|
|
export interface ChildStdin {
|
|
readonly writable: boolean;
|
|
write(chunk: string | Uint8Array): boolean;
|
|
end(): void;
|
|
destroy(): void;
|
|
on(event: "drain" | "finish", listener: () => void): void;
|
|
on(event: "error", listener: (err: Error) => void): void;
|
|
on(event: string, listener: (...args: any[]) => void): void;
|
|
once(event: "drain" | "finish", listener: () => void): void;
|
|
once(event: "error", listener: (err: Error) => void): void;
|
|
once(event: string, listener: (...args: any[]) => void): void;
|
|
}
|
|
|
|
export interface ChildProcess extends Disposable {
|
|
ref(): void;
|
|
on(event: "spawn", listener: () => void): this;
|
|
once(event: "spawn", listener: () => void): this;
|
|
/* The exit listener may also take Node's second parameter — the
|
|
* terminating signal's name, null for a normal exit. */
|
|
on(event: "exit", listener: (code: number | null, signal: string | null) => void): this;
|
|
on(event: "close", listener: (code: number | null, signal: string | null) => void): this;
|
|
on(event: "error", listener: (err: Error) => void): this;
|
|
once(event: "exit", listener: (code: number | null, signal: string | null) => void): this;
|
|
once(event: "close", listener: (code: number | null, signal: string | null) => void): this;
|
|
once(event: "error", listener: (err: Error) => void): this;
|
|
on(event: "message", listener: (message: any) => void): this;
|
|
on(event: "disconnect", listener: () => void): this;
|
|
once(event: "message", listener: (message: any) => void): this;
|
|
once(event: "disconnect", listener: () => void): this;
|
|
/* off/removeListener remove by FUNCTION IDENTITY — pass the same
|
|
* function value (a const binding) that was registered. */
|
|
off(event: "spawn", listener: () => void): this;
|
|
off(event: "exit", listener: (code: number | null, signal: string | null) => void): this;
|
|
off(event: "close", listener: (code: number | null, signal: string | null) => void): this;
|
|
off(event: "error", listener: (err: Error) => void): this;
|
|
removeListener(event: "spawn", listener: () => void): this;
|
|
removeListener(
|
|
event: "exit",
|
|
listener: (code: number | null, signal: string | null) => void,
|
|
): this;
|
|
removeListener(
|
|
event: "close",
|
|
listener: (code: number | null, signal: string | null) => void,
|
|
): this;
|
|
removeListener(event: "error", listener: (err: Error) => void): this;
|
|
/* The lifecycle members, Node's exact shapes: pid is undefined exactly
|
|
* when the spawn failed; exitCode is null while running, the code
|
|
* after a normal exit, null for a signal death, and -errno once a
|
|
* spawn failure's "error" event has fired; kill resolves signal names
|
|
* through Node's table (unknown names throw the Unknown signal
|
|
* TypeError), answers true when the signal was sent, false once the
|
|
* child was reaped (or never spawned), and sets `killed` on success;
|
|
* unref() drops the child from the event loop's keep-alive set (it is
|
|
* still reaped while the loop runs for other reasons — SEMANTICS.md
|
|
* has the reap story). */
|
|
readonly pid: number | undefined;
|
|
readonly exitCode: number | null;
|
|
/* signalCode is the signal that terminated the child, null while it
|
|
* runs, after a normal exit, and after a spawn failure. */
|
|
readonly signalCode: string | null;
|
|
readonly killed: boolean;
|
|
kill(signal?: string | number): boolean;
|
|
unref(): void;
|
|
readonly connected: boolean;
|
|
send(message: object, callback?: (error: Error | null) => void): boolean;
|
|
disconnect(): void;
|
|
[Symbol.dispose](): void;
|
|
/* The piped streams — non-null exactly when the matching stdio slot
|
|
* was "pipe" (Node's shape). */
|
|
readonly stdin: ChildStdin | null;
|
|
readonly stdout: NodeJS.ReadableStream | null;
|
|
readonly stderr: NodeJS.ReadableStream | null;
|
|
}
|
|
export interface SpawnOptions {
|
|
/* The 3-tuple form admits number fds in the stdout/stderr slots —
|
|
* openSync results dup2'd into the child (the daemon-log idiom
|
|
* ["ignore", logFd, logFd]) — and "pipe" there too (child.stdout/
|
|
* child.stderr streams); "pipe" in the stdin slot exposes
|
|
* child.stdin. */
|
|
stdio?: "ignore" | "inherit" | "pipe" | ("ignore" | "inherit" | "pipe" | number)[];
|
|
/* detached gives the child its own session and process group
|
|
* (POSIX_SPAWN_SETSID); env REPLACES the child environment; cwd
|
|
* sets its working directory; windowsHide is a POSIX no-op; shell
|
|
* runs the complete command string through /bin/sh or cmd.exe. */
|
|
detached?: boolean;
|
|
env?: { [k: string]: string | undefined };
|
|
cwd?: string;
|
|
windowsHide?: boolean;
|
|
shell?: boolean;
|
|
}
|
|
export function spawn(command: string, args?: string[], options?: SpawnOptions): ChildProcess;
|
|
export function spawn(command: string, options?: SpawnOptions): ChildProcess;
|
|
|
|
export function fork(
|
|
modulePath: string | URL,
|
|
args?: string[],
|
|
options?: {
|
|
cwd?: string;
|
|
env?: { [k: string]: string | undefined };
|
|
execArgv?: string[];
|
|
silent?: boolean;
|
|
stdio?: "pipe" | "ignore" | "inherit" | ("pipe" | "ignore" | "inherit" | "ipc")[];
|
|
serialization?: "json" | "advanced";
|
|
windowsHide?: boolean;
|
|
},
|
|
): ChildProcess;
|
|
|
|
/* The synchronous exec pair: execFileSync runs a file directly (no
|
|
* shell), execSync runs a command through /bin/sh -c. Both capture
|
|
* stdout as a utf8 string (pass { encoding: "utf8" } so plain Node
|
|
* agrees — without it Node returns a Buffer) and THROW on a non-zero
|
|
* exit / signal death — the Error's message is Node's "Command failed:
|
|
* <cmd>" (with the captured stderr appended). Supported options:
|
|
* encoding ("utf8"), cwd, env (REPLACES the child environment), input
|
|
* (fed to stdin), timeout (ms — SIGTERMs the child, then throws
|
|
* ETIMEDOUT), stdio ("pipe"/"ignore" or a 3-tuple of those — the
|
|
* default captures stderr AND echoes it to the parent, like Node), and
|
|
* maxBuffer (accepted, not enforced). Node's status/stdout/stderr error
|
|
* properties are not carried (SEMANTICS.md). */
|
|
interface ExecSyncOptions {
|
|
encoding?: "utf8" | "utf-8";
|
|
cwd?: string;
|
|
env?: { [k: string]: string | undefined };
|
|
input?: string;
|
|
timeout?: number;
|
|
stdio?: "pipe" | "ignore" | "inherit" | ("pipe" | "ignore" | "inherit")[];
|
|
maxBuffer?: number;
|
|
killSignal?: string;
|
|
windowsHide?: boolean;
|
|
shell?: boolean;
|
|
}
|
|
/* The named options interface for the string-encoding exec form —
|
|
* @types/node's ExecFileSyncOptionsWithStringEncoding, narrowed to the
|
|
* honestly-implemented members. VALUES of this type are a real record
|
|
* (type-mapper.ts interns the shape), so a typed const or a runner function's
|
|
* options parameter flows to execFileSync at runtime — the windows-ca
|
|
* command-runner idiom. */
|
|
export interface ExecFileSyncOptionsWithStringEncoding {
|
|
encoding: "utf8" | "utf-8";
|
|
cwd?: string;
|
|
input?: string;
|
|
timeout?: number;
|
|
stdio?: "pipe" | "ignore" | "inherit" | ("pipe" | "ignore" | "inherit")[];
|
|
maxBuffer?: number;
|
|
windowsHide?: boolean;
|
|
}
|
|
export function execFileSync(file: string, args?: string[], options?: ExecSyncOptions): string;
|
|
export function execSync(command: string, options?: ExecSyncOptions): string;
|
|
export type ExecFileCallback = (error: Error | null, stdout: string, stderr: string) => void;
|
|
/* The callback form starts an asynchronous child and captures utf8
|
|
* stdout/stderr. The no-callback overload remains for util.promisify's
|
|
* static target recognition; reached no-callback calls still fence. */
|
|
export function execFile(file: string, callback: ExecFileCallback): ChildProcess;
|
|
export function execFile(
|
|
file: string,
|
|
args: string[] | null,
|
|
callback: ExecFileCallback,
|
|
): ChildProcess;
|
|
export function execFile(
|
|
file: string,
|
|
args: string[] | null,
|
|
options: ExecSyncOptions,
|
|
callback: ExecFileCallback,
|
|
): ChildProcess;
|
|
export function execFile(
|
|
file: string,
|
|
args?: string[] | null,
|
|
options?: ExecSyncOptions,
|
|
): unknown;
|
|
/* The async shell form. The callback forms lower like execFile's, with
|
|
* the command line run by /bin/sh -c, and util.promisify recognizes it
|
|
* statically, as it does execFile; reached calls without a callback
|
|
* fence at their site. */
|
|
export function exec(
|
|
command: string,
|
|
options?: ExecSyncOptions | ((error: Error | null, stdout: string, stderr: string) => void),
|
|
callback?: (error: Error | null, stdout: string, stderr: string) => void,
|
|
): ChildProcess;
|
|
}
|
|
|
|
declare module "node:child_process" {
|
|
export * from "child_process";
|
|
}
|