Skip to content

Commit

Permalink
bun-types: infer strict Subprocess from Bun.spawn() options, part…
Browse files Browse the repository at this point in the history
… 2 (#2573)
  • Loading branch information
paperdave authored Apr 6, 2023
1 parent 8a73c2a commit f788519
Show file tree
Hide file tree
Showing 9 changed files with 165 additions and 67 deletions.
105 changes: 61 additions & 44 deletions docs/api/spawn.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
```

Expand Down Expand Up @@ -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>;

Expand All @@ -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"
Expand Down
46 changes: 43 additions & 3 deletions packages/bun-types/bun.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2972,7 +2972,7 @@ declare module "bun" {
| "inherit"
| "ignore"
| null // equivalent to "ignore"
| undefined // use default
| undefined // to use default
| FileBlob
| ArrayBufferView
| number;
Expand All @@ -2985,7 +2985,7 @@ declare module "bun" {
| "inherit"
| "ignore"
| null // equivalent to "ignore"
| undefined // use default
| undefined // to use default
| FileBlob
| ArrayBufferView
| number
Expand Down Expand Up @@ -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>;

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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}.
Expand Down
44 changes: 43 additions & 1 deletion packages/bun-types/tests/spawn.test-d.ts
Original file line number Diff line number Diff line change
@@ -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"]);
Expand Down Expand Up @@ -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([], {}));
12 changes: 6 additions & 6 deletions test/cli/hot/hot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")) {
Expand Down Expand Up @@ -66,15 +66,15 @@ 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
onError && onError(str);
}
})();

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")) {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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")) {
Expand Down Expand Up @@ -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")) {
Expand Down
2 changes: 1 addition & 1 deletion test/js/bun/spawn/spawn-streaming-stdin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion test/js/bun/spawn/spawn-streaming-stdout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading

0 comments on commit f788519

Please sign in to comment.