diff --git a/src/js/node/child_process.ts b/src/js/node/child_process.ts index aa892ffe8c..510e4aca7b 100644 --- a/src/js/node/child_process.ts +++ b/src/js/node/child_process.ts @@ -550,11 +550,16 @@ function spawnSync(file, args, options) { stderr = null; } + // When stdio is redirected to a file descriptor, Bun.spawnSync returns the fd number + // instead of the actual output. We should treat this as no output available. + const outputStdout = typeof stdout === "number" ? null : stdout; + const outputStderr = typeof stderr === "number" ? null : stderr; + const result = { signal: signalCode ?? null, status: exitCode, // TODO: Need to expose extra pipes from Bun.spawnSync to child_process - output: [null, stdout, stderr], + output: [null, outputStdout, outputStderr], pid, }; @@ -562,11 +567,11 @@ function spawnSync(file, args, options) { result.error = error; } - if (stdout && encoding && encoding !== "buffer") { + if (outputStdout && encoding && encoding !== "buffer") { result.output[1] = result.output[1]?.toString(encoding); } - if (stderr && encoding && encoding !== "buffer") { + if (outputStderr && encoding && encoding !== "buffer") { result.output[2] = result.output[2]?.toString(encoding); } diff --git a/test/regression/issue/20321.test.ts b/test/regression/issue/20321.test.ts new file mode 100644 index 0000000000..6d7a500e30 --- /dev/null +++ b/test/regression/issue/20321.test.ts @@ -0,0 +1,95 @@ +import { expect, test } from "bun:test"; +import { spawnSync } from "child_process"; +import { bunEnv, bunExe, tempDirWithFiles } from "harness"; + +test("spawnSync should not crash when stdout is set to process.stderr (issue #20321)", () => { + // Test with process.stderr as stdout + const proc1 = spawnSync(bunExe(), ["-e", 'console.log("hello")'], { + encoding: "utf-8", + stdio: ["ignore", process.stderr, "inherit"], + env: bunEnv, + }); + + expect(proc1.error).toBeUndefined(); + expect(proc1.status).toBe(0); + // When redirecting to a file descriptor, we don't capture the output + expect(proc1.stdout).toBeNull(); +}); + +test("spawnSync should not crash when stderr is set to process.stdout", () => { + // Test with process.stdout as stderr + const proc2 = spawnSync(bunExe(), ["-e", 'console.log("hello")'], { + encoding: "utf-8", + stdio: ["ignore", "pipe", process.stdout], + env: bunEnv, + }); + + expect(proc2.error).toBeUndefined(); + expect(proc2.status).toBe(0); + expect(proc2.stdout).toBe("hello\n"); + // When redirecting to a file descriptor, we don't capture the output + expect(proc2.stderr).toBeNull(); +}); + +test("spawnSync should handle process.stdin/stdout/stderr in stdio array", () => { + // Test with all process streams + const proc3 = spawnSync(bunExe(), ["-e", 'console.log("test")'], { + encoding: "utf-8", + stdio: [process.stdin, process.stdout, process.stderr], + env: bunEnv, + }); + + expect(proc3.error).toBeUndefined(); + expect(proc3.status).toBe(0); + // When redirecting to file descriptors, we don't capture the output + expect(proc3.stdout).toBeNull(); + expect(proc3.stderr).toBeNull(); +}); + +test("spawnSync with mixed stdio options including process streams", () => { + // Mix of different stdio options + const proc4 = spawnSync(bunExe(), ["-e", 'console.log("mixed")'], { + encoding: "utf-8", + stdio: ["pipe", process.stderr, "pipe"], + env: bunEnv, + }); + + expect(proc4.error).toBeUndefined(); + expect(proc4.status).toBe(0); + // stdout redirected to stderr fd, so no capture + expect(proc4.stdout).toBeNull(); + // stderr is piped, should be empty for echo + expect(proc4.stderr).toBe(""); +}); + +test("spawnSync should work with file descriptors directly", () => { + // Test with raw file descriptors (same as what process.stderr resolves to) + const proc5 = spawnSync(bunExe(), ["-e", 'console.log("fd-test")'], { + encoding: "utf-8", + stdio: ["ignore", 2, "inherit"], // 2 is stderr fd + env: bunEnv, + }); + + expect(proc5.error).toBeUndefined(); + expect(proc5.status).toBe(0); + expect(proc5.stdout).toBeNull(); +}); + +test("spawnSync should handle the AWS CDK use case", () => { + // This is the exact use case from AWS CDK that was failing + const dir = tempDirWithFiles("spawnsync-cdk", { + "test.js": `console.log("CDK output");`, + }); + + const proc = spawnSync(bunExe(), ["test.js"], { + encoding: "utf-8", + stdio: ["ignore", process.stderr, "inherit"], + cwd: dir, + env: bunEnv, + }); + + expect(proc.error).toBeUndefined(); + expect(proc.status).toBe(0); + // Output goes to stderr, not captured + expect(proc.stdout).toBeNull(); +});