mirror of
https://github.com/oven-sh/bun
synced 2026-02-06 00:48:55 +00:00
Document & cover some missing spawn/spawnSync options (#24417)
This commit is contained in:
108
packages/bun-types/bun.d.ts
vendored
108
packages/bun-types/bun.d.ts
vendored
@@ -5289,7 +5289,12 @@ declare module "bun" {
|
||||
options: udp.ConnectSocketOptions<DataBinaryType>,
|
||||
): Promise<udp.ConnectedSocket<DataBinaryType>>;
|
||||
|
||||
namespace SpawnOptions {
|
||||
/**
|
||||
* @deprecated use {@link Bun.Spawn} instead
|
||||
*/
|
||||
export import SpawnOptions = Spawn;
|
||||
|
||||
namespace Spawn {
|
||||
/**
|
||||
* Option for stdout/stderr
|
||||
*/
|
||||
@@ -5320,7 +5325,12 @@ declare module "bun" {
|
||||
| Response
|
||||
| Request;
|
||||
|
||||
interface OptionsObject<In extends Writable, Out extends Readable, Err extends Readable> {
|
||||
/**
|
||||
* @deprecated use BaseOptions or the specific options for the specific {@link spawn} or {@link spawnSync} usage
|
||||
*/
|
||||
type OptionsObject<In extends Writable, Out extends Readable, Err extends Readable> = BaseOptions<In, Out, Err>;
|
||||
|
||||
interface BaseOptions<In extends Writable, Out extends Readable, Err extends Readable> {
|
||||
/**
|
||||
* The current working directory of the process
|
||||
*
|
||||
@@ -5328,6 +5338,22 @@ declare module "bun" {
|
||||
*/
|
||||
cwd?: string;
|
||||
|
||||
/**
|
||||
* Run the child in a separate process group, detached from the parent.
|
||||
*
|
||||
* - POSIX: calls `setsid()` so the child starts a new session and becomes
|
||||
* the process group leader. It can outlive the parent and receive
|
||||
* signals independently of the parent’s terminal/process group.
|
||||
* - Windows: sets `UV_PROCESS_DETACHED`, allowing the child to outlive
|
||||
* the parent and receive signals independently.
|
||||
*
|
||||
* Note: stdio may keep the parent process alive. Pass `stdio: ["ignore",
|
||||
* "ignore", "ignore"]` to the spawn constructor to prevent this.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
detached?: boolean;
|
||||
|
||||
/**
|
||||
* The environment variables of the process
|
||||
*
|
||||
@@ -5430,6 +5456,48 @@ declare module "bun" {
|
||||
error?: ErrorLike,
|
||||
): void | Promise<void>;
|
||||
|
||||
/**
|
||||
* Called exactly once when the IPC channel between the parent and this
|
||||
* subprocess is closed. After this runs, no further IPC messages will be
|
||||
* delivered.
|
||||
*
|
||||
* When it fires:
|
||||
* - The child called `process.disconnect()` or the parent called
|
||||
* `subprocess.disconnect()`.
|
||||
* - The child exited for any reason (normal exit or due to a signal like
|
||||
* `SIGILL`, `SIGKILL`, etc.).
|
||||
* - The child replaced itself with a program that does not support Bun
|
||||
* IPC.
|
||||
*
|
||||
* Notes:
|
||||
* - This callback indicates that the pipe is closed; it is not an error
|
||||
* by itself. Use {@link onExit} or {@link Subprocess.exited} to
|
||||
* determine why the process ended.
|
||||
* - It may occur before or after {@link onExit} depending on timing; do
|
||||
* not rely on ordering. Typically, if you or the child call
|
||||
* `disconnect()` first, this fires before {@link onExit}; if the
|
||||
* process exits without an explicit disconnect, either may happen
|
||||
* first.
|
||||
* - Only runs when {@link ipc} is enabled and runs at most once per
|
||||
* subprocess.
|
||||
* - If the child becomes a zombie (exited but not yet reaped), the IPC is
|
||||
* already closed, and this callback will fire (or may already have
|
||||
* fired).
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```ts
|
||||
* const subprocess = spawn({
|
||||
* cmd: ["echo", "hello"],
|
||||
* ipc: (message) => console.log(message),
|
||||
* onDisconnect: () => {
|
||||
* console.log("IPC channel disconnected");
|
||||
* },
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
onDisconnect?(): void | Promise<void>;
|
||||
|
||||
/**
|
||||
* When specified, Bun will open an IPC channel to the subprocess. The passed callback is called for
|
||||
* incoming messages, and `subprocess.send` can send messages to the subprocess. Messages are serialized
|
||||
@@ -5549,6 +5617,34 @@ declare module "bun" {
|
||||
maxBuffer?: number;
|
||||
}
|
||||
|
||||
interface SpawnSyncOptions<In extends Writable, Out extends Readable, Err extends Readable>
|
||||
extends BaseOptions<In, Out, Err> {}
|
||||
|
||||
interface SpawnOptions<In extends Writable, Out extends Readable, Err extends Readable>
|
||||
extends BaseOptions<In, Out, Err> {
|
||||
/**
|
||||
* If true, stdout and stderr pipes will not automatically start reading
|
||||
* data. Reading will only begin when you access the `stdout` or `stderr`
|
||||
* properties.
|
||||
*
|
||||
* This can improve performance when you don't need to read output
|
||||
* immediately.
|
||||
*
|
||||
* @default false
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const subprocess = Bun.spawn({
|
||||
* cmd: ["echo", "hello"],
|
||||
* lazy: true, // Don't start reading stdout until accessed
|
||||
* });
|
||||
* // stdout reading hasn't started yet
|
||||
* await subprocess.stdout.text(); // Now reading starts
|
||||
* ```
|
||||
*/
|
||||
lazy?: boolean;
|
||||
}
|
||||
|
||||
type ReadableToIO<X extends Readable> = X extends "pipe" | undefined
|
||||
? ReadableStream<Uint8Array<ArrayBuffer>>
|
||||
: X extends BunFile | ArrayBufferView | number
|
||||
@@ -5806,7 +5902,7 @@ declare module "bun" {
|
||||
const Out extends SpawnOptions.Readable = "pipe",
|
||||
const Err extends SpawnOptions.Readable = "inherit",
|
||||
>(
|
||||
options: SpawnOptions.OptionsObject<In, Out, Err> & {
|
||||
options: SpawnOptions.SpawnOptions<In, Out, Err> & {
|
||||
/**
|
||||
* The command to run
|
||||
*
|
||||
@@ -5856,7 +5952,7 @@ declare module "bun" {
|
||||
* ```
|
||||
*/
|
||||
cmds: string[],
|
||||
options?: SpawnOptions.OptionsObject<In, Out, Err>,
|
||||
options?: SpawnOptions.SpawnOptions<In, Out, Err>,
|
||||
): Subprocess<In, Out, Err>;
|
||||
|
||||
/**
|
||||
@@ -5878,7 +5974,7 @@ declare module "bun" {
|
||||
const Out extends SpawnOptions.Readable = "pipe",
|
||||
const Err extends SpawnOptions.Readable = "pipe",
|
||||
>(
|
||||
options: SpawnOptions.OptionsObject<In, Out, Err> & {
|
||||
options: SpawnOptions.SpawnSyncOptions<In, Out, Err> & {
|
||||
/**
|
||||
* The command to run
|
||||
*
|
||||
@@ -5929,7 +6025,7 @@ declare module "bun" {
|
||||
* ```
|
||||
*/
|
||||
cmds: string[],
|
||||
options?: SpawnOptions.OptionsObject<In, Out, Err>,
|
||||
options?: SpawnOptions.SpawnSyncOptions<In, Out, Err>,
|
||||
): SyncSubprocess<Out, Err>;
|
||||
|
||||
/** Utility type for any process from {@link Bun.spawn()} with both stdout and stderr set to `"pipe"` */
|
||||
|
||||
4
packages/bun-types/test.d.ts
vendored
4
packages/bun-types/test.d.ts
vendored
@@ -262,7 +262,7 @@ declare module "bun:test" {
|
||||
*/
|
||||
each<T extends Readonly<[any, ...any[]]>>(table: readonly T[]): Describe<[...T]>;
|
||||
each<T extends any[]>(table: readonly T[]): Describe<[...T]>;
|
||||
each<T>(table: T[]): Describe<[T]>;
|
||||
each<const T>(table: T[]): Describe<[T]>;
|
||||
}
|
||||
/**
|
||||
* Describes a group of related tests.
|
||||
@@ -552,7 +552,7 @@ declare module "bun:test" {
|
||||
*/
|
||||
each<T extends Readonly<[unknown, ...unknown[]]>>(table: readonly T[]): Test<T>;
|
||||
each<T extends unknown[]>(table: readonly T[]): Test<T>;
|
||||
each<T>(table: T[]): Test<[T]>;
|
||||
each<const T>(table: T[]): Test<[T]>;
|
||||
}
|
||||
/**
|
||||
* Runs a test.
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user