Document & cover some missing spawn/spawnSync options (#24417)

This commit is contained in:
Alistair Smith
2025-11-06 14:37:26 -08:00
committed by GitHub
parent e01f454635
commit 44402ad27a
5 changed files with 334 additions and 10 deletions

View File

@@ -187,3 +187,45 @@ tsd.expectAssignable<NullSubprocess>(Bun.spawn([], { stdio: ["ignore", "inherit"
tsd.expectAssignable<NullSubprocess>(Bun.spawn([], { stdio: [null, null, null] }));
tsd.expectAssignable<SyncSubprocess<Bun.SpawnOptions.Readable, Bun.SpawnOptions.Readable>>(Bun.spawnSync([], {}));
// Lazy option types (async only)
{
// valid: lazy usable with async spawn
const p1 = Bun.spawn(["echo", "hello"], {
stdout: "pipe",
stderr: "pipe",
lazy: true,
});
tsd.expectType(p1.stdout).is<ReadableStream<Uint8Array<ArrayBuffer>>>();
}
{
// valid: lazy false is also allowed
const p2 = Bun.spawn(["echo", "hello"], {
stdout: "pipe",
stderr: "pipe",
lazy: false,
});
tsd.expectType(p2.stderr).is<ReadableStream<Uint8Array<ArrayBuffer>>>();
}
{
// invalid: lazy is not supported in spawnSync
Bun.spawnSync(["echo", "hello"], {
stdout: "pipe",
stderr: "pipe",
// @ts-expect-error lazy applies only to async spawn
lazy: true,
});
}
{
// invalid: lazy is not supported in spawnSync (object overload)
// @ts-expect-error lazy applies only to async spawn
Bun.spawnSync({
cmd: ["echo", "hello"],
stdout: "pipe",
stderr: "pipe",
lazy: true,
});
}

View File

@@ -79,6 +79,10 @@ describe("bun:test", () => {
});
});
test.each([1, 2, 3])("test.each", a => {
expectType<1 | 2 | 3>(a);
});
// inference should work when data is passed directly in
test.each([
["a", true, 5],

View File

@@ -480,6 +480,7 @@ for (let [gcTick, label] of [
}
resolve && resolve();
// @ts-expect-error
resolve = undefined;
})();
await promise;
@@ -679,9 +680,10 @@ describe("should not hang", () => {
it(
"sleep " + sleep,
async () => {
const runs = [];
const runs: Promise<void>[] = [];
let initialMaxFD = -1;
for (let order of [
for (const order of [
["sleep", "kill", "unref", "exited"],
["sleep", "unref", "kill", "exited"],
["kill", "sleep", "unref", "exited"],
@@ -789,6 +791,7 @@ describe("close handling", () => {
await exitPromise;
})();
Bun.gc(false);
await Bun.sleep(0);
@@ -837,3 +840,182 @@ it("error does not UAF", async () => {
}
expect(emsg).toInclude(" ");
});
describe("onDisconnect", () => {
it.todoIf(isWindows)("ipc delivers message", async () => {
const msg = Promise.withResolvers<void>();
let ipcMessage: unknown;
await using proc = spawn({
cmd: [
bunExe(),
"-e",
`
process.send("hello");
Promise.resolve().then(() => process.exit(0));
`,
],
ipc: message => {
ipcMessage = message;
msg.resolve();
},
stdio: ["inherit", "inherit", "inherit"],
env: bunEnv,
});
await msg.promise;
expect(ipcMessage).toBe("hello");
expect(await proc.exited).toBe(0);
});
it.todoIf(isWindows)("onDisconnect callback is called when IPC disconnects", async () => {
const disc = Promise.withResolvers<void>();
let disconnectCalled = false;
await using proc = spawn({
cmd: [
bunExe(),
"-e",
`
Promise.resolve().then(() => {
process.disconnect();
process.exit(0);
});
`,
],
// Ensure IPC channel is opened without relying on a message
ipc: () => {},
onDisconnect: () => {
disconnectCalled = true;
disc.resolve();
},
stdio: ["inherit", "inherit", "inherit"],
env: bunEnv,
});
await disc.promise;
expect(disconnectCalled).toBe(true);
expect(await proc.exited).toBe(0);
});
it("onDisconnect is not called when IPC is not used", async () => {
await using proc = spawn({
cmd: [bunExe(), "-e", "console.log('hello')"],
onDisconnect: () => {
expect().fail("onDisconnect was called()");
},
stdout: "pipe",
stderr: "ignore",
stdin: "ignore",
});
expect(await proc.exited).toBe(0);
});
});
describe("argv0", () => {
it("argv0 option changes process.argv0 but not executable", async () => {
await using proc = spawn({
cmd: [bunExe(), "-e", "console.log(process.argv0); console.log(process.execPath)"],
argv0: "custom-argv0",
stdout: "pipe",
stderr: "ignore",
stdin: "ignore",
env: bunEnv,
});
const output = await proc.stdout.text();
const lines = output.trim().split(/\r?\n/);
expect(lines[0]).toBe("custom-argv0");
expect(path.normalize(lines[1])).toBe(path.normalize(bunExe()));
await proc.exited;
});
it("argv0 option works with spawnSync", () => {
const argv0 = "custom-argv0-sync";
const proc = spawnSync({
cmd: [bunExe(), "-e", "console.log(JSON.stringify({ argv0: process.argv0, execPath: process.execPath }))"],
argv0,
stdout: "pipe",
stderr: "ignore",
stdin: "ignore",
env: bunEnv,
});
const output = JSON.parse(proc.stdout.toString().trim());
expect(output).toEqual({ argv0, execPath: path.normalize(bunExe()) });
});
it("argv0 defaults to cmd[0] when not specified", async () => {
await using proc = spawn({
cmd: [bunExe(), "-e", "console.log(process.argv0)"],
stdout: "pipe",
stderr: "ignore",
stdin: "ignore",
env: bunEnv,
});
const output = await proc.stdout.text();
expect(output.trim()).toBe(bunExe());
await proc.exited;
});
});
describe("option combinations", () => {
it("detached + argv0 works together", async () => {
await using proc = spawn({
cmd: [bunExe(), "-e", "console.log(process.argv0)"],
detached: true,
argv0: "custom-name",
stdout: "pipe",
stderr: "ignore",
stdin: "ignore",
env: bunEnv,
});
const output = await proc.stdout.text();
expect(output.trim()).toBe("custom-name");
await proc.exited;
});
it.todoIf(isWindows)("onDisconnect + ipc + serialization works together", async () => {
let messageReceived = false;
let disconnectCalled = false;
const msg = Promise.withResolvers<void>();
const disc = Promise.withResolvers<void>();
await using proc = spawn({
cmd: [
bunExe(),
"-e",
`
process.send({type: "hello", data: "world"});
Promise.resolve().then(() => {
process.disconnect();
process.exit(0);
});
`,
],
ipc: message => {
expect(message).toEqual({ type: "hello", data: "world" });
messageReceived = true;
msg.resolve();
},
onDisconnect: () => {
disconnectCalled = true;
disc.resolve();
},
serialization: "advanced",
stdio: ["inherit", "inherit", "inherit"],
env: bunEnv,
});
await Promise.all([msg.promise, disc.promise]);
expect(messageReceived).toBe(true);
expect(disconnectCalled).toBe(true);
expect(await proc.exited).toBe(0);
});
});