diff --git a/docs/runtime/child-process.mdx b/docs/runtime/child-process.mdx index 60bf1a2e733b..a2127bc90ed0 100644 --- a/docs/runtime/child-process.mdx +++ b/docs/runtime/child-process.mdx @@ -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> | number | undefined | null; readonly stderr: ReadableStream> | number | undefined | null; readonly readable: ReadableStream> | number | undefined | null; @@ -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; diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index d8c9402e2871..ed9398927df0 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -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 "pipe" | undefined ? ReadableStream> - : X extends BunFile | ArrayBufferView | number - ? number + : X extends number | BunFile + ? number | undefined : undefined; - type ReadableToSyncIO = 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 "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 "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 { @@ -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; + /** + * 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; + /** + * 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; /** @@ -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; + /** + * 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; exitCode: number; success: boolean; diff --git a/test/integration/bun-types/bun-types.test.ts b/test/integration/bun-types/bun-types.test.ts index 05fa801f45e5..e1fca224640e 100644 --- a/test/integration/bun-types/bun-types.test.ts +++ b/test/integration/bun-types/bun-types.test.ts @@ -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; diff --git a/test/integration/bun-types/fixture/spawn.ts b/test/integration/bun-types/fixture/spawn.ts index 2036413158a0..d11f3448b596 100644 --- a/test/integration/bun-types/fixture/spawn.ts +++ b/test/integration/bun-types/fixture/spawn.ts @@ -159,25 +159,102 @@ function depromise(_promise: Promise): T { tsd.expectType(proc.stdout).is(); tsd.expectType(proc.stderr).is(); } +// 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; { - const proc = Bun.spawn(["echo", "hello"], { - stdio: [new Request("1"), null, null], - }); - - tsd.expectType(proc.stdin); + const proc = Bun.spawn(["cat"], { stdin: fd, stdout: fd, stderr: fd }); + tsd.expectType(proc.stdin).is(); + tsd.expectType(proc.stdout).is(); + tsd.expectType(proc.stderr).is(); + tsd.expectType(proc.readable).is(); } { - 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(); + tsd.expectType(proc.stdout).is(); + tsd.expectType(proc.stderr).is(); +} +{ + const proc = Bun.spawn(["cat"], { + stdin: Bun.file("input.txt"), + stdout: Bun.file("output.txt"), + stderr: Bun.file(fd), }); - tsd.expectType(proc.stdin); + tsd.expectType(proc.stdin).is(); + tsd.expectType(proc.stdout).is(); + tsd.expectType(proc.stderr).is(); + tsd.expectType(proc.readable).is(); } { - 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(); +} +{ + const proc = Bun.spawn(["cat"], { stdin: new Uint8Array([1, 2, 3]) }); + tsd.expectType(proc.stdin).is(); +} +{ + const proc = Bun.spawn(["cat"], { stdio: [new Uint8Array([]), new Uint8Array(64), new Uint8Array(64)] }); + tsd.expectType(proc.stdin).is(); + tsd.expectType(proc.stdout).is(); + tsd.expectType(proc.stderr).is(); +} +{ + const proc = Bun.spawn(["cat"], { stdin: stdinStream }); + tsd.expectType(proc.stdin).is(); +} +{ + const proc = Bun.spawn(["cat"], { stdio: [new Request("1"), null, null] }); + tsd.expectType(proc.stdin).is(); +} +{ + const proc = Bun.spawn(["cat"], { stdin: new Response("1") }); + tsd.expectType(proc.stdin).is(); +} +{ + const proc = Bun.spawn(["cat"], { + stdin: depromise(fetch("https://example.com/")), + stdout: "pipe", }); - tsd.expectType(proc.stdin); + tsd.expectType(proc.stdin).is(); + tsd.expectType(proc.stdout).is>>(); +} +{ + const proc = Bun.spawnSync(["cat"], { stdout: fd, stderr: Bun.file("errors.txt") }); + tsd.expectType(proc.stdout).is(); + tsd.expectType(proc.stderr).is(); } +{ + const proc = Bun.spawnSync(["cat"], { stdout: "pipe", stderr: "inherit" }); + tsd.expectType(proc.stdout).is(); + tsd.expectType(proc.stderr).is(); +} +{ + const proc = Bun.spawnSync(["cat"], { stdio: ["ignore", new Uint8Array(64), null] }); + tsd.expectType(proc.stdout).is(); + tsd.expectType(proc.stderr).is(); +} + +// The unions seen through a Subprocess whose stdio configuration is not known statically. +tsd.expectType().is(); +tsd.expectType().is(); +tsd.expectType().is> | number | undefined>(); +tsd.expectType().is> | number | undefined>(); +tsd.expectType().is(); +tsd.expectType().is(); +tsd.expectType().is>>(); +tsd.expectType().is(); +tsd.expectType().is(); +tsd.expectAssignable(Bun.spawn([], { stdin: stdinStream, stdout: Bun.file("out.txt"), stderr: fd })); +tsd.expectAssignable(Bun.spawn([], { stdin: new Blob(["hello"]), stdout: "inherit" })); +tsd.expectAssignable(Bun.spawnSync([], { stdout: fd, stderr: Bun.file("errors.txt") })); + tsd.expectAssignable(Bun.spawn([], { stdio: ["pipe", "pipe", "pipe"] })); tsd.expectAssignable(Bun.spawn([], { stdio: ["ignore", "pipe", "pipe"] })); tsd.expectAssignable(Bun.spawn([], { stdio: ["pipe", "pipe", "pipe"] }));