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
6 changes: 3 additions & 3 deletions docs/runtime/child-process.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,7 @@ namespace SpawnOptions {
}

interface Subprocess extends AsyncDisposable {
readonly stdin: FileSink | number | undefined | null;
readonly stdin: FileSink | ReadableStream | number | undefined | null;
readonly stdout: ReadableStream<Uint8Array<ArrayBuffer>> | number | undefined | null;
readonly stderr: ReadableStream<Uint8Array<ArrayBuffer>> | number | undefined | null;
readonly readable: ReadableStream<Uint8Array<ArrayBuffer>> | number | undefined | null;
Expand All @@ -593,8 +593,8 @@ interface Subprocess extends AsyncDisposable {
}

interface SyncSubprocess {
stdout: Buffer | undefined;
stderr: Buffer | undefined;
stdout: Buffer | number | undefined;
stderr: Buffer | number | undefined;
exitCode: number;
success: boolean;
resourceUsage: ResourceUsage;
Expand Down
84 changes: 77 additions & 7 deletions packages/bun-types/bun.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7436,21 +7436,70 @@ declare module "bun" {
terminal?: TerminalOptions | Terminal;
}

/**
* The value of {@link Subprocess.stdout} / {@link Subprocess.stderr} for a given
* `stdout` / `stderr` option:
*
* - `"pipe"`: a {@link ReadableStream} of the process's output
* - a file descriptor, as a `number` or as `Bun.file(fd)`: that file descriptor, except
* that the parent's own descriptor for the same stream (`1` for `stdout`, `2` for
* `stderr`) is treated as `"inherit"` and gives `undefined`
* - anything else, including `Bun.file(path)`: `undefined`. Bun sends the output where the
* option says; nothing is exposed on the {@link Subprocess}.
*
* When the process was spawned with the `terminal` option, `stdin`, `stdout` and `stderr`
* are all `null` instead.
*/
type ReadableToIO<X extends Readable> = X extends "pipe" | undefined
? ReadableStream<Uint8Array<ArrayBuffer>>
: X extends BunFile | ArrayBufferView | number
? number
: X extends number | BunFile
? number | undefined
: undefined;

type ReadableToSyncIO<X extends Readable> = X extends "pipe" | undefined ? Buffer : undefined;
/**
* The value of {@link SyncSubprocess.stdout} / {@link SyncSubprocess.stderr} for a given
* `stdout` / `stderr` option:
*
* - `"pipe"`: everything the process wrote, as a `Buffer`
* - a file descriptor, as a `number` or as `Bun.file(fd)`: that file descriptor, except
* that the parent's own descriptor for the same stream (`1` for `stdout`, `2` for
* `stderr`) is treated as `"inherit"` and gives `undefined`
* - anything else, including `Bun.file(path)`: `undefined`
*/
type ReadableToSyncIO<X extends Readable> = X extends "pipe" | undefined
? Buffer
: X extends number | BunFile
? number | undefined
: undefined;

type WritableIO = FileSink | number | undefined;
/**
* Every value {@link Subprocess.stdin} can hold. See {@link WritableToIO}.
*/
type WritableIO = FileSink | ReadableStream | number | undefined;

/**
* The value of {@link Subprocess.stdin} for a given `stdin` option:
*
* - `"pipe"`: a {@link FileSink} that writes to the process's input
* - a file descriptor, as a `number` or as `Bun.file(fd)`: that file descriptor, except
* that `0`, the parent's own stdin, is treated as `"inherit"` and gives `undefined`
* - a {@link ReadableStream}, or a {@link Request} / {@link Response} whose body is one:
* the stream Bun is piping into the process. A stream Bun can read without piping
* (`blob.stream()`, `Bun.file(path).stream()`), or a `Request` / `Response` whose body is
* not a stream, is handled like the blob or file behind it and gives `undefined`.
* - anything else, including `Blob`, `ArrayBufferView` and `Bun.file(path)`: `undefined`.
* Bun feeds the input to the process itself; nothing is exposed on the {@link Subprocess}.
*
* When the process was spawned with the `terminal` option, `stdin`, `stdout` and `stderr`
* are all `null` instead.
*/
type WritableToIO<X extends Writable> = X extends "pipe"
? FileSink
: X extends BunFile | ArrayBufferView | Blob | Request | Response | number
? number
: undefined;
: X extends number | BunFile
? number | undefined
: X extends ReadableStream | Request | Response
? ReadableStream | undefined
: undefined;
}

interface ResourceUsage {
Expand Down Expand Up @@ -7544,8 +7593,21 @@ declare module "bun" {
Out extends SpawnOptions.Readable = SpawnOptions.Readable,
Err extends SpawnOptions.Readable = SpawnOptions.Readable,
> extends AsyncDisposable {
/**
* The process's standard input: a {@link FileSink} with `stdin: "pipe"`, otherwise
* whatever {@link Spawn.WritableToIO} lists for the `stdin` option that was passed.
*/
readonly stdin: SpawnOptions.WritableToIO<In>;
/**
* The process's standard output: a {@link ReadableStream} with `stdout: "pipe"` (the default),
* otherwise whatever {@link Spawn.ReadableToIO} lists for the `stdout` option that was passed.
*/
readonly stdout: SpawnOptions.ReadableToIO<Out>;
/**
* The process's standard error: a {@link ReadableStream} with `stderr: "pipe"`, otherwise
* whatever {@link Spawn.ReadableToIO} lists for the `stderr` option that was passed
* (`undefined` for the default, `"inherit"`).
*/
readonly stderr: SpawnOptions.ReadableToIO<Err>;

/**
Expand Down Expand Up @@ -7680,7 +7742,15 @@ declare module "bun" {
Out extends SpawnOptions.Readable = SpawnOptions.Readable,
Err extends SpawnOptions.Readable = SpawnOptions.Readable,
> {
/**
* Everything the process wrote to stdout, as a `Buffer`, with `stdout: "pipe"` (the default).
* Otherwise whatever {@link Spawn.ReadableToSyncIO} lists for the `stdout` option that was passed.
*/
stdout: SpawnOptions.ReadableToSyncIO<Out>;
/**
* Everything the process wrote to stderr, as a `Buffer`, with `stderr: "pipe"` (the default).
* Otherwise whatever {@link Spawn.ReadableToSyncIO} lists for the `stderr` option that was passed.
*/
stderr: SpawnOptions.ReadableToSyncIO<Err>;
exitCode: number;
success: boolean;
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
99 changes: 88 additions & 11 deletions test/integration/bun-types/fixture/spawn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,25 +159,102 @@ function depromise<T>(_promise: Promise<T>): T {
tsd.expectType(proc.stdout).is<undefined>();
tsd.expectType(proc.stderr).is<undefined>();
}
// What each stdio option turns into on the Subprocess. Only a caller-supplied file descriptor
// (a number or Bun.file(fd)) comes back out as a number, and even that becomes undefined when it
// is the parent's own standard stream (Bun treats that as "inherit"). A ReadableStream passed as
// stdin, including the body of a Request/Response, comes back as the stream; inputs Bun copies
// into the process itself (Blob, ArrayBufferView, Bun.file(path), an in-memory Request/Response
// body) leave the property undefined.
declare const fd: number;
declare const stdinStream: ReadableStream<Uint8Array>;
{
const proc = Bun.spawn(["echo", "hello"], {
stdio: [new Request("1"), null, null],
});

tsd.expectType<number>(proc.stdin);
const proc = Bun.spawn(["cat"], { stdin: fd, stdout: fd, stderr: fd });
tsd.expectType(proc.stdin).is<number | undefined>();
tsd.expectType(proc.stdout).is<number | undefined>();
tsd.expectType(proc.stderr).is<number | undefined>();
tsd.expectType(proc.readable).is<number | undefined>();
}
{
const proc = Bun.spawn(["echo", "hello"], {
stdio: [new Response("1"), null, null],
const proc = Bun.spawn(["cat"], { stdin: 0, stdout: 1, stderr: 2 });
tsd.expectType(proc.stdin).is<number | undefined>();
tsd.expectType(proc.stdout).is<number | undefined>();
tsd.expectType(proc.stderr).is<number | undefined>();
}
{
const proc = Bun.spawn(["cat"], {
stdin: Bun.file("input.txt"),
stdout: Bun.file("output.txt"),
stderr: Bun.file(fd),
});
tsd.expectType<number>(proc.stdin);
tsd.expectType(proc.stdin).is<number | undefined>();
tsd.expectType(proc.stdout).is<number | undefined>();
tsd.expectType(proc.stderr).is<number | undefined>();
tsd.expectType(proc.readable).is<number | undefined>();
}
{
const proc = Bun.spawn(["echo", "hello"], {
stdio: [new Uint8Array([]), null, null],
const proc = Bun.spawn(["cat"], { stdin: new Blob(["hello"]) });
tsd.expectType(proc.stdin).is<undefined>();
}
{
const proc = Bun.spawn(["cat"], { stdin: new Uint8Array([1, 2, 3]) });
tsd.expectType(proc.stdin).is<undefined>();
}
{
const proc = Bun.spawn(["cat"], { stdio: [new Uint8Array([]), new Uint8Array(64), new Uint8Array(64)] });
tsd.expectType(proc.stdin).is<undefined>();
tsd.expectType(proc.stdout).is<undefined>();
tsd.expectType(proc.stderr).is<undefined>();
}
{
const proc = Bun.spawn(["cat"], { stdin: stdinStream });
tsd.expectType(proc.stdin).is<ReadableStream | undefined>();
}
{
const proc = Bun.spawn(["cat"], { stdio: [new Request("1"), null, null] });
tsd.expectType(proc.stdin).is<ReadableStream | undefined>();
}
{
const proc = Bun.spawn(["cat"], { stdin: new Response("1") });
tsd.expectType(proc.stdin).is<ReadableStream | undefined>();
}
{
const proc = Bun.spawn(["cat"], {
stdin: depromise(fetch("https://example.com/")),
stdout: "pipe",
});
tsd.expectType<number>(proc.stdin);
tsd.expectType(proc.stdin).is<ReadableStream | undefined>();
tsd.expectType(proc.stdout).is<ReadableStream<Uint8Array<ArrayBuffer>>>();
}
{
const proc = Bun.spawnSync(["cat"], { stdout: fd, stderr: Bun.file("errors.txt") });
tsd.expectType(proc.stdout).is<number | undefined>();
tsd.expectType(proc.stderr).is<number | undefined>();
}
{
const proc = Bun.spawnSync(["cat"], { stdout: "pipe", stderr: "inherit" });
tsd.expectType(proc.stdout).is<Buffer>();
tsd.expectType(proc.stderr).is<undefined>();
}
{
const proc = Bun.spawnSync(["cat"], { stdio: ["ignore", new Uint8Array(64), null] });
tsd.expectType(proc.stdout).is<undefined>();
tsd.expectType(proc.stderr).is<undefined>();
}

// The unions seen through a Subprocess whose stdio configuration is not known statically.
tsd.expectType<Bun.Subprocess["stdin"]>().is<FileSink | ReadableStream | number | undefined>();
tsd.expectType<Bun.Subprocess["stdin"]>().is<Bun.Spawn.WritableIO>();
tsd.expectType<Bun.Subprocess["stdout"]>().is<ReadableStream<Uint8Array<ArrayBuffer>> | number | undefined>();
tsd.expectType<Bun.Subprocess["stderr"]>().is<ReadableStream<Uint8Array<ArrayBuffer>> | number | undefined>();
tsd.expectType<SyncSubprocess["stdout"]>().is<Buffer | number | undefined>();
tsd.expectType<ReadableSubprocess["stdin"]>().is<FileSink | ReadableStream | number | undefined>();
tsd.expectType<ReadableSubprocess["stdout"]>().is<ReadableStream<Uint8Array<ArrayBuffer>>>();
tsd.expectType<WritableSubprocess["stdin"]>().is<FileSink>();
tsd.expectType<NullSubprocess["stdin"]>().is<undefined>();
tsd.expectAssignable<Bun.Subprocess>(Bun.spawn([], { stdin: stdinStream, stdout: Bun.file("out.txt"), stderr: fd }));
tsd.expectAssignable<Bun.Subprocess>(Bun.spawn([], { stdin: new Blob(["hello"]), stdout: "inherit" }));
tsd.expectAssignable<SyncSubprocess>(Bun.spawnSync([], { stdout: fd, stderr: Bun.file("errors.txt") }));

tsd.expectAssignable<PipedSubprocess>(Bun.spawn([], { stdio: ["pipe", "pipe", "pipe"] }));
tsd.expectAssignable<ReadableSubprocess>(Bun.spawn([], { stdio: ["ignore", "pipe", "pipe"] }));
tsd.expectAssignable<ReadableSubprocess>(Bun.spawn([], { stdio: ["pipe", "pipe", "pipe"] }));
Expand Down
Loading