Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 26 additions & 11 deletions packages/bun-types/bun.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7119,11 +7119,12 @@ declare module "bun" {
*
* For stdout and stderr you may pass:
*
* - `"pipe"`, `undefined`: The process has a {@link ReadableStream} for standard output/error
* - `"pipe"`: The process has a {@link ReadableStream} for standard output/error
* - `"ignore"`, `null`: The process has no standard output/error
* - `"inherit"`: The process inherits the standard output/error of the current process
* - `ArrayBufferView`: The process writes to the preallocated buffer. Not implemented.
* - `number`: The process writes to the file descriptor
* - `undefined`: The default for that position, see below
*
* At indices >= 3, `"socket-fd"` (POSIX only) is also accepted:
* creates a socketpair like `"pipe"`, but the parent-end fd exposed
Expand Down Expand Up @@ -7152,23 +7153,26 @@ declare module "bun" {
/**
* The file descriptor for the standard output. It may be:
*
* - `"pipe"`, `undefined`: The process has a {@link ReadableStream} for standard output/error
* - `"pipe"`: The process has a {@link ReadableStream} for standard output/error
* - `"ignore"`, `null`: The process has no standard output/error
* - `"inherit"`: The process inherits the standard output/error of the current process
* - `ArrayBufferView`: The process writes to the preallocated buffer. Not implemented.
* - `number`: The process writes to the file descriptor
* - `undefined`: The default below
*
* @default "pipe"
*/
stdout?: Out;
/**
* The file descriptor for the standard error. It may be:
*
* - `"pipe"`, `undefined`: The process has a {@link ReadableStream} for standard output/error
* - `"pipe"`: The process has a {@link ReadableStream} for standard output/error
* - `"ignore"`, `null`: The process has no standard output/error
* - `"inherit"`: The process inherits the standard output/error of the current process
* - `ArrayBufferView`: The process writes to the preallocated buffer. Not implemented.
* - `number`: The process writes to the file descriptor
* - `undefined`: The default below, so with {@link spawn} the process inherits standard error
* and {@link Subprocess.stderr} is `undefined`
*
* @default "inherit" for `spawn`
* "pipe" for `spawnSync`
Expand Down Expand Up @@ -7436,11 +7440,22 @@ declare module "bun" {
terminal?: TerminalOptions | Terminal;
}

type ReadableToIO<X extends Readable> = X extends "pipe" | undefined
? ReadableStream<Uint8Array<ArrayBuffer>>
: X extends BunFile | ArrayBufferView | number
? number
: undefined;
/**
* The type of {@link Subprocess.stdout} / {@link Subprocess.stderr} for a `stdout` / `stderr`
* option of type `X`.
*
* An `undefined` option means the slot's default, and {@link spawn} has a different default
* for each slot: `"pipe"` for stdout, `"inherit"` for stderr. `Default` is that slot default,
* so `ReadableToIO<undefined, "pipe">` (stdout) is a {@link ReadableStream} and
* `ReadableToIO<undefined, "inherit">` (stderr) is `undefined`.
*/
type ReadableToIO<X extends Readable, Default extends Exclude<Readable, undefined> = "pipe"> = X extends undefined
? ReadableToIO<Default>
: X extends "pipe"
? ReadableStream<Uint8Array<ArrayBuffer>>
: X extends BunFile | ArrayBufferView | number
? number
: undefined;

type ReadableToSyncIO<X extends Readable> = X extends "pipe" | undefined ? Buffer : undefined;

Expand Down Expand Up @@ -7545,8 +7560,8 @@ declare module "bun" {
Err extends SpawnOptions.Readable = SpawnOptions.Readable,
> extends AsyncDisposable {
readonly stdin: SpawnOptions.WritableToIO<In>;
readonly stdout: SpawnOptions.ReadableToIO<Out>;
readonly stderr: SpawnOptions.ReadableToIO<Err>;
readonly stdout: SpawnOptions.ReadableToIO<Out, "pipe">;
readonly stderr: SpawnOptions.ReadableToIO<Err, "inherit">;

/**
* The terminal attached to this subprocess, if spawned with the `terminal` option.
Expand Down Expand Up @@ -7583,7 +7598,7 @@ declare module "bun" {
*
* Exists for compatibility with {@link ReadableStream.pipeThrough}
*/
readonly readable: SpawnOptions.ReadableToIO<Out>;
readonly readable: SpawnOptions.ReadableToIO<Out, "pipe">;

/**
* The process ID of the child process
Expand Down
29 changes: 29 additions & 0 deletions test/integration/bun-types/bun-types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,35 @@ describe("@types/bun integration test", () => {
});
});

// Also runs on debug builds, where the in-process typeTest cases (which cover the whole
// fixture directory) are skipped: checks fixture/spawn.ts alone against the packed bun-types.
describe("Bun.spawn", () => {
test("fixture/spawn.ts type-checks", async () => {
const checkDir = join(TEMP_DIR, "spawn-fixture-check");
const tsconfig = structuredClone(sourceTsconfig);
tsconfig.files = [join(BASE_FIXTURE_DIR, "spawn.ts")];
tsconfig.compilerOptions.typeRoots = [join(BASE_FIXTURE_DIR, "node_modules", "@types")];
await mkdir(checkDir, { recursive: true });
await makeTree(checkDir, {
"tsconfig.json": JSON.stringify(tsconfig, null, 2),
});

await using proc = Bun.spawn({
cmd: [bunExe(), join(BASE_FIXTURE_DIR, "node_modules", "typescript", "bin", "tsc"), "-p", "."],
env: bunEnv,
cwd: checkDir,
stdout: "pipe",
stderr: "pipe",
});

const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect(stderr.trim()).toBe("");
expect(stdout.trim()).toBe("");
expect(exitCode).toBe(0);
});
});

describe("Test Globals", () => {
const code = `
const test_shouldBeAFunction: Function = test;
Expand Down
75 changes: 75 additions & 0 deletions test/integration/bun-types/fixture/spawn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,3 +226,78 @@ tsd.expectAssignable<SyncSubprocess<Bun.SpawnOptions.Readable, Bun.SpawnOptions.
Bun.spawnSync({ cmd: ["echo", "hello"], stdout: "pipe", stderr: "pipe", lazy: true,
});
}

// An undefined stdout/stderr option means that slot's default, which differs between the two slots
// of Bun.spawn: stdout defaults to "pipe", stderr to "inherit" (so proc.stderr is undefined).
// Bun.spawnSync defaults both to "pipe".
//
// Without exactOptionalPropertyTypes, TypeScript drops undefined from a union passed to the optional
// `stderr` property (`stderr: cond ? "pipe" : undefined` infers Err = "pipe"), so the union case is
// asserted through the stdio tuple and through explicit type arguments below.
declare const pipeWhenCapturing: boolean;
{
const proc = Bun.spawn(["cat"], { stderr: undefined });
tsd.expectType(proc).is<Bun.Subprocess<"ignore", "pipe", undefined>>();
tsd.expectType(proc.stderr).is<undefined>();
}
{
const proc = Bun.spawn({ cmd: ["cat"], stderr: undefined });
tsd.expectType(proc.stderr).is<undefined>();
}
{
const proc = Bun.spawn(["cat"], { stdout: undefined });
tsd.expectType(proc).is<Bun.Subprocess<"ignore", undefined, "inherit">>();
tsd.expectType(proc.stdout).is<ReadableStream<Uint8Array<ArrayBuffer>>>();
tsd.expectType(proc.readable).is<ReadableStream<Uint8Array<ArrayBuffer>>>();
}
{
const proc = Bun.spawn(["cat"], { stdio: [undefined, undefined, undefined] });
tsd.expectType(proc.stdin).is<undefined>();
tsd.expectType(proc.stdout).is<ReadableStream<Uint8Array<ArrayBuffer>>>();
tsd.expectType(proc.stderr).is<undefined>();
}
{
const proc = Bun.spawn(["cat"], {
stdio: ["ignore", pipeWhenCapturing ? "pipe" : undefined, pipeWhenCapturing ? "pipe" : undefined],
});
tsd.expectType(proc).is<Bun.Subprocess<"ignore", "pipe" | undefined, "pipe" | undefined>>();
tsd.expectType(proc.stdout).is<ReadableStream<Uint8Array<ArrayBuffer>>>();
tsd.expectType(proc.stderr).is<ReadableStream<Uint8Array<ArrayBuffer>> | undefined>();
}
{
Bun.spawn(["cat"], {
stderr: undefined,
onExit(proc) {
tsd.expectType(proc.stderr).is<undefined>();
},
ipc(message, proc) {
tsd.expectType(proc.stderr).is<undefined>();
},
});
}
{
const proc = Bun.spawnSync(["cat"], { stdout: undefined, stderr: undefined });
tsd.expectType(proc.stdout).is<Buffer>();
tsd.expectType(proc.stderr).is<Buffer>();
}
{
const proc = Bun.spawnSync(["cat"], { stdio: ["ignore", undefined, undefined] });
tsd.expectType(proc.stdout).is<Buffer>();
tsd.expectType(proc.stderr).is<Buffer>();
}
tsd
.expectType<Bun.Subprocess<"ignore", "pipe", "pipe" | undefined>["stderr"]>()
.is<ReadableStream<Uint8Array<ArrayBuffer>> | undefined>();
tsd
.expectType<Bun.Subprocess<"ignore", "pipe" | undefined, "inherit">["stdout"]>()
.is<ReadableStream<Uint8Array<ArrayBuffer>>>();
tsd.expectType<NullSubprocess["stderr"]>().is<undefined>();
// Configurations that are not known statically still see every possible value.
tsd.expectType<Bun.Subprocess["stderr"]>().is<ReadableStream<Uint8Array<ArrayBuffer>> | number | undefined>();
tsd.expectType<WritableSubprocess["stderr"]>().is<ReadableStream<Uint8Array<ArrayBuffer>> | number | undefined>();
// The one-parameter form of the alias keeps stdout's default.
tsd.expectType<Bun.Spawn.ReadableToIO<undefined>>().is<ReadableStream<Uint8Array<ArrayBuffer>>>();
tsd.expectType<Bun.Spawn.ReadableToIO<undefined, "inherit">>().is<undefined>();
tsd
.expectType<Bun.Spawn.ReadableToIO<"pipe" | undefined, "inherit">>()
.is<ReadableStream<Uint8Array<ArrayBuffer>> | undefined>();