summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarvin Hagemeister <hello@marvinh.dev>2023-05-18 14:02:14 +0200
committerGitHub <noreply@github.com>2023-05-18 14:02:14 +0200
commit695b5de6cb0cb4a10b95cbae99f2f19e5621a9eb (patch)
tree5ba8cef3ffb4cf97798879da88da6bf5e8f76f24
parent9dc3ae8523364b8df6b8e92346907d1020e80d33 (diff)
fix(node): support passing parent stdio streams (#19171)
This is a bit bare bones but gets `npm-run-all` working. For full stdio compatibility with node more work is needed which is probably better done in follow up PRs. Fixes #19159
-rw-r--r--cli/tests/unit_node/child_process_test.ts22
-rw-r--r--cli/tests/unit_node/testdata/child_process_stdio.js15
-rw-r--r--ext/node/polyfills/internal/child_process.ts50
3 files changed, 77 insertions, 10 deletions
diff --git a/cli/tests/unit_node/child_process_test.ts b/cli/tests/unit_node/child_process_test.ts
index b40cfd9ff..e89fd7d79 100644
--- a/cli/tests/unit_node/child_process_test.ts
+++ b/cli/tests/unit_node/child_process_test.ts
@@ -577,3 +577,25 @@ Deno.test(
assertStringIncludes(output, "typescript");
},
);
+
+Deno.test(
+ "[node/child_process spawn] supports stdio array option",
+ async () => {
+ const cmdFinished = deferred();
+ let output = "";
+ const script = path.join(
+ path.dirname(path.fromFileUrl(import.meta.url)),
+ "testdata",
+ "child_process_stdio.js",
+ );
+ const cp = spawn(Deno.execPath(), ["run", "-A", script]);
+ cp.stdout?.on("data", (data) => {
+ output += data;
+ });
+ cp.on("close", () => cmdFinished.resolve());
+ await cmdFinished;
+
+ assertStringIncludes(output, "foo");
+ assertStringIncludes(output, "close");
+ },
+);
diff --git a/cli/tests/unit_node/testdata/child_process_stdio.js b/cli/tests/unit_node/testdata/child_process_stdio.js
new file mode 100644
index 000000000..399b890ed
--- /dev/null
+++ b/cli/tests/unit_node/testdata/child_process_stdio.js
@@ -0,0 +1,15 @@
+import childProcess from "node:child_process";
+import process from "node:process";
+import * as path from "node:path";
+
+const script = path.join(
+ path.dirname(path.fromFileUrl(import.meta.url)),
+ "node_modules",
+ "foo",
+ "index.js",
+);
+
+const child = childProcess.spawn(process.execPath, [script], {
+ stdio: [process.stdin, process.stdout, process.stderr],
+});
+child.on("close", () => console.log("close"));
diff --git a/ext/node/polyfills/internal/child_process.ts b/ext/node/polyfills/internal/child_process.ts
index edc4caa5e..365af4add 100644
--- a/ext/node/polyfills/internal/child_process.ts
+++ b/ext/node/polyfills/internal/child_process.ts
@@ -177,9 +177,9 @@ export class ChildProcess extends EventEmitter {
args: cmdArgs,
cwd,
env: stringEnv,
- stdin: toDenoStdio(stdin as NodeStdio | number),
- stdout: toDenoStdio(stdout as NodeStdio | number),
- stderr: toDenoStdio(stderr as NodeStdio | number),
+ stdin: toDenoStdio(stdin),
+ stdout: toDenoStdio(stdout),
+ stderr: toDenoStdio(stderr),
windowsRawArguments: windowsVerbatimArguments,
}).spawn();
this.pid = this.#process.pid;
@@ -189,6 +189,16 @@ export class ChildProcess extends EventEmitter {
this.stdin = Writable.fromWeb(this.#process.stdin);
}
+ if (stdin instanceof Stream) {
+ this.stdin = stdin;
+ }
+ if (stdout instanceof Stream) {
+ this.stdout = stdout;
+ }
+ if (stderr instanceof Stream) {
+ this.stderr = stderr;
+ }
+
if (stdout === "pipe") {
assert(this.#process.stdout);
this.stdout = Readable.fromWeb(this.#process.stdout);
@@ -285,15 +295,22 @@ export class ChildProcess extends EventEmitter {
async #_waitForChildStreamsToClose() {
const promises = [] as Array<Promise<void>>;
- if (this.stdin && !this.stdin.destroyed) {
+ // Don't close parent process stdin if that's passed through
+ if (this.stdin && !this.stdin.destroyed && this.stdin !== process.stdin) {
assert(this.stdin);
this.stdin.destroy();
promises.push(waitForStreamToClose(this.stdin));
}
- if (this.stdout && !this.stdout.destroyed) {
+ // Only readable streams need to be closed
+ if (
+ this.stdout && !this.stdout.destroyed && this.stdout instanceof Readable
+ ) {
promises.push(waitForReadableToClose(this.stdout));
}
- if (this.stderr && !this.stderr.destroyed) {
+ // Only readable streams need to be closed
+ if (
+ this.stderr && !this.stderr.destroyed && this.stderr instanceof Readable
+ ) {
promises.push(waitForReadableToClose(this.stderr));
}
await Promise.all(promises);
@@ -317,9 +334,13 @@ const supportedNodeStdioTypes: NodeStdio[] = ["pipe", "ignore", "inherit"];
function toDenoStdio(
pipe: NodeStdio | number | Stream | null | undefined,
): DenoStdio {
+ if (pipe instanceof Stream) {
+ return "inherit";
+ }
+
if (
!supportedNodeStdioTypes.includes(pipe as NodeStdio) ||
- typeof pipe === "number" || pipe instanceof Stream
+ typeof pipe === "number"
) {
notImplemented(`toDenoStdio pipe=${typeof pipe} (${pipe})`);
}
@@ -441,8 +462,17 @@ function normalizeStdioOption(
"pipe",
"pipe",
],
-) {
+): [
+ Stream | NodeStdio | number,
+ Stream | NodeStdio | number,
+ Stream | NodeStdio | number,
+ ...Array<Stream | NodeStdio | number>,
+] {
if (Array.isArray(stdio)) {
+ // At least 3 stdio must be created to match node
+ while (stdio.length < 3) {
+ ArrayPrototypePush(stdio, undefined);
+ }
return stdio;
} else {
switch (stdio) {
@@ -796,8 +826,8 @@ export function spawnSync(
args,
cwd,
env,
- stdout: toDenoStdio(normalizedStdio[1] as NodeStdio | number),
- stderr: toDenoStdio(normalizedStdio[2] as NodeStdio | number),
+ stdout: toDenoStdio(normalizedStdio[1]),
+ stderr: toDenoStdio(normalizedStdio[2]),
uid,
gid,
windowsRawArguments: windowsVerbatimArguments,