diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index d8c9402e2871..b9ca9d516c1e 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -8157,19 +8157,27 @@ declare module "bun" { type WritableSubprocess = Subprocess<"pipe", any, any>; /** Utility type for any process from {@link Bun.spawn()} with stdin, stdout, stderr all set to `"pipe"`. A combination of {@link ReadableSubprocess} and {@link WritableSubprocess} */ type PipedSubprocess = Subprocess<"pipe", "pipe", "pipe">; - /** Utility type for any process from {@link Bun.spawn()} with stdin, stdout, stderr all set to `null` or similar. */ + /** + * Utility type for any process from {@link Bun.spawn()} with stdin, stdout, stderr all set to `"ignore"`, `"inherit"` + * or `null`, so none of them is exposed on the process. + * + * An `undefined` option means the slot's default. That qualifies for stdin (`"ignore"`) and stderr (`"inherit"`), + * but not for stdout, whose default is `"pipe"`. + */ type NullSubprocess = Subprocess< "ignore" | "inherit" | null | undefined, - "ignore" | "inherit" | null | undefined, + "ignore" | "inherit" | null, "ignore" | "inherit" | null | undefined >; /** Utility type for any process from {@link Bun.spawnSync()} with both stdout and stderr set to `"pipe"` */ type ReadableSyncSubprocess = SyncSubprocess<"pipe", "pipe">; - /** Utility type for any process from {@link Bun.spawnSync()} with both stdout and stderr set to `null` or similar */ - type NullSyncSubprocess = SyncSubprocess< - "ignore" | "inherit" | null | undefined, - "ignore" | "inherit" | null | undefined - >; + /** + * Utility type for any process from {@link Bun.spawnSync()} with both stdout and stderr set to `"ignore"`, `"inherit"` + * or `null`, so neither is captured. + * + * An `undefined` option does not qualify: {@link Bun.spawnSync()} defaults both slots to `"pipe"`. + */ + type NullSyncSubprocess = SyncSubprocess<"ignore" | "inherit" | null, "ignore" | "inherit" | null>; /** * Options for creating a pseudo-terminal (PTY). 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..120888a9a87c 100644 --- a/test/integration/bun-types/fixture/spawn.ts +++ b/test/integration/bun-types/fixture/spawn.ts @@ -185,6 +185,23 @@ tsd.expectAssignable(Bun.spawn([], { stdio: ["pipe", "pipe", tsd.expectAssignable(Bun.spawn([], { stdio: ["pipe", "ignore", "inherit"] })); tsd.expectAssignable(Bun.spawn([], { stdio: ["ignore", "inherit", "ignore"] })); tsd.expectAssignable(Bun.spawn([], { stdio: [null, null, null] })); +// A Null* process exposes none of its stdio slots. An undefined option means the slot's default: "ignore" +// for Bun.spawn's stdin and "inherit" for its stderr, but "pipe" for Bun.spawn's stdout and for both +// Bun.spawnSync slots. So the aliases accept undefined in Bun.spawn's stdin and stderr slots only. +tsd.expectType().is(); +tsd.expectType().is(); +tsd.expectType().is(); +tsd.expectType().is(); +tsd.expectAssignable(Bun.spawn([], { stdio: [undefined, "ignore", undefined] })); +tsd.expectAssignable(Bun.spawn([], { stdout: "ignore" })); +// @ts-expect-error stdout: undefined means "pipe", so this process has a stdout stream +tsd.expectAssignable(Bun.spawn([], { stdio: ["ignore", undefined, "ignore"] })); +tsd.expectAssignable(Bun.spawnSync([], { stdio: ["ignore", "ignore", "inherit"] })); +tsd.expectAssignable(Bun.spawnSync([], { stdio: [null, null, null] })); +// @ts-expect-error stdout: undefined means "pipe", so this process has a stdout Buffer +tsd.expectAssignable(Bun.spawnSync([], { stdio: ["ignore", undefined, "ignore"] })); +// @ts-expect-error stderr: undefined means "pipe" in spawnSync, so this process has a stderr Buffer +tsd.expectAssignable(Bun.spawnSync([], { stdio: ["ignore", "ignore", undefined] })); tsd.expectAssignable>(Bun.spawnSync([], {}));