diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index d8c9402e2871..ab0df74c4be5 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -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 @@ -7152,11 +7153,12 @@ 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" */ @@ -7164,11 +7166,13 @@ declare module "bun" { /** * 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` @@ -7436,11 +7440,22 @@ declare module "bun" { terminal?: TerminalOptions | Terminal; } - type ReadableToIO = X extends "pipe" | undefined - ? ReadableStream> - : 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` (stdout) is a {@link ReadableStream} and + * `ReadableToIO` (stderr) is `undefined`. + */ + type ReadableToIO = "pipe"> = X extends undefined + ? ReadableToIO + : X extends "pipe" + ? ReadableStream> + : X extends BunFile | ArrayBufferView | number + ? number + : undefined; type ReadableToSyncIO = X extends "pipe" | undefined ? Buffer : undefined; @@ -7545,8 +7560,8 @@ declare module "bun" { Err extends SpawnOptions.Readable = SpawnOptions.Readable, > extends AsyncDisposable { readonly stdin: SpawnOptions.WritableToIO; - readonly stdout: SpawnOptions.ReadableToIO; - readonly stderr: SpawnOptions.ReadableToIO; + readonly stdout: SpawnOptions.ReadableToIO; + readonly stderr: SpawnOptions.ReadableToIO; /** * The terminal attached to this subprocess, if spawned with the `terminal` option. @@ -7583,7 +7598,7 @@ declare module "bun" { * * Exists for compatibility with {@link ReadableStream.pipeThrough} */ - readonly readable: SpawnOptions.ReadableToIO; + readonly readable: SpawnOptions.ReadableToIO; /** * The process ID of the child process 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..4363551fb4f9 100644 --- a/test/integration/bun-types/fixture/spawn.ts +++ b/test/integration/bun-types/fixture/spawn.ts @@ -226,3 +226,78 @@ tsd.expectAssignable>(); + tsd.expectType(proc.stderr).is(); +} +{ + const proc = Bun.spawn({ cmd: ["cat"], stderr: undefined }); + tsd.expectType(proc.stderr).is(); +} +{ + const proc = Bun.spawn(["cat"], { stdout: undefined }); + tsd.expectType(proc).is>(); + tsd.expectType(proc.stdout).is>>(); + tsd.expectType(proc.readable).is>>(); +} +{ + const proc = Bun.spawn(["cat"], { stdio: [undefined, undefined, undefined] }); + tsd.expectType(proc.stdin).is(); + tsd.expectType(proc.stdout).is>>(); + tsd.expectType(proc.stderr).is(); +} +{ + const proc = Bun.spawn(["cat"], { + stdio: ["ignore", pipeWhenCapturing ? "pipe" : undefined, pipeWhenCapturing ? "pipe" : undefined], + }); + tsd.expectType(proc).is>(); + tsd.expectType(proc.stdout).is>>(); + tsd.expectType(proc.stderr).is> | undefined>(); +} +{ + Bun.spawn(["cat"], { + stderr: undefined, + onExit(proc) { + tsd.expectType(proc.stderr).is(); + }, + ipc(message, proc) { + tsd.expectType(proc.stderr).is(); + }, + }); +} +{ + const proc = Bun.spawnSync(["cat"], { stdout: undefined, stderr: undefined }); + tsd.expectType(proc.stdout).is(); + tsd.expectType(proc.stderr).is(); +} +{ + const proc = Bun.spawnSync(["cat"], { stdio: ["ignore", undefined, undefined] }); + tsd.expectType(proc.stdout).is(); + tsd.expectType(proc.stderr).is(); +} +tsd + .expectType["stderr"]>() + .is> | undefined>(); +tsd + .expectType["stdout"]>() + .is>>(); +tsd.expectType().is(); +// Configurations that are not known statically still see every possible value. +tsd.expectType().is> | number | undefined>(); +tsd.expectType().is> | number | undefined>(); +// The one-parameter form of the alias keeps stdout's default. +tsd.expectType>().is>>(); +tsd.expectType>().is(); +tsd + .expectType>() + .is> | undefined>();