aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar dave caruso <me@paperdave.net> 2023-04-06 16:59:06 -0400
committerGravatar GitHub <noreply@github.com> 2023-04-06 13:59:06 -0700
commitf788519263b5713cd5655a6e3bcf3e3f9b1aea02 (patch)
tree748c890359263f9a7603f0f06d4d307907db939f
parent8a73c2a453fc3a569073f6f1f584369d657686f5 (diff)
downloadbun-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.md105
-rw-r--r--packages/bun-types/bun.d.ts46
-rw-r--r--packages/bun-types/tests/spawn.test-d.ts44
-rw-r--r--test/cli/hot/hot.test.ts12
-rw-r--r--test/js/bun/spawn/spawn-streaming-stdin.test.ts2
-rw-r--r--test/js/bun/spawn/spawn-streaming-stdout.test.ts2
-rw-r--r--test/js/bun/spawn/spawn.test.ts8
-rw-r--r--test/js/node/child_process/child_process.test.ts2
-rw-r--r--test/regression/issue/02499.test.ts11
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!);
}
}
}