mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 08:35:07 +08:00
feat(child_process): support writable child stdin (#341)
* feat(child_process): support writable child stdin - Add nonblocking child stdin writers with lifecycle events and platform-aware backpressure. - Lower writable streams through the C and LLVM backends while fencing unsupported overloads. - Add differential, type, diagnostics, and compatibility coverage. Fixes #311 Co-authored-by: euxaristia <25621994+euxaristia@users.noreply.github.com> * Reject non-literal spawn options - Emit a named lowering diagnostic instead of silently dropping variable options. - Cover two- and three-argument spawn overloads with diagnostic regressions. Fixes #311 Co-authored-by: euxaristia <25621994+euxaristia@users.noreply.github.com> --------- Co-authored-by: euxaristia <25621994+euxaristia@users.noreply.github.com>
This commit is contained in:
@@ -2,6 +2,6 @@
|
||||
"schemaVersion": 3,
|
||||
"nodeVersion": "24.15.0",
|
||||
"nodeCommit": "848430679556aed0bd073f2bc263331ad84fa119",
|
||||
"artifactVersion": "13aa4b44face2c6c630d",
|
||||
"artifactVersion": "748941357fe7bf09817d",
|
||||
"rowCount": 3662
|
||||
}
|
||||
|
||||
@@ -218,9 +218,9 @@
|
||||
"apiEntries": 39,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 6,
|
||||
"partial": 7,
|
||||
"refused": 1,
|
||||
"not-implemented": 32,
|
||||
"not-implemented": 31,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
@@ -986,9 +986,9 @@
|
||||
"apiEntries": 137,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 33,
|
||||
"partial": 36,
|
||||
"refused": 0,
|
||||
"not-implemented": 104,
|
||||
"not-implemented": 101,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
@@ -12261,9 +12261,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
},
|
||||
"dynamic": {
|
||||
"status": "not-implemented",
|
||||
@@ -72438,9 +72438,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented static async-iteration subset.",
|
||||
"verification": "test-backed"
|
||||
},
|
||||
"dynamic": {
|
||||
"status": "partial",
|
||||
@@ -72662,9 +72662,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented static async-iteration subset.",
|
||||
"verification": "test-backed"
|
||||
},
|
||||
"dynamic": {
|
||||
"status": "not-implemented",
|
||||
@@ -72970,9 +72970,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented static async-iteration subset.",
|
||||
"verification": "test-backed"
|
||||
},
|
||||
"dynamic": {
|
||||
"status": "not-implemented",
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
"tiers": {
|
||||
"static": {
|
||||
"replace-refusal": 59,
|
||||
"verify-gap": 2746,
|
||||
"audit-partial": 415,
|
||||
"verify-gap": 2742,
|
||||
"audit-partial": 419,
|
||||
"classify": 2,
|
||||
"implement": 81
|
||||
},
|
||||
@@ -6612,12 +6612,19 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:child_process",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:child_process.stdin",
|
||||
"tests": [
|
||||
"tests/corpus/2893-child-stdin-duplex.ts",
|
||||
"tests/corpus/2894-child-stdin-backpressure.ts",
|
||||
"tests/corpus/2895-child-stdin-write-after-end.ts",
|
||||
"tests/corpus/2896-child-stdin-write-after-finish.ts",
|
||||
"tests/corpus/2897-child-stdin-destroy.ts",
|
||||
"tests/corpus/2898-child-stdin-spawn-failure.ts"
|
||||
]
|
||||
},
|
||||
"dynamic": {
|
||||
"status": "not-implemented",
|
||||
@@ -78843,12 +78850,14 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:stream",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:stream.writable.destroy",
|
||||
"tests": [
|
||||
"tests/corpus/2897-child-stdin-destroy.ts"
|
||||
]
|
||||
},
|
||||
"dynamic": {
|
||||
"status": "partial",
|
||||
@@ -79133,12 +79142,15 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:stream",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:stream.writable",
|
||||
"tests": [
|
||||
"tests/corpus/2893-child-stdin-duplex.ts",
|
||||
"tests/corpus/2895-child-stdin-write-after-end.ts"
|
||||
]
|
||||
},
|
||||
"dynamic": {
|
||||
"status": "not-implemented",
|
||||
@@ -79496,12 +79508,14 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:stream",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:stream.'drain'",
|
||||
"tests": [
|
||||
"tests/corpus/2894-child-stdin-backpressure.ts"
|
||||
]
|
||||
},
|
||||
"dynamic": {
|
||||
"status": "not-implemented",
|
||||
|
||||
@@ -195,9 +195,9 @@
|
||||
"apiEntries": 39,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 6,
|
||||
"partial": 7,
|
||||
"refused": 1,
|
||||
"not-implemented": 32,
|
||||
"not-implemented": 31,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
@@ -1107,9 +1107,9 @@
|
||||
"apiEntries": 137,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 33,
|
||||
"partial": 36,
|
||||
"refused": 0,
|
||||
"not-implemented": 104,
|
||||
"not-implemented": 101,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
@@ -13988,7 +13988,13 @@
|
||||
"tests": [
|
||||
"tests/corpus/1361-spawn-events.ts",
|
||||
"tests/corpus/1565-spawn-pipe-streams.ts",
|
||||
"tests/corpus/2844-node-disposable-resources.ts"
|
||||
"tests/corpus/2844-node-disposable-resources.ts",
|
||||
"tests/corpus/2893-child-stdin-duplex.ts",
|
||||
"tests/corpus/2894-child-stdin-backpressure.ts",
|
||||
"tests/corpus/2895-child-stdin-write-after-end.ts",
|
||||
"tests/corpus/2896-child-stdin-write-after-finish.ts",
|
||||
"tests/corpus/2897-child-stdin-destroy.ts",
|
||||
"tests/corpus/2898-child-stdin-spawn-failure.ts"
|
||||
]
|
||||
},
|
||||
"dynamic": {
|
||||
@@ -14952,8 +14958,16 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:child_process"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:child_process.stdin",
|
||||
"tests": [
|
||||
"tests/corpus/2893-child-stdin-duplex.ts",
|
||||
"tests/corpus/2894-child-stdin-backpressure.ts",
|
||||
"tests/corpus/2895-child-stdin-write-after-end.ts",
|
||||
"tests/corpus/2896-child-stdin-write-after-finish.ts",
|
||||
"tests/corpus/2897-child-stdin-destroy.ts",
|
||||
"tests/corpus/2898-child-stdin-spawn-failure.ts"
|
||||
]
|
||||
},
|
||||
"dynamic": {
|
||||
"status": "not-implemented",
|
||||
@@ -93993,7 +94007,11 @@
|
||||
"tests/corpus/1688-stream-writable-basics.ts",
|
||||
"tests/corpus/1746-stream-for-await.ts",
|
||||
"tests/corpus/1814-stream-pipeline.cjs",
|
||||
"tests/corpus/2861-readable-iterator-close.ts"
|
||||
"tests/corpus/2861-readable-iterator-close.ts",
|
||||
"tests/corpus/2893-child-stdin-duplex.ts",
|
||||
"tests/corpus/2894-child-stdin-backpressure.ts",
|
||||
"tests/corpus/2895-child-stdin-write-after-end.ts",
|
||||
"tests/corpus/2897-child-stdin-destroy.ts"
|
||||
]
|
||||
},
|
||||
"dynamic": {
|
||||
@@ -95566,8 +95584,11 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:stream"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:stream.writable.destroy",
|
||||
"tests": [
|
||||
"tests/corpus/2897-child-stdin-destroy.ts"
|
||||
]
|
||||
},
|
||||
"dynamic": {
|
||||
"status": "partial",
|
||||
@@ -95817,8 +95838,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:stream"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:stream.writable",
|
||||
"tests": [
|
||||
"tests/corpus/2893-child-stdin-duplex.ts",
|
||||
"tests/corpus/2895-child-stdin-write-after-end.ts"
|
||||
]
|
||||
},
|
||||
"dynamic": {
|
||||
"status": "not-implemented",
|
||||
@@ -96114,8 +96139,11 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:stream"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:stream.'drain'",
|
||||
"tests": [
|
||||
"tests/corpus/2894-child-stdin-backpressure.ts"
|
||||
]
|
||||
},
|
||||
"dynamic": {
|
||||
"status": "not-implemented",
|
||||
|
||||
@@ -88,10 +88,14 @@
|
||||
{ "chapter": "packages", "symbols": ["package.json", "\"name\"", "\"main\"", "\"type\"", "\"exports\"", "\"imports\""], "status": "partial", "evidence": ["tests/corpus/2092-package-imports/main.ts", "tests/corpus/2120-package-self-import/main.ts", "tests/corpus/2124-imports-field-wildcard/main.ts", "tests/corpus/2390-dot-requires/main.cjs", "tests/corpus/2830-cjs-require-self-name/main.cjs"] },
|
||||
{ "chapter": "packages", "symbols": ["--input-type"], "status": "not-applicable" },
|
||||
{ "chapter": "child_process", "signatures": ["`subprocess[Symbol.dispose]()`"], "status": "partial", "evidence": ["tests/corpus/2844-node-disposable-resources.ts"] },
|
||||
{ "chapter": "child_process", "signatures": ["`stdin` Type: {stream.Writable|null|undefined}"], "status": "partial", "evidence": ["tests/corpus/2893-child-stdin-duplex.ts", "tests/corpus/2894-child-stdin-backpressure.ts", "tests/corpus/2895-child-stdin-write-after-end.ts", "tests/corpus/2896-child-stdin-write-after-finish.ts", "tests/corpus/2897-child-stdin-destroy.ts", "tests/corpus/2898-child-stdin-spawn-failure.ts"] },
|
||||
{ "chapter": "fs", "signatures": ["`filehandle[Symbol.asyncDispose]()`"], "status": "partial", "evidence": ["tests/corpus/2844-node-disposable-resources.ts"] },
|
||||
{ "chapter": "readline", "signatures": ["`rl[Symbol.dispose]()`"], "status": "partial", "evidence": ["tests/corpus/2844-node-disposable-resources.ts"] },
|
||||
{ "chapter": "timers", "signatures": ["`immediate[Symbol.dispose]()`", "`timeout[Symbol.dispose]()`"], "status": "partial", "evidence": ["tests/corpus/2844-node-disposable-resources.ts"] },
|
||||
{ "chapter": "stream", "signatures": ["`readable[Symbol.asyncIterator]()`", "`readable.iterator([options])`"], "status": "partial", "evidence": ["tests/corpus/1746-stream-for-await.ts", "tests/corpus/2861-readable-iterator-close.ts"] },
|
||||
{ "chapter": "stream", "signatures": ["`writable.destroy([error])`"], "status": "partial", "evidence": ["tests/corpus/2897-child-stdin-destroy.ts"] },
|
||||
{ "chapter": "stream", "signatures": ["`writable` Type: {boolean}"], "status": "partial", "evidence": ["tests/corpus/2893-child-stdin-duplex.ts", "tests/corpus/2895-child-stdin-write-after-end.ts"] },
|
||||
{ "chapter": "stream", "signatures": ["Event: `'drain'`"], "status": "partial", "evidence": ["tests/corpus/2894-child-stdin-backpressure.ts"] },
|
||||
{ "chapter": "webstreams", "signatures": ["`readableStream.values([options])`"], "status": "partial", "evidence": ["tests/corpus/2862-web-readable-async-iteration.ts"] },
|
||||
{ "chapter": "typescript", "symbols": ["type"], "status": "supported", "evidence": ["tests/corpus/2702-type-import-link-static/main.ts", "tests/corpus/2703-type-import-link-static-reexport/main.ts", "tests/corpus/2704-type-import-link-static-success/main.ts", "tests/corpus/2707-type-import-link-static-default-reexport/main.ts", "tests/harness/errors.test.ts"] }
|
||||
],
|
||||
|
||||
+18
-4
@@ -1533,7 +1533,8 @@ declare module "child_process" {
|
||||
): SpawnSyncReturns;
|
||||
|
||||
/* The asynchronous slice: spawn with stdio "ignore"/"inherit"/fd/"pipe"
|
||||
* tuples (piped stdout/stderr deliver through child.stdout/stderr) and
|
||||
* tuples (piped stdin is writable through child.stdin; piped stdout/stderr
|
||||
* deliver through child.stdout/stderr) and
|
||||
* the two 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
|
||||
@@ -1545,6 +1546,17 @@ declare module "child_process" {
|
||||
* 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;
|
||||
once(event: "drain" | "finish", listener: () => void): void;
|
||||
once(event: "error", listener: (err: Error) => void): void;
|
||||
}
|
||||
|
||||
export interface ChildProcess extends Disposable {
|
||||
/* The exit listener may also take Node's second parameter — the
|
||||
* terminating signal's name, null for a normal exit. */
|
||||
@@ -1566,8 +1578,9 @@ declare module "child_process" {
|
||||
kill(signal?: string | number): boolean;
|
||||
unref(): void;
|
||||
[Symbol.dispose](): void;
|
||||
/* The piped-output streams — non-null exactly when the matching
|
||||
* stdio slot was "pipe" (Node's shape). */
|
||||
/* 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;
|
||||
}
|
||||
@@ -1578,7 +1591,8 @@ declare module "child_process" {
|
||||
/* 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); piped STDIN stays a compile fence. */
|
||||
* 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
|
||||
|
||||
@@ -3,7 +3,7 @@ import { InternalCompilerError } from "../../errors.js";
|
||||
* expression lands in a fresh C temp, with RC ownership tracked on the
|
||||
* emitter's frames (see the discipline comment in emitter core). */
|
||||
import type { CEmitter, Temp } from "./c-emitter.js";
|
||||
import { arrayOf, BOOL, BYTES_U8, bytesOf, canMarshalFuncIntoIsland, CHILDSTREAM_T, DYN, F64, IrExpr, IrLibFn, IrRecordShape, IrType, islandPromisePayloadTag, isClassOwnEnumerableFieldName, isDynTypedRefType, isFfiCallbackParam, isFfiContextParam, isFfiReleaseParam, isRefCounted, isUnitType, MAY_THROW_LIB_FNS, RUNTIME_ERROR_CLASSES, STRING, typeEquals, typeKey } from "../../ir/ir.js";
|
||||
import { arrayOf, BOOL, BYTES_U8, bytesOf, canMarshalFuncIntoIsland, CHILDSTREAM_T, CHILDWRITER_T, DYN, F64, IrExpr, IrLibFn, IrRecordShape, IrType, islandPromisePayloadTag, isClassOwnEnumerableFieldName, isDynTypedRefType, isFfiCallbackParam, isFfiContextParam, isFfiReleaseParam, isRefCounted, isUnitType, MAY_THROW_LIB_FNS, RUNTIME_ERROR_CLASSES, STRING, typeEquals, typeKey } from "../../ir/ir.js";
|
||||
import { boxAccess, BYTES_NUM_KIND_C, BYTES_NUM_VAR_C, bytesElemKindC, cDecl, cFnPtrCast, cNumberLiteral, cStringLiteral, cType, DV_GET_KIND_C, DV_SET_KIND_C, elemAccess, mapKeyAccess, mapKeyKindC, mapValKindC, releaseCallC, retainCallC, vAdapters } from "./types.js";
|
||||
import { mangleClassNew, mangleClassRetain, mangleClassStruct, mangleField, mangleFnClosure, mangleFunction, mangleGlobal, mangleLocal, mangleRecordClone, mangleRecordNew, mangleRecordStruct, mangleVtStruct } from "../mangle.js";
|
||||
import { OVERFLOW_MEMBER } from "./shapes.js";
|
||||
@@ -5483,26 +5483,58 @@ function emitChildProcessLibCall(state: LibCallState): Temp {
|
||||
const absent = emitter.unitInstanceRef(e.type.unionId, unitTag);
|
||||
return emitter.newTemp(e.type, `${has}(${arg(0)}) ? ${present} : ${absent}`);
|
||||
}
|
||||
case "child.stdin":
|
||||
case "child.stdout":
|
||||
case "child.stderr": {
|
||||
// `Readable | null` — the child.pid pattern with a REF arm:
|
||||
// the runtime answers a +1 stream handle or NULL (not piped).
|
||||
// `Writable | null` / `Readable | null` — the child.pid
|
||||
// pattern with a REF arm: the runtime answers a +1 stream
|
||||
// handle or NULL (not piped).
|
||||
if (e.type.kind !== "union") {
|
||||
throw new InternalCompilerError(`emitter bug: ${e.fn} result is not a union`);
|
||||
}
|
||||
const def = emitter.unionsById.get(e.type.unionId);
|
||||
const streamTag = def ? def.arms.findIndex((a) => a.kind === "childStream") : -1;
|
||||
const writer = e.fn === "child.stdin";
|
||||
const streamTag = def ? def.arms.findIndex((a) => a.kind === (writer ? "childWriter" : "childStream")) : -1;
|
||||
const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1;
|
||||
if (streamTag < 0 || nullTag < 0) {
|
||||
throw new InternalCompilerError(`emitter bug: ${e.fn} union lacks its arms`);
|
||||
}
|
||||
const get = e.fn === "child.stdout" ? "scr_child_stdout" : "scr_child_stderr";
|
||||
const raw = emitter.newTemp(CHILDSTREAM_T, `${get}(${arg(0)})`);
|
||||
const get = writer ? "scr_child_stdin" : e.fn === "child.stdout" ? "scr_child_stdout" : "scr_child_stderr";
|
||||
const rawType = writer ? CHILDWRITER_T : CHILDSTREAM_T;
|
||||
const raw = emitter.newTemp(rawType, `${get}(${arg(0)})`);
|
||||
emitter.moveTemp(raw); // ownership passes into the union arm below
|
||||
const present = `scr_union_new_ref(${streamTag}, ${raw.name}, &scr_child_stream_retain_v, &scr_child_stream_release_v, NULL)`;
|
||||
const stem = writer ? "scr_child_writer" : "scr_child_stream";
|
||||
const present = `scr_union_new_ref(${streamTag}, ${raw.name}, &${stem}_retain_v, &${stem}_release_v, NULL)`;
|
||||
const absent = emitter.unitInstanceRef(e.type.unionId, nullTag);
|
||||
return emitter.newTemp(e.type, `${raw.name} != NULL ? ${present} : ${absent}`);
|
||||
}
|
||||
case "writer.writeString":
|
||||
return finish(`scr_child_writer_write_string(${arg(0)}, ${arg(1)})`);
|
||||
case "writer.writeBytes":
|
||||
return finish(`scr_child_writer_write_bytes(${arg(0)}, ${arg(1)})`);
|
||||
case "writer.end":
|
||||
return finish(`scr_child_writer_end(${arg(0)})`);
|
||||
case "writer.destroy":
|
||||
return finish(`scr_child_writer_destroy(${arg(0)})`);
|
||||
case "writer.writable":
|
||||
return finish(`scr_child_writer_writable(${arg(0)})`);
|
||||
case "writer.onDrain":
|
||||
case "writer.onFinish": {
|
||||
const cb = args[1]!;
|
||||
emitter.moveTemp(cb);
|
||||
const fnName = fn === "writer.onDrain" ? "scr_child_writer_on_drain" : "scr_child_writer_on_finish";
|
||||
emitter.line(`${fnName}(${arg(0)}, ${cb.name}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
case "writer.onError": {
|
||||
const cbT = e.args[1]!.type;
|
||||
if (cbT.kind !== "func") throw new InternalCompilerError("emitter bug: writer.onError callback not a func");
|
||||
const cb = args[1]!;
|
||||
emitter.moveTemp(cb);
|
||||
const adapter = cbT.params.length === 0 ? "scr_child_err_thunk0" : "scr_child_err_thunk_error";
|
||||
emitter.line(`scr_child_writer_on_error(${arg(0)}, ${cb.name}, &${adapter}, ${arg(2)});${emitter.srcComment(e.loc)}`);
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
case "procStream.write":
|
||||
// The receiver IS the fd scalar; dispatches onto the exact
|
||||
// promptly-submitted stdout/stderr paths (ordering identical).
|
||||
@@ -8437,6 +8469,7 @@ function emitLibCallExpr(emitter: CEmitter, e: LibCallExpr): Temp {
|
||||
case "cp":
|
||||
case "spawnRes":
|
||||
case "child":
|
||||
case "writer":
|
||||
case "procStream":
|
||||
return emitChildProcessLibCall(state);
|
||||
case "net":
|
||||
|
||||
@@ -805,7 +805,7 @@ function emitRecordCloneC(
|
||||
t.kind === "httpReq" || t.kind === "httpRes" ||
|
||||
t.kind === "httpClientReq" || t.kind === "secureCtx" ||
|
||||
t.kind === "cryptoHash" || t.kind === "cryptoHmac" ||
|
||||
t.kind === "fsWatcher" || t.kind === "childStream" ||
|
||||
t.kind === "fsWatcher" || t.kind === "childStream" || t.kind === "childWriter" ||
|
||||
// Island handles: the box carries scr_jsval_retain_v/release_v and
|
||||
// no trace — the same stance as jsval array elements.
|
||||
t.kind === "jsval" ||
|
||||
|
||||
@@ -85,6 +85,8 @@ export function cType(t: IrType): string {
|
||||
return "ScrWatcher *";
|
||||
case "childStream":
|
||||
return "ScrChildStream *";
|
||||
case "childWriter":
|
||||
return "ScrChildWriter *";
|
||||
case "procStream":
|
||||
// A SCALAR kind: the stream value IS its fd (1 = stdout, 2 =
|
||||
// stderr) — no heap, no refcount.
|
||||
|
||||
@@ -2480,7 +2480,7 @@ class LlEmitter {
|
||||
case "array": case "record": case "object": case "classval": case "func":
|
||||
case "map": case "set": case "symbol": case "regex": case "promise": case "bytes":
|
||||
case "url": case "searchParams": case "stats": case "fileHandle": case "spawnRes":
|
||||
case "child": case "childStream": case "generator": case "fsWatcher":
|
||||
case "child": case "childStream": case "childWriter": case "generator": case "fsWatcher":
|
||||
case "cryptoHash": case "cryptoHmac": {
|
||||
if (unionArm) return "true";
|
||||
const truthy = B.tmp();
|
||||
|
||||
@@ -226,6 +226,7 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
case "cp":
|
||||
case "spawnRes":
|
||||
case "child":
|
||||
case "writer":
|
||||
case "procStream":
|
||||
return host.emitChildProcessLibCall(e);
|
||||
case "tp":
|
||||
|
||||
@@ -137,21 +137,64 @@ export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr
|
||||
B.line(`${raw} = call ptr @scr_spawn_res_error(ptr ${recv.name}) ; +1 or NULL`);
|
||||
return host.wrapNullable(raw, raw, { kind: "object", className: "%Error" }, errTag, e.type, undefTag);
|
||||
}
|
||||
if (e.fn === "child.stdout" || e.fn === "child.stderr") {
|
||||
// `Readable | null` — the child.pid pattern with a REF arm: the
|
||||
// runtime answers a +1 stream handle or NULL (not piped).
|
||||
if (e.fn === "child.stdin" || e.fn === "child.stdout" || e.fn === "child.stderr") {
|
||||
// `Writable | null` / `Readable | null` — the child.pid pattern
|
||||
// with a REF arm: the runtime answers a +1 handle or NULL.
|
||||
if (e.type.kind !== "union") throw new InternalCompilerError(`llvm emitter bug: ${e.fn} result is not a union`);
|
||||
const def = host.unionsById.get(e.type.unionId);
|
||||
const streamTag = def ? def.arms.findIndex((a) => a.kind === "childStream") : -1;
|
||||
const writer = e.fn === "child.stdin";
|
||||
const streamTag = def ? def.arms.findIndex((a) => a.kind === (writer ? "childWriter" : "childStream")) : -1;
|
||||
const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1;
|
||||
if (streamTag < 0 || nullTag < 0) throw new InternalCompilerError(`llvm emitter bug: ${e.fn} union lacks its arms`);
|
||||
const get = e.fn === "child.stdout" ? "scr_child_stdout" : "scr_child_stderr";
|
||||
const get = writer ? "scr_child_stdin" : e.fn === "child.stdout" ? "scr_child_stdout" : "scr_child_stderr";
|
||||
const recv = host.emitExpr(e.args[0]!);
|
||||
host.declare(`declare ptr @${get}(ptr)`);
|
||||
const raw = B.tmp();
|
||||
B.line(`${raw} = call ptr @${get}(ptr ${recv.name}) ; +1 or NULL`);
|
||||
return host.wrapNullable(raw, raw, def!.arms[streamTag]!, streamTag, e.type, nullTag);
|
||||
}
|
||||
if (e.fn === "writer.writeString" || e.fn === "writer.writeBytes") {
|
||||
const writer = host.emitExpr(e.args[0]!);
|
||||
const data = host.emitExpr(e.args[1]!);
|
||||
const sym = e.fn === "writer.writeString" ? "scr_child_writer_write_string" : "scr_child_writer_write_bytes";
|
||||
host.declare(`declare zeroext i1 @${sym}(ptr, ptr)`);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = call zeroext i1 @${sym}(ptr ${writer.name}, ptr ${data.name})`);
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
if (e.fn === "writer.end" || e.fn === "writer.destroy") {
|
||||
const writer = host.emitExpr(e.args[0]!);
|
||||
const sym = e.fn === "writer.end" ? "scr_child_writer_end" : "scr_child_writer_destroy";
|
||||
host.declare(`declare void @${sym}(ptr)`);
|
||||
B.line(`call void @${sym}(ptr ${writer.name})`);
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
if (e.fn === "writer.writable") {
|
||||
const writer = host.emitExpr(e.args[0]!);
|
||||
host.declare(`declare zeroext i1 @scr_child_writer_writable(ptr)`);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = call zeroext i1 @scr_child_writer_writable(ptr ${writer.name})`);
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
if (e.fn === "writer.onDrain" || e.fn === "writer.onFinish" || e.fn === "writer.onError") {
|
||||
const writer = host.emitExpr(e.args[0]!);
|
||||
const cbT = e.args[1]!.type;
|
||||
if (cbT.kind !== "func") throw new InternalCompilerError(`llvm emitter bug: ${e.fn} callback not a func`);
|
||||
const cb = host.emitExpr(e.args[1]!);
|
||||
host.moveTemp(cb);
|
||||
const once = host.emitExpr(e.args[2]!);
|
||||
if (e.fn === "writer.onError") {
|
||||
const adapter = cbT.params.length === 0 ? "scr_child_err_thunk0" : "scr_child_err_thunk_error";
|
||||
host.declare(`declare void @${adapter}(ptr, ptr)`);
|
||||
host.declare(`declare void @scr_child_writer_on_error(ptr, ptr, ptr, i1 zeroext)`);
|
||||
B.line(`call void @scr_child_writer_on_error(ptr ${writer.name}, ptr ${cb.name}, ptr @${adapter}, i1 ${once.name})`);
|
||||
} else {
|
||||
const sym = e.fn === "writer.onDrain" ? "scr_child_writer_on_drain" : "scr_child_writer_on_finish";
|
||||
host.declare(`declare void @${sym}(ptr, ptr, i1 zeroext)`);
|
||||
B.line(`call void @${sym}(ptr ${writer.name}, ptr ${cb.name}, i1 ${once.name})`);
|
||||
}
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
return host.emitGenericLibCall(e);
|
||||
}
|
||||
|
||||
|
||||
@@ -333,7 +333,7 @@ export function boxNewCall(host: ShapeHost, t: IrType): string {
|
||||
t.kind === "record" || t.kind === "object" || t.kind === "classval" || t.kind === "union" ||
|
||||
t.kind === "array" || t.kind === "map" || t.kind === "set" || t.kind === "symbol" || t.kind === "regex" ||
|
||||
t.kind === "promise" || t.kind === "bytes" || t.kind === "url" || t.kind === "searchParams" ||
|
||||
t.kind === "stats" || t.kind === "fileHandle" || t.kind === "spawnRes" || t.kind === "child" || t.kind === "childStream" ||
|
||||
t.kind === "stats" || t.kind === "fileHandle" || t.kind === "spawnRes" || t.kind === "child" || t.kind === "childStream" || t.kind === "childWriter" ||
|
||||
t.kind === "generator" ||
|
||||
t.kind === "netServer" || t.kind === "netSocket" || t.kind === "dgramSocket" ||
|
||||
t.kind === "httpReq" || t.kind === "httpRes" || t.kind === "httpClientReq" ||
|
||||
|
||||
@@ -37,7 +37,7 @@ import { CRYPTO_CIPHERS, CRYPTO_CONSTANTS, CRYPTO_CURVES, CRYPTO_HASHES } from "
|
||||
import { generatorMeta, timerStyleCallback } from "./lower-calls.js";
|
||||
import { registerHttpClientFnBinding, voidizedCallback } from "./lower-server.js";
|
||||
import { pairsSnapshotHelper } from "./pairs-snapshot.js";
|
||||
import { BOOL, BYTES_U8, CHILD_T, CHILDSTREAM_T, CRYPTOHASH_T, CRYPTOHMAC_T, DYN, F64, FILEHANDLE_T, FSWATCHER_T, PROCSTREAM_T, IrExpr, IrFunction, IrLibFn, IrLocal, IrStmt, IrType, JSVAL, NULL_T, SEARCH_PARAMS_T, SPAWNRES_T, STRING, SrcLoc, UNDEFINED_T, VOID, arrayOf, canBoxFuncIntoDyn, canConvertToDyn, funcOf, isUnitType, typeEquals, typeKey } from "../../ir/ir.js";
|
||||
import { BOOL, BYTES_U8, CHILD_T, CHILDSTREAM_T, CHILDWRITER_T, CRYPTOHASH_T, CRYPTOHMAC_T, DYN, F64, FILEHANDLE_T, FSWATCHER_T, PROCSTREAM_T, IrExpr, IrFunction, IrLibFn, IrLocal, IrStmt, IrType, JSVAL, NULL_T, SEARCH_PARAMS_T, SPAWNRES_T, STRING, SrcLoc, UNDEFINED_T, VOID, arrayOf, canBoxFuncIntoDyn, canConvertToDyn, funcOf, isUnitType, typeEquals, typeKey } from "../../ir/ir.js";
|
||||
import { boolLit, countedFor, numLit, strLit, varRef } from "../../ir/build.js";
|
||||
|
||||
function optionalStringTags(lowerer: Lowerer, type: IrType): { stringTag: number; undefinedTag: number } | null {
|
||||
@@ -2287,13 +2287,12 @@ function optionMember(p: ts.ObjectLiteralElementLike): { name: string; value: ts
|
||||
}
|
||||
|
||||
/** `spawn(command, args?, options)` → one cp.spawn / cp.spawnOpts
|
||||
* libCall. The options argument must be an object LITERAL with an
|
||||
* EXPLICIT stdio — "ignore" or "inherit" as the scalar, or the 3-tuple
|
||||
* whose stdout/stderr slots may also be "pipe" (the child.stdout/
|
||||
* child.stderr streams) or number fds; piped STDIN fences, and
|
||||
* OMITTING the options means Node's default stdio, "pipe" on all
|
||||
* three — fenced too, so a program never silently loses its child's
|
||||
* output. The other lowered
|
||||
* libCall. The options argument, when present, must be an object
|
||||
* LITERAL. Stdio accepts "ignore", "inherit", or "pipe" as the scalar,
|
||||
* or the 3-tuple whose stdin pipe becomes child.stdin, stdout/stderr
|
||||
* pipes become child.stdout/child.stderr, and output slots may also be
|
||||
* number fds. Omitting stdio uses Node's default: all three piped. The
|
||||
* other lowered
|
||||
* members: `detached` (a boolean literal, inline or carried by the
|
||||
* conditional spread `...(c ? { detached: true } : {})` in either
|
||||
* orientation — POSIX_SPAWN_SETSID, the child gets its own session and
|
||||
@@ -2306,12 +2305,30 @@ function optionMember(p: ts.ObjectLiteralElementLike): { name: string; value: ts
|
||||
lowerer.noLowering(
|
||||
"spawn with this argument shape",
|
||||
expr,
|
||||
'the supported form is spawn(command, args?, { stdio: "ignore" | "inherit", detached?, env?, cwd? })',
|
||||
'the supported form is spawn(command, args?, { stdio: "pipe" | "ignore" | "inherit", detached?, env?, cwd? })',
|
||||
);
|
||||
}
|
||||
const cmd = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING);
|
||||
const argsNode = expr.arguments.length === 3 ? expr.arguments[1] : undefined;
|
||||
const optsNode = expr.arguments[expr.arguments.length - 1];
|
||||
let argsNode: ts.Expression | undefined;
|
||||
let optsNode: ts.Expression | undefined;
|
||||
if (expr.arguments.length === 3) {
|
||||
argsNode = expr.arguments[1];
|
||||
optsNode = expr.arguments[2];
|
||||
} else if (expr.arguments.length === 2) {
|
||||
const second = expr.arguments[1]!;
|
||||
const secondType = lowerer.typeOf(second);
|
||||
const mapped = lowerer.mapTypeOf(secondType);
|
||||
if (lowerer.checker.isArrayType(secondType) || lowerer.checker.isTupleType(secondType) ||
|
||||
(mapped?.kind === "array" && mapped.elem.kind === "string")) argsNode = second;
|
||||
else optsNode = second;
|
||||
}
|
||||
if (optsNode !== undefined && !ts.isObjectLiteralExpression(optsNode)) {
|
||||
lowerer.noLowering(
|
||||
"spawn with a non-literal options argument",
|
||||
optsNode,
|
||||
"pass stdio, detached, env, cwd, and windowsHide in an inline object literal",
|
||||
);
|
||||
}
|
||||
|
||||
const emptyStr: IrExpr = { kind: "strLit", value: "", type: STRING, loc };
|
||||
// Per-slot stdio modes (scr_child.c: 0 ignore, 1 inherit, 2 fd) and
|
||||
@@ -2327,13 +2344,7 @@ function optionMember(p: ts.ObjectLiteralElementLike): { name: string; value: ts
|
||||
let cwd: IrExpr = emptyStr;
|
||||
let plain = true; // exactly { stdio: "ignore" }: the historical libCall
|
||||
|
||||
const pipeFence = (node: ts.Node): never =>
|
||||
lowerer.noLowering(
|
||||
'spawn with stdio: "pipe"',
|
||||
node,
|
||||
'piped STDIN has no lowering — pipe stdout/stderr with the tuple form (stdio: ["ignore", "pipe", "pipe"]), or capture with spawnSync',
|
||||
);
|
||||
if (optsNode && ts.isObjectLiteralExpression(optsNode) && expr.arguments.length >= 2) {
|
||||
if (optsNode && ts.isObjectLiteralExpression(optsNode)) {
|
||||
for (const p of optsNode.properties) {
|
||||
// The conditional-spread idiom `...(isWindows ? {} : { detached:
|
||||
// true })` (either orientation): the one carried member supported
|
||||
@@ -2378,20 +2389,18 @@ function optionMember(p: ts.ObjectLiteralElementLike): { name: string; value: ts
|
||||
}
|
||||
switch (m.name) {
|
||||
case "stdio": {
|
||||
// The 3-tuple form: stdin a "ignore"/"inherit" literal,
|
||||
// stdout/stderr each a literal (including "pipe" — the
|
||||
// child.stdout/stderr stream slots) OR a number-typed fd (an
|
||||
// openSync result — dup2'd into the child, Node's fd slots).
|
||||
// The 3-tuple form: every slot accepts a literal, including
|
||||
// "pipe" (child.stdin/stdout/stderr); stdout/stderr also accept
|
||||
// a number-typed fd (an openSync result — dup2'd into the child).
|
||||
if (ts.isArrayLiteralExpression(m.value) && m.value.elements.length === 3) {
|
||||
const slot = (el: ts.Expression, which: 0 | 1 | 2): void => {
|
||||
const t = lowerer.typeOf(el);
|
||||
if (t.isStringLiteralType()) {
|
||||
if (t.value === "pipe" && which === 0) pipeFence(el);
|
||||
if (t.value !== "ignore" && t.value !== "inherit" && t.value !== "pipe") {
|
||||
lowerer.noLowering(
|
||||
`spawn with stdio "${t.value}"`,
|
||||
el,
|
||||
'"ignore", "inherit", "pipe" (stdout/stderr), and number fds are the supported stdio slots',
|
||||
'"ignore", "inherit", "pipe", and number output fds are the supported stdio slots',
|
||||
);
|
||||
}
|
||||
const mode = t.value === "inherit" ? 1 : t.value === "pipe" ? 3 : 0;
|
||||
@@ -2405,7 +2414,7 @@ function optionMember(p: ts.ObjectLiteralElementLike): { name: string; value: ts
|
||||
lowerer.noLowering(
|
||||
"spawn with this stdin slot",
|
||||
el,
|
||||
'stdin takes "ignore" or "inherit" (fd stdin has no lowering)',
|
||||
'stdin takes "pipe", "ignore", or "inherit" (fd stdin has no lowering)',
|
||||
);
|
||||
}
|
||||
if (lowerer.mapTypeOf(t)?.kind !== "f64") {
|
||||
@@ -2426,16 +2435,15 @@ function optionMember(p: ts.ObjectLiteralElementLike): { name: string; value: ts
|
||||
}
|
||||
const t = lowerer.typeOf(m.value);
|
||||
const v = t.isStringLiteralType() ? t.value : null;
|
||||
if (v === "pipe") pipeFence(m.value);
|
||||
if (v !== "ignore" && v !== "inherit") {
|
||||
if (v !== "pipe" && v !== "ignore" && v !== "inherit") {
|
||||
lowerer.noLowering(
|
||||
"spawn with this stdio option",
|
||||
m.value,
|
||||
'"ignore" and "inherit" are the supported stdio literals ' +
|
||||
'(or a 3-tuple of those and number fds; "pipe" has no lowering)',
|
||||
'"pipe", "ignore", and "inherit" are the supported stdio literals ' +
|
||||
"(or a 3-tuple of those and number output fds)",
|
||||
);
|
||||
}
|
||||
const mode = v === "inherit" ? 1 : 0;
|
||||
const mode = v === "inherit" ? 1 : v === "pipe" ? 3 : 0;
|
||||
inMode = outMode = errMode = mode;
|
||||
sawStdio = true;
|
||||
if (v !== "ignore") plain = false;
|
||||
@@ -2478,11 +2486,8 @@ function optionMember(p: ts.ObjectLiteralElementLike): { name: string; value: ts
|
||||
}
|
||||
}
|
||||
if (!sawStdio) {
|
||||
lowerer.noLowering(
|
||||
"spawn without { stdio: \"ignore\" }",
|
||||
expr,
|
||||
'Node\'s default stdio is "pipe" (streams, no lowering) — pass { stdio: "ignore" } or { stdio: "inherit" } explicitly, or capture with spawnSync',
|
||||
);
|
||||
inMode = outMode = errMode = 3;
|
||||
plain = false;
|
||||
}
|
||||
const argv = lowerer.lowerChildArgsArg(argsNode, loc);
|
||||
if (plain) {
|
||||
@@ -5306,7 +5311,7 @@ function lowerOptionalStringSearchParams(lowerer: Lowerer, init: IrExpr, loc: Sr
|
||||
return { kind: "recordGet", obj: receiver, shapeId: receiver.type.shapeId, field: "%enc", type: STRING, loc: locOf(expr) };
|
||||
}
|
||||
const kind = lowerer.mapTypeOf(lowerer.typeOf(expr.expression))?.kind;
|
||||
if (kind !== "stats" && kind !== "fileHandle" && kind !== "spawnRes" && kind !== "child") return null;
|
||||
if (kind !== "stats" && kind !== "fileHandle" && kind !== "spawnRes" && kind !== "child" && kind !== "childWriter") return null;
|
||||
if (kind === "child" ? !isChildSurfaceMember(lowerer, expr) : !lowerer.isStdlibMember(expr)) return null;
|
||||
const name = expr.name.text;
|
||||
const loc = locOf(expr);
|
||||
@@ -5326,19 +5331,20 @@ function lowerOptionalStringSearchParams(lowerer: Lowerer, init: IrExpr, loc: Sr
|
||||
lowerer.checker.getSymbolAtLocation(expr.name),
|
||||
);
|
||||
}
|
||||
// child.stdout / child.stderr — the piped-output streams: the
|
||||
// checker's `Readable | null` (null exactly when the slot was not
|
||||
// piped), constructed type-directedly in the backend over the
|
||||
// child.stdin/stdout/stderr — the piped streams: the checker's
|
||||
// `Writable | null` / `Readable | null` (null exactly when the slot
|
||||
// was not piped), constructed type-directedly in the backend over the
|
||||
// +1-or-NULL runtime pair.
|
||||
if (kind === "child" && (name === "stdout" || name === "stderr")) {
|
||||
if (kind === "child" && (name === "stdin" || name === "stdout" || name === "stderr")) {
|
||||
const receiver = lowerer.lowerExpr(expr.expression);
|
||||
const streamType = name === "stdin" ? CHILDWRITER_T : CHILDSTREAM_T;
|
||||
const type: IrType = {
|
||||
kind: "union",
|
||||
unionId: lowerer.unions.intern([CHILDSTREAM_T, { kind: "nullT" }]),
|
||||
unionId: lowerer.unions.intern([streamType, { kind: "nullT" }]),
|
||||
};
|
||||
const read: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: name === "stdout" ? "child.stdout" : "child.stderr",
|
||||
fn: name === "stdin" ? "child.stdin" : name === "stdout" ? "child.stdout" : "child.stderr",
|
||||
args: [receiver],
|
||||
type,
|
||||
loc,
|
||||
@@ -5346,6 +5352,21 @@ function lowerOptionalStringSearchParams(lowerer: Lowerer, init: IrExpr, loc: Sr
|
||||
return lowerer.maybeNarrow(read, expr);
|
||||
}
|
||||
if (kind === "child") return null; // pid/exitCode/killed live in lowerIntrinsicProperty
|
||||
if (kind === "childWriter") {
|
||||
if (name === "writable") {
|
||||
const receiver = lowerer.lowerExprExpecting(expr.expression, CHILDWRITER_T);
|
||||
return { kind: "libCall", fn: "writer.writable", args: [receiver], type: BOOL, loc };
|
||||
}
|
||||
if (name === "write" || name === "end" || name === "destroy" || name === "on" || name === "once") {
|
||||
lowerer.unsupported("SC1090", expr, `child stdin methods as values (call '${name}' directly)`);
|
||||
}
|
||||
lowerer.noLowering(
|
||||
`Writable.${name}`,
|
||||
expr,
|
||||
'write(string | Uint8Array), end(), destroy(), writable, and on/once("drain" | "finish" | "error", cb) are supported',
|
||||
lowerer.checker.getSymbolAtLocation(expr.name),
|
||||
);
|
||||
}
|
||||
if (
|
||||
kind === "stats" &&
|
||||
(name === "blocks" || name === "nlink" || name === "atimeMs" || name === "mtimeMs")
|
||||
@@ -5597,6 +5618,91 @@ function lowerOptionalStringSearchParams(lowerer: Lowerer, init: IrExpr, loc: Sr
|
||||
);
|
||||
}
|
||||
|
||||
/** Method calls on a piped child stdin writer. Writes copy one string or
|
||||
* Uint8Array into the nonblocking runtime queue and return the backpressure
|
||||
* signal. end()/destroy() are statement-only, as are drain/finish/error
|
||||
* listener registrations; unsupported callbacks/encodings stay fenced. */
|
||||
export function lowerChildWriterMethodCall(lowerer: Lowerer, call: ts.CallExpression,
|
||||
access: ts.PropertyAccessExpression,): IrExpr | null {
|
||||
if (lowerer.chainBlocked(call, access)) return null;
|
||||
if (lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind !== "childWriter") return null;
|
||||
if (!lowerer.isStdlibMember(access)) return null;
|
||||
const name = access.name.text;
|
||||
const loc = locOf(call);
|
||||
|
||||
if (name === "write") {
|
||||
if (call.arguments.length !== 1 || call.arguments.some(ts.isSpreadElement)) {
|
||||
lowerer.noLowering(
|
||||
`child stdin write with ${call.arguments.length} arguments`,
|
||||
call,
|
||||
"write(string | Uint8Array) with no encoding or callback is supported",
|
||||
);
|
||||
}
|
||||
const receiver = lowerer.lowerExprExpecting(access.expression, CHILDWRITER_T);
|
||||
const data = lowerer.lowerExpr(call.arguments[0]!);
|
||||
if (data.type.kind === "string") {
|
||||
return { kind: "libCall", fn: "writer.writeString", args: [receiver, data], type: BOOL, loc };
|
||||
}
|
||||
if (data.type.kind === "bytes" && data.type.elem === "u8") {
|
||||
return { kind: "libCall", fn: "writer.writeBytes", args: [receiver, data], type: BOOL, loc };
|
||||
}
|
||||
lowerer.noLowering(
|
||||
`child stdin write of '${lowerer.fmt(data.type)}'`,
|
||||
call.arguments[0]!,
|
||||
"write one string, Buffer, or Uint8Array value (narrow unions first)",
|
||||
);
|
||||
}
|
||||
|
||||
if ((name === "end" || name === "destroy") && call.arguments.length === 0) {
|
||||
if (!ts.isExpressionStatement(call.parent)) {
|
||||
lowerer.unsupported("SC1090", call, `chaining child.stdin.${name}() (call it as its own statement)`);
|
||||
}
|
||||
const receiver = lowerer.lowerExprExpecting(access.expression, CHILDWRITER_T);
|
||||
return { kind: "libCall", fn: name === "end" ? "writer.end" : "writer.destroy", args: [receiver], type: VOID, loc };
|
||||
}
|
||||
|
||||
if ((name === "on" || name === "once") && call.arguments.length === 2) {
|
||||
const eventType = lowerer.typeOf(call.arguments[0]!);
|
||||
const event = eventType.isStringLiteralType() ? eventType.value : null;
|
||||
if (event !== "drain" && event !== "finish" && event !== "error") {
|
||||
lowerer.noLowering(
|
||||
`child stdin ${name}(${event === null ? "non-literal event" : `"${event}"`}, ...)`,
|
||||
call.arguments[0]!,
|
||||
'"drain", "finish", and "error" are the supported child stdin events',
|
||||
);
|
||||
}
|
||||
if (!ts.isExpressionStatement(call.parent)) {
|
||||
lowerer.unsupported("SC1090", call, "chaining child stdin listener registration");
|
||||
}
|
||||
const receiver = lowerer.lowerExprExpecting(access.expression, CHILDWRITER_T);
|
||||
const cb = lowerer.lowerExpr(call.arguments[1]!);
|
||||
const maxParams = event === "error" ? 1 : 0;
|
||||
if (cb.type.kind !== "func" || cb.type.ret.kind !== "void" || cb.type.params.length > maxParams) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
call.arguments[1]!,
|
||||
event === "error"
|
||||
? "error listeners with at most one Error parameter and no return value"
|
||||
: `${event} listeners with no parameters or return value`,
|
||||
);
|
||||
}
|
||||
if (event === "error" && cb.type.params[0] !== undefined &&
|
||||
!(cb.type.params[0]!.kind === "object" && cb.type.params[0]!.className === "%Error")) {
|
||||
lowerer.unsupported("SC1090", call.arguments[1]!, "child stdin error listeners whose parameter is Error");
|
||||
}
|
||||
const once = boolLit(name === "once", loc);
|
||||
const fn = event === "drain" ? "writer.onDrain" : event === "finish" ? "writer.onFinish" : "writer.onError";
|
||||
return { kind: "libCall", fn, args: [receiver, cb, once], type: VOID, loc };
|
||||
}
|
||||
|
||||
lowerer.noLowering(
|
||||
`Writable.${name}`,
|
||||
call,
|
||||
'write(string | Uint8Array), end(), destroy(), and on/once("drain" | "finish" | "error", cb) are supported',
|
||||
lowerer.checker.getSymbolAtLocation(access.name),
|
||||
);
|
||||
}
|
||||
|
||||
/** Method calls on first-class process-stream receivers (procStream —
|
||||
* a WritableStream-typed value like prefixStream's `output` param):
|
||||
* write(data) with one string, dispatched at runtime onto the exact
|
||||
|
||||
@@ -19,7 +19,7 @@ import { mixinFnShapeOf } from "./lower-mixins.js";
|
||||
import { dynStringReceiver, lowerArrayFromCall, lowerDynArrayFilterCall, lowerDynArrayFlatMapCall, lowerGroupByStaticCall, lowerIteratorHelperCall, lowerObjectAssignIndexShape, lowerObjectFromEntriesCall, lowerObjectIterOverIndexShape, lowerTupleReadMethodCall } from "./lower-containers.js";
|
||||
import { bufEncoding } from "./containers/bytes.js";
|
||||
import { lowerRegexMethodCall, lowerStringMethodCall } from "./containers/string-and-regexp.js";
|
||||
import { lowerChildStreamMethodCall, lowerCreateRequireCall, lowerCryptoHashMethodCall, lowerDirentMethodCall, lowerFileHandleMethodCall, lowerImportMetaResolveCall, lowerPerfHooksCall, lowerProcStreamMethodCall, lowerReflectApplyCall, lowerRequireResolveCall, lowerWatcherMethodCall } from "./lower-builtins.js";
|
||||
import { lowerChildStreamMethodCall, lowerChildWriterMethodCall, lowerCreateRequireCall, lowerCryptoHashMethodCall, lowerDirentMethodCall, lowerFileHandleMethodCall, lowerImportMetaResolveCall, lowerPerfHooksCall, lowerProcStreamMethodCall, lowerReflectApplyCall, lowerRequireResolveCall, lowerWatcherMethodCall } from "./lower-builtins.js";
|
||||
import { lowerAbsenceProbe, lowerPromiseAllTupleCall, lowerPromiseRejectCall, templateRawTextOf } from "./lower-exprs.js";
|
||||
import { isSafeToDiscard } from "./expressions/evaluation-safety.js";
|
||||
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
|
||||
@@ -4341,6 +4341,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
lowerer.lowerChildMethodCall(expr, expr.expression) ??
|
||||
// Piped child-output stream receivers — on/once("data" | "end").
|
||||
lowerChildStreamMethodCall(lowerer, expr, expr.expression) ??
|
||||
lowerChildWriterMethodCall(lowerer, expr, expr.expression) ??
|
||||
// First-class process-stream receivers — write(data).
|
||||
lowerProcStreamMethodCall(lowerer, expr, expr.expression) ??
|
||||
// FSWatcher receivers — close() (fs.watch's handle).
|
||||
|
||||
@@ -6824,7 +6824,7 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
case "netServer": case "netSocket": case "httpReq": case "httpRes":
|
||||
case "httpClientReq": case "secureCtx": case "fsWatcher":
|
||||
case "cryptoHash": case "cryptoHmac":
|
||||
case "childStream": case "procStream":
|
||||
case "childStream": case "childWriter": case "procStream":
|
||||
case "object": case "record": case "promise":
|
||||
return "object";
|
||||
default:
|
||||
|
||||
@@ -513,7 +513,7 @@ export function provenanceElidedConstDecl(lowerer: Lowerer, decl: ts.VariableDec
|
||||
// shape is the const's own initializer capturing the const:
|
||||
// `const server = http.createServer(h).listen(0, done)` with `done`
|
||||
// reading `server` — a TDZ read until the assign completes, JS exactly.
|
||||
"child", "childStream", "netServer", "netSocket", "dgramSocket", "testCtx",
|
||||
"child", "childStream", "childWriter", "netServer", "netSocket", "dgramSocket", "testCtx",
|
||||
"httpReq", "httpRes", "httpClientReq",
|
||||
// The h2 handles — `const client = http2.connect(url, mustCall(() =>
|
||||
// ...client...))` is the suite's canonical self-capturing const.
|
||||
|
||||
@@ -533,6 +533,8 @@ export function formatIrType(t: IrType, shapes: ShapeRegistry, unions: UnionRegi
|
||||
return "FSWatcher";
|
||||
case "childStream":
|
||||
return "Readable";
|
||||
case "childWriter":
|
||||
return "Writable";
|
||||
case "procStream":
|
||||
return "WriteStream";
|
||||
case "promise":
|
||||
@@ -1225,7 +1227,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
// no class identity of its own.
|
||||
if (widened.isIntersectionType()) {
|
||||
const HANDLE_KINDS = new Set([
|
||||
"netServer", "netSocket", "httpReq", "httpRes", "httpClientReq", "dgramSocket",
|
||||
"netServer", "netSocket", "httpReq", "httpRes", "httpClientReq", "dgramSocket", "childWriter",
|
||||
// process.stdout's own type IS the refined intersection
|
||||
// `WriteStream & { fd: 1 }` — the scalar stream kind rides the same
|
||||
// refinement rule.
|
||||
@@ -1805,14 +1807,19 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
) {
|
||||
return { kind: "spawnRes" };
|
||||
}
|
||||
// child_process.ChildProcess: @types/node's class or the fallback
|
||||
// declarations' interface. Provenance-checked like Stats.
|
||||
// child_process.ChildProcess and @types/node's stdio-refined return
|
||||
// interfaces: all carry the same runtime child handle; the generic
|
||||
// refinements only tell the checker which stdio properties are null.
|
||||
if (
|
||||
psym?.name === "ChildProcess" &&
|
||||
psym !== undefined &&
|
||||
(psym.name === "ChildProcess" ||
|
||||
psym.name === "ChildProcessWithoutNullStreams" ||
|
||||
psym.name === "ChildProcessByStdio") &&
|
||||
checker.declarationsOf(psym).some(
|
||||
(d) =>
|
||||
(ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
|
||||
ctx.isStdlibFile(d.getSourceFile()),
|
||||
ctx.isStdlibFile(d.getSourceFile()) &&
|
||||
isDeclaredInAmbientModule(d, "child_process"),
|
||||
)
|
||||
) {
|
||||
return { kind: "child" };
|
||||
@@ -1925,6 +1932,29 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
) {
|
||||
return PROCSTREAM_T;
|
||||
}
|
||||
// A piped child stdin writer. The fallback declarations give it the
|
||||
// dedicated ChildStdin name; @types/node spells the same value as
|
||||
// stream.Writable. As with stream.Readable below, unsupported producers
|
||||
// of the broad @types class fence where they are constructed.
|
||||
if (
|
||||
(psym?.name === "ChildStdin" &&
|
||||
checker.declarationsOf(psym).some(
|
||||
(d) =>
|
||||
ts.isInterfaceDeclaration(d) &&
|
||||
ctx.isStdlibFile(d.getSourceFile()) &&
|
||||
isDeclaredInAmbientModule(d, "child_process"),
|
||||
)) ||
|
||||
(psym?.name === "Writable" &&
|
||||
checker.declarationsOf(psym).some(
|
||||
(d) =>
|
||||
(ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
|
||||
ctx.isStdlibFile(d.getSourceFile()) &&
|
||||
isNodeTypesPath(d.getSourceFile().fileName) &&
|
||||
isDeclaredInAmbientModule(d, "stream"),
|
||||
))
|
||||
) {
|
||||
return { kind: "childWriter" };
|
||||
}
|
||||
// The static node:stream classes (the shipped fallback declarations —
|
||||
// NOT @types/node, whose stream.Readable also types child stdio; under
|
||||
// @types/node the childStream mapping below keeps priority and the
|
||||
@@ -2589,7 +2619,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
// NgrokChildProcess idiom — `spawn(...) as NgrokChildProcess`, declared
|
||||
// so tests can hand in mocks): every member is named on the lowered
|
||||
// ChildProcess surface AND at least two are the handle-defining ones
|
||||
// (kill/on/stdout/stderr/unref/exitCode — a data record like
|
||||
// (kill/on/stdin/stdout/stderr/unref/exitCode — a data record like
|
||||
// `{ pid: number }` never qualifies). The TYPE maps to the child
|
||||
// handle: in a compiled program the only VALUE producer is spawn
|
||||
// itself (a mock object literal in this slot fences at its
|
||||
@@ -2607,9 +2637,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
) {
|
||||
const CHILD_SURFACE = new Set([
|
||||
"pid", "exitCode", "killed", "kill", "on", "once", "off",
|
||||
"removeListener", "unref", "ref", "stdout", "stderr",
|
||||
"removeListener", "unref", "ref", "stdin", "stdout", "stderr",
|
||||
]);
|
||||
const CHILD_CORE = new Set(["kill", "on", "stdout", "stderr", "unref", "exitCode"]);
|
||||
const CHILD_CORE = new Set(["kill", "on", "stdin", "stdout", "stderr", "unref", "exitCode"]);
|
||||
const props = checker.getPropertiesOfType(widened);
|
||||
let core = 0;
|
||||
const childShaped =
|
||||
|
||||
@@ -383,6 +383,7 @@ function moduleWasiUnavailableSurface(mod: IrModule): { surface: string; loc: Sr
|
||||
["child", "child processes (WASI Preview 1 has no process-spawning API)"],
|
||||
["spawnRes", "child processes (WASI Preview 1 has no process-spawning API)"],
|
||||
["childStream", "child processes (WASI Preview 1 has no process-spawning API)"],
|
||||
["childWriter", "child processes (WASI Preview 1 has no process-spawning API)"],
|
||||
["netServer", "network sockets (WASI Preview 1 has no socket API)"],
|
||||
["netSocket", "network sockets (WASI Preview 1 has no socket API)"],
|
||||
["http2Session", "network sockets (WASI Preview 1 has no socket API)"],
|
||||
|
||||
@@ -186,6 +186,11 @@ export type IrType =
|
||||
* rules as child: union arms fine (the checker's `Readable | null`),
|
||||
* arrays/maps/JSON fenced. */
|
||||
| { kind: "childStream" }
|
||||
/** A piped child-input stream (child.stdin — spawn with a piped stdin
|
||||
* slot; scr_child.c). Heap, refcounted, and mutable: writes queue into
|
||||
* the platform pipe without blocking the JavaScript thread, and
|
||||
* drain/finish/error listeners drop when the writer settles. */
|
||||
| { kind: "childWriter" }
|
||||
/** A process output stream as a FIRST-CLASS value (process.stdout /
|
||||
* process.stderr flowing into a `NodeJS.WritableStream` slot — the
|
||||
* prefixStream idiom). Representation is the raw FD as a double (1 or
|
||||
@@ -338,6 +343,7 @@ const POINTER_HANDLE_KINDS = [
|
||||
"cryptoHmac",
|
||||
"fsWatcher",
|
||||
"childStream",
|
||||
"childWriter",
|
||||
] as const satisfies readonly IrType["kind"][];
|
||||
|
||||
interface IrKindSet<K extends IrType["kind"]> extends ReadonlySet<IrType["kind"]> {
|
||||
@@ -415,6 +421,7 @@ export const RUNTIME_RC_STEMS: Record<IrType["kind"], string> = {
|
||||
httpRes: "scr_http_res",
|
||||
httpClientReq: "scr_http_client",
|
||||
childStream: "scr_child_stream",
|
||||
childWriter: "scr_child_writer",
|
||||
procStream: "",
|
||||
fsWatcher: "scr_watcher",
|
||||
secureCtx: "scr_secure_ctx",
|
||||
@@ -458,7 +465,7 @@ export const REF_TRUTHY_KINDS: ReadonlySet<string> = new Set([
|
||||
"symbol",
|
||||
"date", "array", "map", "set", "regex", "url", "searchParams", "stats", "fileHandle", "spawnRes", "child",
|
||||
"netServer", "netSocket", "http2Session", "http2Stream", "dgramSocket", "testCtx", "httpReq", "httpRes", "httpClientReq",
|
||||
"secureCtx", "cryptoHash", "cryptoHmac", "fsWatcher", "childStream", "procStream", "bytes", "func", "object", "record", "promise",
|
||||
"secureCtx", "cryptoHash", "cryptoHmac", "fsWatcher", "childStream", "childWriter", "procStream", "bytes", "func", "object", "record", "promise",
|
||||
// A generator object is a JS object: always truthy.
|
||||
"generator",
|
||||
// A class object is a JS object (constructors are functions): always truthy.
|
||||
@@ -492,6 +499,7 @@ export const CRYPTOHASH_T: IrType = { kind: "cryptoHash" };
|
||||
export const CRYPTOHMAC_T: IrType = { kind: "cryptoHmac" };
|
||||
export const FSWATCHER_T: IrType = { kind: "fsWatcher" };
|
||||
export const CHILDSTREAM_T: IrType = { kind: "childStream" };
|
||||
export const CHILDWRITER_T: IrType = { kind: "childWriter" };
|
||||
export const PROCSTREAM_T: IrType = { kind: "procStream" };
|
||||
export const VOID: IrType = { kind: "void" };
|
||||
export const DYN: IrType = { kind: "dyn" };
|
||||
@@ -2341,10 +2349,9 @@ export type IrLibFn =
|
||||
* the backend (the envGet convention). Never throws. */
|
||||
| "spawnRes.error"
|
||||
/** child_process.spawn (scr_child.c + the scr_async.c loop): posix_spawnp
|
||||
* with stdio "ignore" (all three fds on /dev/null — the only supported
|
||||
* stdio; "pipe"/"inherit" are frontend-fenced), the child registered
|
||||
* with the event loop, which polls waitpid(WNOHANG) at quiescence like
|
||||
* timers (kqueue is the follow-up; SEMANTICS.md documents the polling).
|
||||
* with per-slot ignore/inherit/pipe modes (and number output fds), the
|
||||
* all-piped Node default, and the child registered with the event loop,
|
||||
* which polls waitpid(WNOHANG) at quiescence like timers.
|
||||
* NEVER throws: spawn failure defers to the "error" event, Node-exact
|
||||
* (the error message is Node's "spawn <cmd> <ERRNO-NAME>"; an "error"
|
||||
* event with no listener prints it and exits 1 like an EventEmitter).
|
||||
@@ -2402,6 +2409,19 @@ export type IrLibFn =
|
||||
| "child.stderr"
|
||||
| "stream.onData"
|
||||
| "stream.onEnd"
|
||||
/** The piped-input writer (stdio mode 3 on fd 0). child.stdin answers
|
||||
* `Writable | null`; writes copy borrowed data into the nonblocking
|
||||
* queue, end/destroy settle it, writable is a pure state read, and
|
||||
* listener callbacks move into the writer registry. */
|
||||
| "child.stdin"
|
||||
| "writer.writeString"
|
||||
| "writer.writeBytes"
|
||||
| "writer.end"
|
||||
| "writer.destroy"
|
||||
| "writer.writable"
|
||||
| "writer.onDrain"
|
||||
| "writer.onFinish"
|
||||
| "writer.onError"
|
||||
/** The first-class WritableStream write (`output.write(line)` — the
|
||||
* prefixStream idiom): the receiver IS the fd scalar (process.stdout/
|
||||
* stderr reads mint 1/2), dispatched onto the exact stdoutWrite/
|
||||
@@ -6450,12 +6470,12 @@ export function moduleUsesChildProcess(mod: IrModule): boolean {
|
||||
if (
|
||||
node.kind === "libCall" &&
|
||||
typeof node.fn === "string" &&
|
||||
(node.fn.startsWith("cp.") || node.fn.startsWith("child.") || node.fn.startsWith("spawnRes."))
|
||||
(node.fn.startsWith("cp.") || node.fn.startsWith("child.") || node.fn.startsWith("writer.") || node.fn.startsWith("spawnRes."))
|
||||
) {
|
||||
found = true;
|
||||
return;
|
||||
}
|
||||
if (node.kind === "child" || node.kind === "childStream" || node.kind === "spawnRes") {
|
||||
if (node.kind === "child" || node.kind === "childStream" || node.kind === "childWriter" || node.kind === "spawnRes") {
|
||||
found = true;
|
||||
return;
|
||||
}
|
||||
@@ -6903,6 +6923,7 @@ const LIB_MODE_REFUSED_KINDS: ReadonlyMap<string, string> = new Map([
|
||||
["child", "the child_process surface"],
|
||||
["spawnRes", "the child_process surface"],
|
||||
["childStream", "the child_process surface"],
|
||||
["childWriter", "the child_process surface"],
|
||||
["netServer", "the node:net surface"],
|
||||
["netSocket", "the node:net surface"],
|
||||
["http2Session", "the node:http2 surface"],
|
||||
|
||||
@@ -18,7 +18,7 @@ import type {
|
||||
IrUnionDef,
|
||||
SrcLoc,
|
||||
} from "./ir.js";
|
||||
import { arrayOf, BOOL, BYTES_U8, bytesOf, canAdaptDynFuncTo, canConvertToDyn, canExitIslandToType, canMarshalIntoIsland, canMarshalTypedFuncIntoIsland, CHILD_T, CHILDSTREAM_T, CRYPTOHASH_T, CRYPTOHMAC_T, DATE_T, DGRAMSOCK_T, DYN, DYN_HANDLE_KINDS, F64, ffiClassType, ffiSourceParamTypes, FILEHANDLE_T, FSWATCHER_T, HTTP2SESSION_T, HTTP2STREAM_T, HTTPCLIENTREQ_T, HTTPREQ_T, HTTPRES_T, islandPromisePayloadTag, isDynTypedRefType, isFfiCallbackParam, isFfiContextParam, isFfiReleaseParam, isJsonSafeType, isJsonStringifySafeType, isRefCounted, isSupportedArrayElem, isSupportedIndexValue, isSupportedMapKey, isSupportedMapValue, isSupportedSetElem, isUnitType, jsOpResultKind, JSVAL, NETSERVER_T, NETSOCKET_T, PROCSTREAM_T, REF_TRUTHY_KINDS, REGEX, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, SEARCH_PARAMS_T, SECURECTX_T, shapeHasAccessorSlots, SPAWNRES_T, STATS_T, STRING, SYMBOL_T, TESTCTX_T, typeEquals, typeKey, unionFuncSetArmsOk, URL_T, VOID } from "./ir.js";
|
||||
import { arrayOf, BOOL, BYTES_U8, bytesOf, canAdaptDynFuncTo, canConvertToDyn, canExitIslandToType, canMarshalIntoIsland, canMarshalTypedFuncIntoIsland, CHILD_T, CHILDSTREAM_T, CHILDWRITER_T, CRYPTOHASH_T, CRYPTOHMAC_T, DATE_T, DGRAMSOCK_T, DYN, DYN_HANDLE_KINDS, F64, ffiClassType, ffiSourceParamTypes, FILEHANDLE_T, FSWATCHER_T, HTTP2SESSION_T, HTTP2STREAM_T, HTTPCLIENTREQ_T, HTTPREQ_T, HTTPRES_T, islandPromisePayloadTag, isDynTypedRefType, isFfiCallbackParam, isFfiContextParam, isFfiReleaseParam, isJsonSafeType, isJsonStringifySafeType, isRefCounted, isSupportedArrayElem, isSupportedIndexValue, isSupportedMapKey, isSupportedMapValue, isSupportedSetElem, isUnitType, jsOpResultKind, JSVAL, NETSERVER_T, NETSOCKET_T, PROCSTREAM_T, REF_TRUTHY_KINDS, REGEX, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, SEARCH_PARAMS_T, SECURECTX_T, shapeHasAccessorSlots, SPAWNRES_T, STATS_T, STRING, SYMBOL_T, TESTCTX_T, typeEquals, typeKey, unionFuncSetArmsOk, URL_T, VOID } from "./ir.js";
|
||||
|
||||
/** Per-method signature for strIntrinsic: `argTypes` lists every argument
|
||||
* position (optional ones included); `minArgs` is how many may be omitted
|
||||
@@ -729,11 +729,20 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
|
||||
// case checks the arms — the child.pid pattern with a ref arm).
|
||||
"child.stdout": { argTypes: [CHILD_T], result: VOID },
|
||||
"child.stderr": { argTypes: [CHILD_T], result: VOID },
|
||||
"child.stdin": { argTypes: [CHILD_T], result: VOID },
|
||||
// Listener registrations: callback func shapes are program-dependent
|
||||
// (zero-param / Buffer / Buffer-armed union) — the slots pin arity,
|
||||
// the stream receiver, and the once-flag.
|
||||
"stream.onData": { argTypes: [CHILDSTREAM_T, null, BOOL], result: VOID },
|
||||
"stream.onEnd": { argTypes: [CHILDSTREAM_T, null, BOOL], result: VOID },
|
||||
"writer.writeString": { argTypes: [CHILDWRITER_T, STRING], result: BOOL },
|
||||
"writer.writeBytes": { argTypes: [CHILDWRITER_T, BYTES_U8], result: BOOL },
|
||||
"writer.end": { argTypes: [CHILDWRITER_T], result: VOID },
|
||||
"writer.destroy": { argTypes: [CHILDWRITER_T], result: VOID },
|
||||
"writer.writable": { argTypes: [CHILDWRITER_T], result: BOOL },
|
||||
"writer.onDrain": { argTypes: [CHILDWRITER_T, null, BOOL], result: VOID },
|
||||
"writer.onFinish": { argTypes: [CHILDWRITER_T, null, BOOL], result: VOID },
|
||||
"writer.onError": { argTypes: [CHILDWRITER_T, null, BOOL], result: VOID },
|
||||
"procStream.write": { argTypes: [PROCSTREAM_T, STRING], result: BOOL },
|
||||
"child.killed": { argTypes: [CHILD_T], result: BOOL },
|
||||
"child.kill": { argTypes: [CHILD_T, STRING], result: BOOL },
|
||||
@@ -4352,6 +4361,17 @@ function validateFunction(
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (e.fn === "writer.onDrain" || e.fn === "writer.onFinish" || e.fn === "writer.onError") {
|
||||
const cbT = e.args[1]?.type;
|
||||
const maxParams = e.fn === "writer.onError" ? 1 : 0;
|
||||
let ok = cbT?.kind === "func" && cbT.ret.kind === "void" && cbT.params.length <= maxParams;
|
||||
if (ok && cbT?.kind === "func" && cbT.params.length === 1) {
|
||||
const p = cbT.params[0]!;
|
||||
ok = p.kind === "object" && p.className === "%Error";
|
||||
}
|
||||
if (!ok) err(`libCall ${e.fn} callback shape (frontend must fence)`, e.loc);
|
||||
break;
|
||||
}
|
||||
if (e.fn === "process.onExit" || e.fn === "process.offExit" ||
|
||||
e.fn === "stdin.onData" || e.fn === "stdin.onError") {
|
||||
// The listener: a void closure with no params, or exactly the
|
||||
@@ -4478,15 +4498,16 @@ function validateFunction(
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (e.fn === "child.stdout" || e.fn === "child.stderr") {
|
||||
if (e.fn === "child.stdin" || e.fn === "child.stdout" || e.fn === "child.stderr") {
|
||||
const def = e.type.kind === "union" ? unions.get(e.type.unionId) : undefined;
|
||||
const handleKind = e.fn === "child.stdin" ? "childWriter" : "childStream";
|
||||
const ok =
|
||||
def &&
|
||||
def.arms.length === 2 &&
|
||||
def.arms.some((a) => a.kind === "childStream") &&
|
||||
def.arms.some((a) => a.kind === handleKind) &&
|
||||
def.arms.some((a) => a.kind === "nullT");
|
||||
if (!ok) {
|
||||
err(`libCall ${e.fn} must return the 'Readable | null' union`, e.loc);
|
||||
err(`libCall ${e.fn} must return the '${e.fn === "child.stdin" ? "Writable" : "Readable"} | null' union`, e.loc);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -6211,6 +6211,48 @@
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/2892-windows-console-unicode.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/2892-windows-console-unicode.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/2893-child-stdin-duplex.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/2893-child-stdin-duplex.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/2894-child-stdin-backpressure.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/2894-child-stdin-backpressure.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/2895-child-stdin-write-after-end.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/2895-child-stdin-write-after-end.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/2896-child-stdin-write-after-finish.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/2896-child-stdin-write-after-finish.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/2897-child-stdin-destroy.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/2897-child-stdin-destroy.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/2898-child-stdin-spawn-failure.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/2898-child-stdin-spawn-failure.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/300-if-else.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/300-if-else.ts"
|
||||
@@ -8937,6 +8979,18 @@
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/fixtures/node-types/child-stdin-fences.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/node-types/child-stdin-fences.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/fixtures/node-types/child-stdin.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/node-types/child-stdin.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/fixtures/node-types/fenced.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/node-types/fenced.ts"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2624,6 +2624,13 @@ typedef struct ScrChild ScrChild;
|
||||
* own 'exit' (Node's pinned ordering; the settle path drains first). A
|
||||
* flowing stream (data consumer, not yet EOF) keeps the loop alive. */
|
||||
typedef struct ScrChildStream ScrChildStream;
|
||||
/* A piped child-input writer (child.stdin — stdio mode 3 on fd 0).
|
||||
* write copies borrowed string/bytes data into a nonblocking queue and
|
||||
* returns false once buffered data reaches the stream high-water mark;
|
||||
* drain fires when that queue empties. end closes after queued data and
|
||||
* then fires finish; destroy closes immediately without finish. Errors
|
||||
* are asynchronous and use the child error adapters. */
|
||||
typedef struct ScrChildWriter ScrChildWriter;
|
||||
/* The data adapter: the chunk arrives BORROWED (multiple listeners see
|
||||
* the same chunk); adapters retain what they keep. */
|
||||
typedef void (*ScrChildStreamDataFn)(ScrClosure *cb, ScrBytes *chunk);
|
||||
@@ -2638,7 +2645,8 @@ ScrChild *scr_spawn(ScrStr *cmd, ScrArr *args); /* +1, never throws */
|
||||
/* The options form (cp.spawnOpts): PER-SLOT stdio modes — 0 = ignore
|
||||
* (/dev/null), 1 = inherit, 2 = fd (out/err only: out_fd/err_fd dup2
|
||||
* into the child's slot, the openSync daemon-log idiom), 3 = pipe
|
||||
* (out/err only: the read end becomes the child.stdout/stderr stream);
|
||||
* (stdin's write end becomes child.stdin; out/err read ends become the
|
||||
* child.stdout/stderr streams);
|
||||
* detached = POSIX_SPAWN_SETSID (own session/process group); env
|
||||
* REPLACES the child environment when has_env ([k,v,...] pairs); cwd ""
|
||||
* = inherit. */
|
||||
@@ -2673,6 +2681,20 @@ void scr_child_stream_on_data(ScrChildStream *s, ScrClosure *cb /*moves*/,
|
||||
void scr_child_stream_on_end(ScrChildStream *s, ScrClosure *cb /*moves*/, bool once);
|
||||
void scr_child_stream_thunk0(ScrClosure *cb, ScrBytes *chunk);
|
||||
void scr_child_stream_thunk_bytes(ScrClosure *cb, ScrBytes *chunk);
|
||||
ScrChildWriter *scr_child_writer_retain(ScrChildWriter *w);
|
||||
void scr_child_writer_release(ScrChildWriter *w);
|
||||
void *scr_child_writer_retain_v(void *p);
|
||||
void scr_child_writer_release_v(void *p);
|
||||
ScrChildWriter *scr_child_stdin(ScrChild *c); /* +1, or NULL */
|
||||
bool scr_child_writer_write_string(ScrChildWriter *w, const ScrStr *data);
|
||||
bool scr_child_writer_write_bytes(ScrChildWriter *w, const ScrBytes *data);
|
||||
void scr_child_writer_end(ScrChildWriter *w);
|
||||
void scr_child_writer_destroy(ScrChildWriter *w);
|
||||
bool scr_child_writer_writable(ScrChildWriter *w);
|
||||
void scr_child_writer_on_drain(ScrChildWriter *w, ScrClosure *cb /*moves*/, bool once);
|
||||
void scr_child_writer_on_finish(ScrChildWriter *w, ScrClosure *cb /*moves*/, bool once);
|
||||
void scr_child_writer_on_error(ScrChildWriter *w, ScrClosure *cb /*moves*/,
|
||||
ScrChildErrFn fn, bool once);
|
||||
/* The lifecycle members, Node's exact shapes (pinned by the corpus):
|
||||
* pid is undefined (has_pid false) exactly on spawn failure; exitCode is
|
||||
* null while running / after a signal death, the code after a normal
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { spawn } from "node:child_process";
|
||||
|
||||
const child = spawn("node", ["-e", "process.stdin.on('data', b => process.stdout.write(b));"]);
|
||||
const input = child.stdin;
|
||||
const output = child.stdout;
|
||||
const errors = child.stderr;
|
||||
if (input === null || output === null || errors === null) throw new Error("missing pipe");
|
||||
|
||||
let echoed = "";
|
||||
output.on("data", (chunk) => { echoed += chunk.toString(); });
|
||||
output.on("end", () => { console.log("echo", JSON.stringify(echoed)); });
|
||||
errors.on("data", (chunk) => { process.stderr.write(chunk); });
|
||||
input.on("finish", () => { console.log("finish", input.writable); });
|
||||
input.on("error", (err) => { console.log("error", err.message); });
|
||||
console.log("initial", input.writable);
|
||||
console.log("writes", input.write("hé"), input.write(new Uint8Array([108, 108, 111, 10])));
|
||||
input.end();
|
||||
console.log("ended", input.writable);
|
||||
child.on("exit", (code) => { console.log("exit", code); });
|
||||
@@ -0,0 +1,21 @@
|
||||
import { spawn } from "node:child_process";
|
||||
|
||||
const child = spawn("node", ["-e", "let n=0;setTimeout(()=>{process.stdin.on('data',b=>n+=b.length);process.stdin.on('end',()=>console.log(n));process.stdin.resume()},50)"], {
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
const input = child.stdin;
|
||||
const output = child.stdout;
|
||||
if (input === null || output === null) throw new Error("missing pipe");
|
||||
|
||||
let count = "";
|
||||
output.on("data", (chunk) => { count += chunk.toString(); });
|
||||
output.on("end", () => { console.log("count", count.trim()); });
|
||||
input.on("drain", () => {
|
||||
console.log("drain", input.writable);
|
||||
input.end();
|
||||
});
|
||||
input.on("finish", () => { console.log("finish", input.writable); });
|
||||
input.on("error", (err) => { console.log("error", err.message); });
|
||||
console.log("boundary", input.write(Buffer.alloc(64 * 1024 + 1, 66)));
|
||||
console.log("write", input.write(Buffer.alloc(1024 * 1024, 65)));
|
||||
child.on("exit", (code) => { console.log("exit", code); });
|
||||
@@ -0,0 +1,11 @@
|
||||
import { spawn } from "node:child_process";
|
||||
|
||||
const child = spawn("node", ["-e", "process.stdin.resume()"], { stdio: ["pipe", "ignore", "ignore"] });
|
||||
const input = child.stdin;
|
||||
if (input === null) throw new Error("missing pipe");
|
||||
|
||||
input.on("finish", () => { console.log("finish"); });
|
||||
input.on("error", (err) => { console.log("error", (err as NodeJS.ErrnoException).code, err.message, input.writable); });
|
||||
input.end();
|
||||
console.log("write", input.write("x"), input.writable);
|
||||
child.on("exit", (code) => { console.log("exit", code); });
|
||||
@@ -0,0 +1,13 @@
|
||||
import { spawn } from "node:child_process";
|
||||
|
||||
const child = spawn("node", ["-e", "process.stdin.resume()"], { stdio: ["pipe", "ignore", "ignore"] });
|
||||
const input = child.stdin;
|
||||
if (input === null) throw new Error("missing pipe");
|
||||
|
||||
input.on("error", (err) => { console.log("error", (err as NodeJS.ErrnoException).code, err.message, input.writable); });
|
||||
input.on("finish", () => {
|
||||
console.log("finish", input.writable);
|
||||
console.log("write", input.write("x"), input.writable);
|
||||
});
|
||||
input.end();
|
||||
child.on("exit", (code) => { console.log("exit", code); });
|
||||
@@ -0,0 +1,12 @@
|
||||
import { spawn } from "node:child_process";
|
||||
|
||||
const child = spawn("node", ["-e", "setTimeout(()=>{},25)"], { stdio: ["pipe", "ignore", "ignore"] });
|
||||
const input = child.stdin;
|
||||
if (input === null) throw new Error("missing pipe");
|
||||
|
||||
input.on("finish", () => { console.log("finish"); });
|
||||
input.on("error", (err) => { console.log("error", err.message); });
|
||||
console.log("before", input.writable);
|
||||
input.destroy();
|
||||
console.log("after", input.writable);
|
||||
child.on("exit", (code) => { console.log("exit", code); });
|
||||
@@ -0,0 +1,10 @@
|
||||
import { spawn } from "node:child_process";
|
||||
|
||||
const child = spawn("definitely-not-a-binary-child-stdin", []);
|
||||
const input = child.stdin;
|
||||
if (input === null) throw new Error("missing default stdin pipe");
|
||||
|
||||
console.log("initial", input.writable);
|
||||
child.on("error", (err) => {
|
||||
console.log("error", (err as NodeJS.ErrnoException).code, input.writable);
|
||||
});
|
||||
@@ -5,15 +5,18 @@
|
||||
|
||||
import { spawn } from "node:child_process";
|
||||
|
||||
// Node's default stdio is "pipe" (streams — no lowering): omitting the
|
||||
// options never silently loses the child's output.
|
||||
// The default and explicit "pipe" forms use piped streams.
|
||||
// Keep both adjacent to the remaining rejection cases.
|
||||
const noOpts = spawn("/bin/echo");
|
||||
|
||||
// "pipe" and "inherit" typecheck against the declared union but have no
|
||||
// lowering — each names its gap.
|
||||
// "inherit" and "ignore" remain process-only forms without child streams.
|
||||
// All four supported modes must coexist with the fences below.
|
||||
const piped = spawn("/bin/echo", [], { stdio: "pipe" });
|
||||
const inherited = spawn("/bin/echo", [], { stdio: "inherit" });
|
||||
|
||||
// A variable options value must fence instead of being dropped.
|
||||
const options: { stdio: "ignore"; detached: boolean } = { stdio: "ignore", detached: true };
|
||||
spawn("true", [], options);
|
||||
const c = spawn("true", [], { stdio: "ignore" });
|
||||
|
||||
// `() => 5` IS assignable to a void-returning listener slot and now ADOPTS
|
||||
@@ -21,6 +24,6 @@ const c = spawn("true", [], { stdio: "ignore" });
|
||||
// listener keeps its word and stays fenced — the registry calls listeners
|
||||
// as void.
|
||||
c.on("exit", (): number => 5);
|
||||
|
||||
// Methods have no bound-value form — call on directly.
|
||||
const f = c.on;
|
||||
// Keep each fence on its own statement so diagnostics remain site-specific.
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { spawn } from "node:child_process";
|
||||
|
||||
const options = { stdio: "pipe" as const, detached: true };
|
||||
spawn("node", options);
|
||||
spawn("node", [], options);
|
||||
|
||||
const child = spawn("node", []);
|
||||
child.stdin.write("x", "utf8");
|
||||
child.stdin.end("x");
|
||||
child.stdin.destroy(new Error("stop"));
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import { spawn } from "node:child_process";
|
||||
|
||||
const child = spawn("node", ["-e", "process.stdin.pipe(process.stdout)"]);
|
||||
let text = "";
|
||||
child.stdout.on("data", (chunk: Buffer) => { text += chunk.toString(); });
|
||||
child.stdout.on("end", () => { console.log(text); });
|
||||
child.stdin.write("typed child stdin");
|
||||
child.stdin.end();
|
||||
@@ -1,40 +1,31 @@
|
||||
child-process.ts:10:16 - error SC2020: 'spawn without { stdio: "ignore" }' is part of the standard library types but has no scriptc lowering yet
|
||||
child-process.ts:19:19 - error SC2020: 'spawn with a non-literal options argument' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
9 | // options never silently loses the child's output.
|
||||
10 | const noOpts = spawn("/bin/echo");
|
||||
| ^~~~~~~~~~~~~~~~~~
|
||||
11 |
|
||||
18 | const options: { stdio: "ignore"; detached: boolean } = { stdio: "ignore", detached: true };
|
||||
19 | spawn("true", [], options);
|
||||
| ^~~~~~~
|
||||
20 | const c = spawn("true", [], { stdio: "ignore" });
|
||||
|
||||
hint: Node's default stdio is "pipe" (streams, no lowering) — pass { stdio: "ignore" } or { stdio: "inherit" } explicitly, or capture with spawnSync
|
||||
hint: pass stdio, detached, env, cwd, and windowsHide in an inline object literal
|
||||
|
||||
child-process.ts:14:47 - error SC2020: 'spawn with stdio: "pipe"' is part of the standard library types but has no scriptc lowering yet
|
||||
child-process.ts:26:14 - error SC1090: listeners returning a value (make the callback body a block, or return nothing) is not supported yet
|
||||
|
||||
13 | // lowering — each names its gap.
|
||||
14 | const piped = spawn("/bin/echo", [], { stdio: "pipe" });
|
||||
| ^~~~~~
|
||||
15 | const inherited = spawn("/bin/echo", [], { stdio: "inherit" });
|
||||
|
||||
hint: piped STDIN has no lowering — pipe stdout/stderr with the tuple form (stdio: ["ignore", "pipe", "pipe"]), or capture with spawnSync
|
||||
|
||||
child-process.ts:23:14 - error SC1090: listeners returning a value (make the callback body a block, or return nothing) is not supported yet
|
||||
|
||||
22 | // as void.
|
||||
23 | c.on("exit", (): number => 5);
|
||||
25 | // as void.
|
||||
26 | c.on("exit", (): number => 5);
|
||||
| ^~~~~~~~~~~~~~~
|
||||
24 |
|
||||
27 | // Methods have no bound-value form — call on directly.
|
||||
|
||||
child-process.ts:26:7 - error SC2007: values of type '{ (event: "exit", listener: (code: number | null, signal: string | null) => void): void; (event: "error", listener: (err: Error) => void): void; }' cannot be compiled: the type declares multiple call signatures (overloads), and a compiled function value is always one concrete signature
|
||||
child-process.ts:28:7 - error SC2007: values of type '{ (event: "exit", listener: (code: number | null, signal: string | null) => void): void; (event: "error", listener: (err: Error) => void): void; }' cannot be compiled: the type declares multiple call signatures (overloads), and a compiled function value is always one concrete signature
|
||||
|
||||
25 | // Methods have no bound-value form — call on directly.
|
||||
26 | const f = c.on;
|
||||
27 | // Methods have no bound-value form — call on directly.
|
||||
28 | const f = c.on;
|
||||
| ^
|
||||
27 |
|
||||
29 | // Keep each fence on its own statement so diagnostics remain site-specific.
|
||||
|
||||
hint: annotate the slot with the ONE signature the program actually calls (e.g. '(x: number) => string'), or wrap the overloaded function in a single-signature arrow
|
||||
|
||||
child-process.ts:26:11 - error SC1090: child methods as values (call 'on' directly) are not supported yet
|
||||
child-process.ts:28:11 - error SC1090: child methods as values (call 'on' directly) are not supported yet
|
||||
|
||||
25 | // Methods have no bound-value form — call on directly.
|
||||
26 | const f = c.on;
|
||||
27 | // Methods have no bound-value form — call on directly.
|
||||
28 | const f = c.on;
|
||||
| ^~~~
|
||||
27 |
|
||||
29 | // Keep each fence on its own statement so diagnostics remain site-specific.
|
||||
@@ -102,6 +102,40 @@ test("node-types: captured NodeJS.WritableStream values write through the procSt
|
||||
expect(stderr).toBe("[err] line\n");
|
||||
});
|
||||
|
||||
test("node-types: refined spawn returns expose writable child stdin", async () => {
|
||||
const dir = outDirFor("node-child-stdin");
|
||||
const entry = join(nodeTypesDir, "child-stdin.ts");
|
||||
const result = await compile(entry, {
|
||||
outPath: join(dir, "child-stdin"),
|
||||
outDir: dir,
|
||||
sanitize,
|
||||
});
|
||||
expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true);
|
||||
if (!result.ok) return;
|
||||
const { stdout } = await execFileAsync(result.binaryPath);
|
||||
expect(stdout).toBe("typed child stdin\n");
|
||||
});
|
||||
|
||||
test("node-types: unsupported child stdin and spawn options forms remain named fences", async () => {
|
||||
const dir = outDirFor("node-child-stdin-fences");
|
||||
const entry = join(nodeTypesDir, "child-stdin-fences.ts");
|
||||
const result = await compile(entry, {
|
||||
outPath: join(dir, "child-stdin-fences"),
|
||||
outDir: dir,
|
||||
sanitize,
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(result.diagnostics.map((diagnostic) => diagnostic.code)).toEqual(["SC2020", "SC2020", "SC2020", "SC2020", "SC2020"]);
|
||||
expect(result.diagnostics.map((diagnostic) => diagnostic.message)).toEqual([
|
||||
expect.stringContaining("spawn with a non-literal options argument"),
|
||||
expect.stringContaining("spawn with a non-literal options argument"),
|
||||
expect.stringContaining("child stdin write with 2 arguments"),
|
||||
expect.stringContaining("Writable.end"),
|
||||
expect.stringContaining("Writable.destroy"),
|
||||
]);
|
||||
});
|
||||
|
||||
test("node-types: path and os lower statically under @types/node's shapes", async () => {
|
||||
const outDir = outDirFor("node-path-os");
|
||||
const result = await compile(join(nodeTypesDir, "path-os.ts"), {
|
||||
|
||||
Reference in New Issue
Block a user