diff options
author | 2023-04-06 16:59:06 -0400 | |
---|---|---|
committer | 2023-04-06 13:59:06 -0700 | |
commit | f788519263b5713cd5655a6e3bcf3e3f9b1aea02 (patch) | |
tree | 748c890359263f9a7603f0f06d4d307907db939f | |
parent | 8a73c2a453fc3a569073f6f1f584369d657686f5 (diff) | |
download | bun-f788519263b5713cd5655a6e3bcf3e3f9b1aea02.tar.gz bun-f788519263b5713cd5655a6e3bcf3e3f9b1aea02.tar.zst bun-f788519263b5713cd5655a6e3bcf3e3f9b1aea02.zip |
bun-types: infer strict `Subprocess` from `Bun.spawn()` options, part 2 (#2573)
-rw-r--r-- | docs/api/spawn.md | 105 | ||||
-rw-r--r-- | packages/bun-types/bun.d.ts | 46 | ||||
-rw-r--r-- | packages/bun-types/tests/spawn.test-d.ts | 44 | ||||
-rw-r--r-- | test/cli/hot/hot.test.ts | 12 | ||||
-rw-r--r-- | test/js/bun/spawn/spawn-streaming-stdin.test.ts | 2 | ||||
-rw-r--r-- | test/js/bun/spawn/spawn-streaming-stdout.test.ts | 2 | ||||
-rw-r--r-- | test/js/bun/spawn/spawn.test.ts | 8 | ||||
-rw-r--r-- | test/js/node/child_process/child_process.test.ts | 2 | ||||
-rw-r--r-- | test/regression/issue/02499.test.ts | 11 |
9 files changed, 165 insertions, 67 deletions
diff --git a/docs/api/spawn.md b/docs/api/spawn.md index 3b7c055e8..024f7cf1b 100644 --- a/docs/api/spawn.md +++ b/docs/api/spawn.md @@ -89,17 +89,17 @@ const proc = Bun.spawn(["cat"], { }); // enqueue string data -proc.stdin!.write("hello"); +proc.stdin.write("hello"); // enqueue binary data const enc = new TextEncoder(); -proc.stdin!.write(enc.encode(" world!")); +proc.stdin.write(enc.encode(" world!")); // send buffered data -proc.stdin!.flush(); +proc.stdin.flush(); // close the input stream -proc.stdin!.end(); +proc.stdin.end(); ``` ## Output streams @@ -194,7 +194,7 @@ Bun provides a synchronous equivalent of `Bun.spawn` called `Bun.spawnSync`. Thi ```ts const proc = Bun.spawnSync(["echo", "hello"]); -console.log(proc.stdout!.toString()); +console.log(proc.stdout.toString()); // => "hello\n" ``` @@ -227,55 +227,63 @@ spawnSync echo hi 1.47 ms/iter (1.14 ms … 2.64 ms) 1.57 ms 2.37 ms ## Reference +A simple reference of the Spawn API and types are shown below. The real types have complex generics to strongly type the `Subprocess` streams with the options passed to `Bun.spawn` and `Bun.spawnSync`. For full details, find these types as defined [bun.d.ts](https://github.com/oven-sh/bun/blob/main/packages/bun-types/bun.d.ts). + ```ts interface Bun { - spawn(command: string[], options?: SpawnOptions): Subprocess; - spawnSync(command: string[], options?: SpawnOptions): SyncSubprocess; + spawn(command: string[], options?: SpawnOptions.OptionsObject): Subprocess; + spawnSync(command: string[], options?: SpawnOptions.OptionsObject): SyncSubprocess; + + spawn(options: { cmd: string[] } & SpawnOptions.OptionsObject): Subprocess; + spawnSync(options: { cmd: string[] } & SpawnOptions.OptionsObject): SyncSubprocess; } -interface SpawnOptions { - cwd?: string; - env?: Record<string, string>; - stdin?: +namespace SpawnOptions { + interface OptionsObject { + cwd?: string; + env?: Record<string, string>; + stdin?: SpawnOptions.Readable; + stdout?: SpawnOptions.Writable; + stderr?: SpawnOptions.Writable; + onExit?: ( + proc: Subprocess, + exitCode: number | null, + signalCode: string | null, + error: Error | null, + ) => void; + } + + type Readable = | "pipe" | "inherit" | "ignore" - | ReadableStream - | BunFile - | Blob - | Response - | Request - | number - | null; - stdout?: + | null // equivalent to "ignore" + | undefined // to use default + | FileBlob + | ArrayBufferView + | number; + + type Writable = | "pipe" | "inherit" | "ignore" - | BunFile - | TypedArray - | DataView - | null; - stderr?: - | "pipe" - | "inherit" - | "ignore" - | BunFile - | TypedArray - | DataView - | null; - onExit?: ( - proc: Subprocess, - exitCode: number | null, - signalCode: string | null, - error: Error | null, - ) => void; + | null // equivalent to "ignore" + | undefined // to use default + | FileBlob + | ArrayBufferView + | number + | ReadableStream + | Blob + | Response + | Request; } -interface Subprocess { +interface Subprocess<Stdin, Stdout, Stderr> { readonly pid: number; - readonly stdin?: number | ReadableStream | FileSink; - readonly stdout?: number | ReadableStream; - readonly stderr?: number | ReadableStream; + // the exact stream types here are derived from the generic parameters + readonly stdin: number | ReadableStream | FileSink | undefined; + readonly stdout: number | ReadableStream | undefined; + readonly stderr: number | ReadableStream | undefined; readonly exited: Promise<number>; @@ -288,13 +296,22 @@ interface Subprocess { kill(code?: number): void; } -interface SyncSubprocess { +interface SyncSubprocess<Stdout, Stderr> { readonly pid: number; readonly success: boolean; - readonly stdout: Buffer; - readonly stderr: Buffer; + // the exact buffer types here are derived from the generic parameters + readonly stdout: Buffer | undefined; + readonly stderr: Buffer | undefined; } +type ReadableSubprocess = Subprocess<any, "pipe", "pipe">; +type WritableSubprocess = Subprocess<"pipe", any, any>; +type PipedSubprocess = Subprocess<"pipe", "pipe", "pipe">; +type NullSubprocess = Subprocess<null, null, null> + +type ReadableSyncSubprocess = SyncSubprocess<"pipe", "pipe">; +type NullSyncSubprocess = SyncSubprocess<null, null> + type Signal = | "SIGABRT" | "SIGALRM" diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 0c7116d40..e8374c928 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -2972,7 +2972,7 @@ declare module "bun" { | "inherit" | "ignore" | null // equivalent to "ignore" - | undefined // use default + | undefined // to use default | FileBlob | ArrayBufferView | number; @@ -2985,7 +2985,7 @@ declare module "bun" { | "inherit" | "ignore" | null // equivalent to "ignore" - | undefined // use default + | undefined // to use default | FileBlob | ArrayBufferView | number @@ -3116,7 +3116,7 @@ declare module "bun" { // aka if true that means the user didn't specify anything Writable extends In ? "ignore" : In, Readable extends Out ? "pipe" : Out, - Readable extends Err ? "pipe" : Err + Readable extends Err ? "inherit" : Err > : Subprocess<Writable, Readable, Readable>; @@ -3128,6 +3128,8 @@ declare module "bun" { > : SyncSubprocess<Readable, Readable>; + type ReadableIO = ReadableStream<Buffer> | number | undefined; + type ReadableToIO<X extends Readable> = X extends "pipe" | undefined ? ReadableStream<Buffer> : X extends FileBlob | ArrayBufferView | number @@ -3138,6 +3140,8 @@ declare module "bun" { ? Buffer : undefined; + type WritableIO = FileSink | number | undefined; + type WritableToIO<X extends Writable> = X extends "pipe" ? FileSink : X extends @@ -3151,6 +3155,15 @@ declare module "bun" { : undefined; } + /** + * A process created by {@link Bun.spawn}. + * + * This type accepts 3 optional type parameters which correspond to the `stdio` array from the options object. Instead of specifying these, you should use one of the following utility types instead: + * - {@link ReadableSubprocess} (any, pipe, pipe) + * - {@link WritableSubprocess} (pipe, any, any) + * - {@link PipedSubprocess} (pipe, pipe, pipe) + * - {@link NullSubprocess} (ignore, ignore, ignore) + */ interface Subprocess< In extends SpawnOptions.Writable = SpawnOptions.Writable, Out extends SpawnOptions.Readable = SpawnOptions.Readable, @@ -3229,6 +3242,13 @@ declare module "bun" { unref(): void; } + /** + * A process created by {@link Bun.spawnSync}. + * + * This type accepts 2 optional type parameters which correspond to the `stdout` and `stderr` options. Instead of specifying these, you should use one of the following utility types instead: + * - {@link ReadableSyncSubprocess} (pipe, pipe) + * - {@link NullSyncSubprocess} (ignore, ignore) + */ interface SyncSubprocess< Out extends SpawnOptions.Readable = SpawnOptions.Readable, Err extends SpawnOptions.Readable = SpawnOptions.Readable, @@ -3364,6 +3384,26 @@ declare module "bun" { options?: Opts, ): SpawnOptions.OptionsToSyncSubprocess<Opts>; + /** Utility type for any process from {@link Bun.spawn()} with both stdout and stderr set to `"pipe"` */ + type ReadableSubprocess = Subprocess<any, "pipe", "pipe">; + /** Utility type for any process from {@link Bun.spawn()} with stdin set to `"pipe"` */ + 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. */ + type NullSubprocess = Subprocess< + "ignore" | "inherit" | null | undefined, + "ignore" | "inherit" | null | undefined, + "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 + >; + export class FileSystemRouter { /** * Create a new {@link FileSystemRouter}. diff --git a/packages/bun-types/tests/spawn.test-d.ts b/packages/bun-types/tests/spawn.test-d.ts index f5ef01277..14a753d04 100644 --- a/packages/bun-types/tests/spawn.test-d.ts +++ b/packages/bun-types/tests/spawn.test-d.ts @@ -1,4 +1,11 @@ -import { FileSink } from "bun"; +import { + FileSink, + NullSubprocess, + PipedSubprocess, + ReadableSubprocess, + SyncSubprocess, + WritableSubprocess, +} from "bun"; import * as tsd from "tsd"; Bun.spawn(["echo", "hello"]); @@ -122,3 +129,38 @@ Bun.spawn(["echo", "hello"]); }); tsd.expectType<number>(proc.stdin); } +tsd.expectAssignable<PipedSubprocess>( + Bun.spawn([], { stdio: ["pipe", "pipe", "pipe"] }), +); +tsd.expectNotAssignable<PipedSubprocess>( + Bun.spawn([], { stdio: ["inherit", "inherit", "inherit"] }), +); +tsd.expectAssignable<ReadableSubprocess>( + Bun.spawn([], { stdio: ["ignore", "pipe", "pipe"] }), +); +tsd.expectAssignable<ReadableSubprocess>( + Bun.spawn([], { stdio: ["pipe", "pipe", "pipe"] }), +); +tsd.expectNotAssignable<ReadableSubprocess>( + Bun.spawn([], { stdio: ["pipe", "ignore", "pipe"] }), +); +tsd.expectAssignable<WritableSubprocess>( + Bun.spawn([], { stdio: ["pipe", "pipe", "pipe"] }), +); +tsd.expectAssignable<WritableSubprocess>( + Bun.spawn([], { stdio: ["pipe", "ignore", "inherit"] }), +); +tsd.expectNotAssignable<WritableSubprocess>( + Bun.spawn([], { stdio: ["ignore", "pipe", "pipe"] }), +); +tsd.expectAssignable<NullSubprocess>( + Bun.spawn([], { stdio: ["ignore", "inherit", "ignore"] }), +); +tsd.expectAssignable<NullSubprocess>( + Bun.spawn([], { stdio: [null, null, null] }), +); +tsd.expectNotAssignable<ReadableSubprocess>(Bun.spawn([], {})); +tsd.expectNotAssignable<PipedSubprocess>(Bun.spawn([], {})); + +tsd.expectAssignable<SyncSubprocess>(Bun.spawnSync([], {})); +tsd.expectAssignable<SyncSubprocess>(Bun.spawnSync([], {})); diff --git a/test/cli/hot/hot.test.ts b/test/cli/hot/hot.test.ts index 7888f0308..63cc3b064 100644 --- a/test/cli/hot/hot.test.ts +++ b/test/cli/hot/hot.test.ts @@ -19,7 +19,7 @@ it("should hot reload when file is overwritten", async () => { writeFileSync(root, readFileSync(root, "utf-8")); } - for await (const line of runner.stdout!) { + for await (const line of runner.stdout) { var str = new TextDecoder().decode(line); var any = false; for (let line of str.split("\n")) { @@ -66,7 +66,7 @@ it("should recover from errors", async () => { var errors: string[] = []; var onError: (...args: any[]) => void; (async () => { - for await (let line of runner.stderr!) { + for await (let line of runner.stderr) { var str = new TextDecoder().decode(line); errors.push(str); // @ts-ignore @@ -74,7 +74,7 @@ it("should recover from errors", async () => { } })(); - for await (const line of runner.stdout!) { + for await (const line of runner.stdout) { var str = new TextDecoder().decode(line); var any = false; for (let line of str.split("\n")) { @@ -138,7 +138,7 @@ it("should not hot reload when a random file is written", async () => { if (finished) { return; } - for await (const line of runner.stdout!) { + for await (const line of runner.stdout) { if (finished) { return; } @@ -182,7 +182,7 @@ it("should hot reload when a file is deleted and rewritten", async () => { writeFileSync(root, contents); } - for await (const line of runner.stdout!) { + for await (const line of runner.stdout) { var str = new TextDecoder().decode(line); var any = false; for (let line of str.split("\n")) { @@ -227,7 +227,7 @@ it("should hot reload when a file is renamed() into place", async () => { await 1; } - for await (const line of runner.stdout!) { + for await (const line of runner.stdout) { var str = new TextDecoder().decode(line); var any = false; for (let line of str.split("\n")) { diff --git a/test/js/bun/spawn/spawn-streaming-stdin.test.ts b/test/js/bun/spawn/spawn-streaming-stdin.test.ts index f69e7d9b6..27efa14ec 100644 --- a/test/js/bun/spawn/spawn-streaming-stdin.test.ts +++ b/test/js/bun/spawn/spawn-streaming-stdin.test.ts @@ -22,7 +22,7 @@ test("spawn can write to stdin multiple chunks", async () => { var chunks: any[] = []; const prom = (async function () { try { - for await (var chunk of proc.stdout!) { + for await (var chunk of proc.stdout) { chunks.push(chunk); } } catch (e: any) { diff --git a/test/js/bun/spawn/spawn-streaming-stdout.test.ts b/test/js/bun/spawn/spawn-streaming-stdout.test.ts index 54c1451f0..558a70371 100644 --- a/test/js/bun/spawn/spawn-streaming-stdout.test.ts +++ b/test/js/bun/spawn/spawn-streaming-stdout.test.ts @@ -21,7 +21,7 @@ test("spawn can read from stdout multiple chunks", async () => { var chunks = []; let counter = 0; try { - for await (var chunk of proc.stdout!) { + for await (var chunk of proc.stdout) { chunks.push(chunk); counter++; if (counter > 3) break; diff --git a/test/js/bun/spawn/spawn.test.ts b/test/js/bun/spawn/spawn.test.ts index 54b890d51..c43c06d02 100644 --- a/test/js/bun/spawn/spawn.test.ts +++ b/test/js/bun/spawn/spawn.test.ts @@ -116,7 +116,7 @@ for (let [gcTick, label] of [ cmd: ["sleep", "0.1"], }); gcTick(); - for await (const _ of proc.stdout!) { + for await (const _ of proc.stdout) { throw new Error("should not happen"); } gcTick(); @@ -264,7 +264,7 @@ for (let [gcTick, label] of [ stderr: "inherit", }); - var stdout = proc.stdout!; + var stdout = proc.stdout; var reader = stdout.getReader(); proc.stdin!.write("hey\n"); await proc.stdin!.end(); @@ -319,7 +319,7 @@ for (let [gcTick, label] of [ describe("should should allow reading stdout", () => { it("before exit", async () => { const process = callback(); - const output = await readableStreamToText(process.stdout!); + const output = await readableStreamToText(process.stdout); await process.exited; const expected = fixture + "\n"; @@ -366,7 +366,7 @@ for (let [gcTick, label] of [ it("after exit", async () => { const process = callback(); await process.exited; - const output = await readableStreamToText(process.stdout!); + const output = await readableStreamToText(process.stdout); const expected = fixture + "\n"; expect(output.length).toBe(expected.length); expect(output).toBe(expected); diff --git a/test/js/node/child_process/child_process.test.ts b/test/js/node/child_process/child_process.test.ts index c249c6434..f4e08ac74 100644 --- a/test/js/node/child_process/child_process.test.ts +++ b/test/js/node/child_process/child_process.test.ts @@ -327,7 +327,7 @@ describe("Bun.spawn()", () => { stdout: "pipe", }); - for await (const chunk of proc.stdout!) { + for await (const chunk of proc.stdout) { const text = new TextDecoder().decode(chunk); expect(text.trim()).toBe("hello"); } diff --git a/test/regression/issue/02499.test.ts b/test/regression/issue/02499.test.ts index da114d95d..0e4666b36 100644 --- a/test/regression/issue/02499.test.ts +++ b/test/regression/issue/02499.test.ts @@ -44,17 +44,16 @@ it("onAborted() and onWritable are not called after receiving an empty response env: bunEnv, }); - const reader = bunProcess.stdout?.getReader(); + const reader = bunProcess.stdout.getReader(); let hostname, port; { - const chunks = []; + const chunks: Buffer[] = []; var decoder = new TextDecoder(); while (!hostname && !port) { - // @ts-expect-error TODO - var { value, done } = await reader?.read(); + var { value, done } = await reader.read(); if (done) break; if (chunks.length > 0) { - chunks.push(value); + chunks.push(value!); } try { if (chunks.length > 0) { @@ -63,7 +62,7 @@ it("onAborted() and onWritable are not called after receiving an empty response ({ hostname, port } = JSON.parse(decoder.decode(value).trim())); } catch { - chunks.push(value); + chunks.push(value!); } } } |