Skip to content

bun-types: declare Subprocess.writable and lint interface Subprocess against the classes.ts table - #39279

Open
robobun wants to merge 4 commits into
mainfrom
farm/c8823ce6/subprocess-writable-type
Open

bun-types: declare Subprocess.writable and lint interface Subprocess against the classes.ts table#39279
robobun wants to merge 4 commits into
mainfrom
farm/c8823ce6/subprocess-writable-type

Conversation

@robobun

@robobun robobun commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • Bun.spawn(["cat"], { stdin: "pipe" }).writable fails to type-check: error TS2339: Property 'writable' does not exist on type 'Subprocess<"pipe", "pipe", "inherit">'.
  • The property exists at runtime. src/runtime/api/BunObject.classes.ts:71 registers writable on the Subprocess prototype as { getter: "getStdin", cache: "stdin" }, so it runs the stdin getter and shares its cache slot: proc.writable === proc.stdin for every stdin configuration (transcript below). It was added in the same commit as readable (4700762, 2022).
  • interface Subprocess in packages/bun-types/bun.d.ts has declared readable (the stdout alias) and never writable, and nothing compares the interface with the table: of the 20 members the table installs, writable and connected are undeclared today (connected is being declared by bun-types: declare Subprocess.connected #38677).

Fix

Background

  • *.classes.ts files are the input of bun's class codegen: each proto entry becomes a property on the native class's prototype. cache: true stores a getter's result on the instance so later reads return the same object; cache: "stdin" makes the writable getter read and write the slot stdin uses, which is what makes the two properties return the identical value. Entries marked internal/privateSymbol/publicSymbol are not installed under a plain name, and a key spelled @@x is installed under Symbol.x.
  • packages/bun-types is the published bun-types/@types/bun surface. It is hand-written, not generated from the class definitions, so a prototype property can exist at runtime without a declaration. interface Subprocess there is the type Bun.spawn returns, and SpawnOptions.WritableToIO<In> is the mapping it already uses to turn the stdin option's type into the stdin property's type ("pipe" gives a FileSink, the writer for the process's stdin pipe).
  • test/internal/source-lints/ holds tests that only read the source tree; they run in the source-lints GitHub workflow against a released bun, triggered by the paths listed in the workflow, and are excluded from the Buildkite lanes. test/integration/bun-types/ packs bun-types and type-checks fixture/*.ts against it; expectType(x).is<T>() there is an exact type-equality assertion.
Lint output with main's bun.d.ts, and the simulated drift shapes
(pass) every member of interface Subprocess in packages/bun-types/bun.d.ts has a shape this lint can read
(fail) packages/bun-types/bun.d.ts declares every Subprocess member src/runtime/api/BunObject.classes.ts installs
    - []
    + [
    +   "writable",
    + ]
(pass) packages/bun-types/bun.d.ts declares no Subprocess member src/runtime/api/BunObject.classes.ts does not install
(pass) pendingDeclarations and declaredByExtends describe the current files
### d.ts declares connected while it is still pending
    +   "connected (#38677) is declared in packages/bun-types/bun.d.ts now; delete its entry",
    (fail) pendingDeclarations and declaredByExtends describe the current files
### d.ts declares paused, which is not registered
    +   "paused",
    (fail) packages/bun-types/bun.d.ts declares no Subprocess member src/runtime/api/BunObject.classes.ts does not install
### d.ts gains a member shape the lint does not know
    +   "private brand: never;",
    (fail) every member of interface Subprocess in packages/bun-types/bun.d.ts has a shape this lint can read
### interface no longer extends AsyncDisposable
    +   "@@asyncDispose",
    (fail) packages/bun-types/bun.d.ts declares every Subprocess member src/runtime/api/BunObject.classes.ts installs
    +   "interface Subprocess no longer extends AsyncDisposable, which declared @@asyncDispose",
    (fail) pendingDeclarations and declaredByExtends describe the current files
### d.ts gains a multi-line send overload and declares [Symbol.asyncDispose] in the body too
    4 pass
### table gains paused (public) and hidden (internal: true), d.ts untouched
    +   "paused",
    (fail) packages/bun-types/bun.d.ts declares every Subprocess member src/runtime/api/BunObject.classes.ts installs
### table drops connected while it is still pending
    +   "connected (#38677) is no longer registered in src/runtime/api/BunObject.classes.ts",
    (fail) pendingDeclarations and declaredByExtends describe the current files
### table drops @@asyncDispose while the interface still extends AsyncDisposable
    +   "@@asyncDispose",
    (fail) packages/bun-types/bun.d.ts declares no Subprocess member src/runtime/api/BunObject.classes.ts does not install

Trial merge with #38677 (both declaring their member):

+   "connected (#38677) is declared in packages/bun-types/bun.d.ts now; delete its entry",
(fail) pendingDeclarations and declaredByExtends describe the current files
Fixture failure with main's bun.d.ts (release build)
spawn.ts(199,25): error TS2339: Property 'writable' does not exist on type 'Subprocess<In, Out, Err>'.
spawn.ts(206,23): error TS2339: Property 'writable' does not exist on type 'Subprocess<"pipe", "pipe", "inherit">'.
spawn.ts(207,8): error TS2339: Property 'writable' does not exist on type 'Subprocess<"pipe", "pipe", "inherit">'.
spawn.ts(212,32): error TS2339: Property 'writable' does not exist on type 'PipedSubprocess'.
spawn.ts(213,35): error TS2339: Property 'writable' does not exist on type 'WritableSubprocess'.
spawn.ts(214,31): error TS2339: Property 'writable' does not exist on type 'NullSubprocess'.
Runtime behavior the declaration describes (release build, linux x64)
descriptor: { get: "function", set: "undefined", enumerable: true, configurable: false }
stdin:"pipe"      -> FileSink   writable === stdin: true
stdin: <fd>       -> number     writable === stdin: true
stdin: Blob       -> undefined  writable === stdin: true
stdin: Uint8Array -> undefined  writable === stdin: true
stdin: default    -> undefined  writable === stdin: true
stdin:"ignore"    -> undefined  writable === stdin: true
terminal: {...}   -> null       writable === stdin: true
readable === stdout: true (ReadableStream)
new Response("x").body.pipeThrough(proc) -> TypeError: The transform's 'writable' property must be a WritableStream
Earlier revision

The first push (18bc3c9) covered the declaration with a bun-types.test.ts case that spawned tsc over fixture/spawn.ts alone, plus fixture blocks restating the stdio option mapping (stdin: 0 is a number, and so on). Self-review pointed out that the tsc case duplicated the whole-fixture run #39270 introduces (and would fail the spawn-site lint it adds), that the mapping blocks conflicted with #39283, and that the check only proved one name was declared while a second member was undeclared in the same table. c42e5dc replaced the case with the lint, trimmed the fixture to the alias assertions and moved it out of #39283's region, and switched the docs lines to the indexed form; c34a74b dropped a pointer comment on the proto table, leaving no src/ change.


[review] gate passed · iteration 0 · 5 files touched

fails on main (without fix)
ASAN without fix: 1 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/internal/source-lints/subprocess-types.test.ts
bun test v1.4.0 (f0f6b2cbb)

test/internal/source-lints/subprocess-types.test.ts:
(pass) every member of interface Subprocess in packages/bun-types/bun.d.ts has a shape this lint can read [1.73ms]
92 | 
93 | test(`${dtsFile} declares every Subprocess member ${classesFile} installs`, () => {
94 |   const undeclared = [...registered]
95 |     .filter(name => !declared.has(name) && !Object.hasOwn(pendingDeclarations, name))
96 |     .sort();
97 |   expect(undeclared).toEqual([]);
                          ^
error: expect(received).toEqual(expected)

- []
+ [
+   "writable",
+ ]

- Expected  - 1
+ Received  + 3

      at <anonymous> (/workspace/bun/test/internal/source-lints/subprocess-types.test.ts:97:22)
(fail) packages/bun-types/bun.d.ts declares every Subprocess member src/runtime/api/BunObject.classes.ts installs [6.76ms]
(pass) packages/bun-types/bun.d.ts declares no Subprocess member src/runtime/api/BunObject.classes.ts does not install [3.38ms]
(pass) pendingDeclarations and decla
... (truncated)

release without fix: 1 FAILED
bun test v1.4.0-canary.1 (eabb96de7)

test/internal/source-lints/subprocess-types.test.ts:
(pass) every member of interface Subprocess in packages/bun-types/bun.d.ts has a shape this lint can read [0.05ms]
92 | 
93 | test(`${dtsFile} declares every Subprocess member ${classesFile} installs`, () => {
94 |   const undeclared = [...registered]
95 |     .filter(name => !declared.has(name) && !Object.hasOwn(pendingDeclarations, name))
96 |     .sort();
97 |   expect(undeclared).toEqual([]);
                          ^
error: expect(received).toEqual(expected)

- []
+ [
+   "writable",
+ ]

- Expected  - 1
+ Received  + 3

      at <anonymous> (/workspace/bun/test/internal/source-lints/subprocess-types.test.ts:97:22)
(fail) packages/bun-types/bun.d.ts declares every Subprocess member src/runtime/api/BunObject.classes.ts installs [0.27ms]
(pass) packages/bun-types/bun.d.ts declares no Subprocess member src/runtime/api/BunObject.classes.ts does not install [0.05ms]
(pass) pendingDeclarations and declaredByExtends describe the current files [0.18ms]

 3 pass
 1 fail
 4 expect() calls
Ran 4 tests across 1 file. [168.00ms]
__F:1:S:0
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/internal/source-lints/subprocess-types.test.ts
bun test v1.4.0 (f0f6b2cbb)

test/internal/source-lints/subprocess-types.test.ts:
(pass) every member of interface Subprocess in packages/bun-types/bun.d.ts has a shape this lint can read [1.93ms]
(pass) packages/bun-types/bun.d.ts declares every Subprocess member src/runtime/api/BunObject.classes.ts installs [4.21ms]
(pass) packages/bun-types/bun.d.ts declares no Subprocess member src/runtime/api/BunObject.classes.ts does not install [3.15ms]
(pass) pendingDeclarations and declaredByExtends describe the current files [12.81ms]

 4 pass
 0 fail
 4 expect() calls
Ran 4 tests across 1 file. [2.93s]
__F:0:S:0

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped)
  target       linux-x64-gnu
  build type   Release
  build dir    ./build/release
  revision     c34a74b327
  features     baseline

22 deps, 120 codegen, 1175 objects in 814ms

ninja: Entering directory `/workspace/bun/build/release'
[1/1236] install /workspace/bun
bun install v1.4.0-canary.1 (eabb96de7)

Checked 107 installs across 153 packages (no changes) [32.00ms]
[2/1236] gen ErrorCode+*.h
[3/1236] install /workspace/bun/packages/bun-error
bun install v1.4.0-canary.1 (eabb96de7)

Checked 1 install across 2 packages (no changes) [3.00ms]
[4/1236] fetch tinycc
[tinycc] up to date
[5/1235] gen bindgenv2
[6/1235] fetch libjpeg-turbo
[libjpeg-turbo] up to date
[7/1235] install /workspace/bun/src/node-fallbacks
bun install v1.4.0-canary.1 (eabb96de7)

Checked 129 installs across 147 packages (no changes) [11.00ms]
[8/1235] gen .bind.ts → GeneratedBindings.cpp
[9/1235] fetch zlib
[zlib] up to date
[10/1235] gen ProcessBindingConstants.lut.h
Generating /workspace/bun/build/release/codegen/ProcessBindingConstants.lut.h from /workspace/bun/src/jsc/bindings/ProcessBindingConstants.cpp
... (truncated)
diff hotspot
.github/workflows/source-lints.yml                 |   2 +
 docs/runtime/child-process.mdx                     |   3 +-
 packages/bun-types/bun.d.ts                        |   8 ++
 test/integration/bun-types/fixture/spawn.ts        |  26 +++++
 .../internal/source-lints/subprocess-types.test.ts | 117 +++++++++++++++++++++
 5 files changed, 155 insertions(+), 1 deletion(-)

gate history · 1 passed · 0 rejected · iteration 0

evidence per changed file
file                                                 reads  edits  tests
.github/workflows/source-lints.yml                       0      0      0
docs/runtime/child-process.mdx                           2      2      0
packages/bun-types/bun.d.ts                              3      1      0
test/integration/bun-types/fixture/spawn.ts              3      4      0
test/internal/source-lints/subprocess-types.test.ts      1      2      0

Subprocess.prototype has had a writable getter since the readable one was
added: both are registered in BunObject.classes.ts, and writable returns
the same cached value as stdin. bun.d.ts declared readable but not
writable, so proc.writable was a type error while working at runtime.

Declare it with stdin's type, list it in the docs reference block, and
assert in the bun-types fixture that writable and readable have the same
types as stdin and stdout for every stdio configuration. The fixture is
also type-checked on its own by a tsc run that is not skipped on debug
builds.
@robobun
robobun requested a review from alii as a code owner August 16, 2026 03:10
@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 3 minutes

Limit details: You’ve used all 5 included reviews currently available under your plan.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0e2f6f12-81ae-4e21-a9b4-2243be49b386

📥 Commits

Reviewing files that changed from the base of the PR and between f0f6b2c and 98827ed.

📒 Files selected for processing (5)
  • .github/workflows/source-lints.yml
  • docs/runtime/child-process.mdx
  • packages/bun-types/bun.d.ts
  • test/integration/bun-types/fixture/spawn.ts
  • test/internal/source-lints/subprocess-types.test.ts

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator Author

Status: restructured after self-review, ready for review.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — declares the missing Subprocess.writable type alias to match the runtime getter that has existed since 2022.

What was reviewed:

  • Confirmed src/runtime/api/BunObject.classes.ts:71 registers writable as { getter: "getStdin", cache: "stdin" }, so SpawnOptions.WritableToIO<In> (identical to stdin's type) is the correct declaration — same treatment readable already gets for stdout.
  • Checked the new fixture assertions don't add diagnostics to the lib.dom.d.ts expected-error list (they use the same ReadableStream<Uint8Array<ArrayBuffer>> / FileSink shapes already asserted earlier in the file without error).
  • The new Bun.spawn > fixture/spawn.ts type-checks test uses tsconfig.files with an absolute path into BASE_FIXTURE_DIR (rather than include like the mmap case) so ./utilities resolves relative to the fixture file — verified this is intentional and correct.
Extended reasoning...

Overview

This PR adds readonly writable: SpawnOptions.WritableToIO<In> to interface Subprocess in packages/bun-types/bun.d.ts, mirroring the existing readonly readable: SpawnOptions.ReadableToIO<Out> declaration for stdout. It also adds the property to the docs Reference block, adds fixture type assertions in test/integration/bun-types/fixture/spawn.ts, and adds a debug-build-friendly tsc test case that type-checks that fixture in isolation.

The runtime already exposes this property: src/runtime/api/BunObject.classes.ts:71 registers writable: { getter: "getStdin", cache: "stdin" }, so proc.writable === proc.stdin for every stdin configuration. The declaration uses the exact same WritableToIO<In> mapping as stdin at line 7547, so the two properties have identical types by construction.

Security risks

None. This is a pure type-declaration and docs change with no runtime code modification.

Level of scrutiny

Low. Type declarations for an existing, long-shipped runtime property, following the exact pattern of the sibling readable declaration two lines below it. The PR description demonstrates the tests fail without the .d.ts hunk (9 × TS2339) and pass with it, on both release (all 16 in-process cases + tsgo) and debug builds.

Other factors

  • The fixture additions cover the generic case (writablestdin type for any <In, Out, Err>), concrete resolutions ("pipe"FileSink, fd → number, default/"inherit"undefined, stdio tuple), the utility type aliases, and readonly enforcement via @ts-expect-error.
  • The new test case in bun-types.test.ts follows the existing Bun.mmap pattern but points tsconfig.files at the absolute BASE_FIXTURE_DIR/spawn.ts path (rather than copying an inline file into checkDir), which correctly lets the fixture's import "./utilities" resolve. Pipes are drained concurrently and exit code is asserted last per repo conventions.
  • The JSDoc explicitly notes this is a FileSink, not a WritableStream, so users don't misread it as making pipeThrough(proc) work — a thoughtful clarification.
  • No prior human review comments to address; only a CodeRabbit rate-limit notice in the timeline.

…ead of adding a tsc case

The spawned tsc case duplicated the one test/integration/bun-types is
converging on (one whole-fixture run), and only proved that the one name
was declared. A Subprocess instance of the RedisClient declaration lint
checks every member the table installs, with connected listed as pending
on the PR that declares it and @@asyncDispose accounted for by the
interface's extends clause. bun.d.ts joins the source-lints triggers
because the lint reads it.

The fixture keeps the assertions about the aliases themselves (same type
as stdin/stdout for any Subprocess, FileSink under stdin: "pipe",
read-only, the utility types) and drops the ones that restated the
stdio option mapping; they also move below the block another change to
that mapping rewrites. The docs reference block spells both aliases as
indexed access types so neither copy can drift from stdin/stdout.
Comment thread src/runtime/api/BunObject.classes.ts Outdated
The lint's failure output names both files, so the table does not need
to.
@robobun

robobun commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 1:37 AM PT - Aug 16th, 2026

@robobun, your commit 98827edf21a68f0373218081d30fe6e959fb1b4d passed in Build #99327! 🎉


🧪   To try this PR locally:

bunx bun-pr 39279

That installs a local version of the PR into your bun-39279 executable, so you can run:

bun-39279 --bun

@robobun robobun changed the title bun-types: declare Subprocess.writable bun-types: declare Subprocess.writable and lint interface Subprocess against the classes.ts table Aug 16, 2026

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — types-only addition mirroring the just-merged RedisClient lint pattern.

What was reviewed:

  • writable in BunObject.classes.ts:71 is { getter: "getStdin", cache: "stdin" }, so SpawnOptions.WritableToIO<In> (identical to stdin's declared type) is the correct declaration.
  • The new lint is a close copy of redis-client-types.test.ts (parent commit f0f6b2c), adapted for an interface body: the header regex matches the actual multi-line interface Subprocess<...> extends AsyncDisposable { shape, and the noConstructor/empty-klass guard matches the current table.
  • Fixture assertions use the existing expectType<T>().is<U>() zero-arg overload from utilities.ts; the comment-cop feedback on the earlier src/ change was addressed in c34a74b.
Extended reasoning...

Overview

Adds readonly writable: SpawnOptions.WritableToIO<In> to interface Subprocess in packages/bun-types/bun.d.ts, plus a source-lint test comparing that interface's member set against the Subprocess.proto table in BunObject.classes.ts, a fixture block in test/integration/bun-types/fixture/spawn.ts asserting the alias types, a docs Reference-block line, and a workflow path trigger. No native/runtime code is touched.

Security risks

None. This is a .d.ts declaration, a docs line, a type-fixture, a source-tree-reading lint test, and a CI workflow path filter. Nothing executes at runtime and nothing handles untrusted input.

Level of scrutiny

Low-to-medium. The core change is an 8-line type declaration that exactly mirrors the existing stdin declaration (both use SpawnOptions.WritableToIO<In>), matching runtime behavior confirmed at BunObject.classes.ts:71 where writable shares stdin's getter and cache slot. The lint test is a near-verbatim adaptation of redis-client-types.test.ts from #39271 (this PR's direct parent commit), differing only where an interface body differs from a class body: no static/get/set in the member regex, an extends-clause map for @@asyncDispose, and a guard rejecting constructor/statics that an interface cannot mirror. I checked the header regex against the actual d.ts (multi-line type-param list, extends AsyncDisposable) and it matches; noConstructor: true and klass: {} on the Subprocess entry satisfy the guard.

Other factors

The gate evidence shows the lint fails on main naming exactly ["writable"] and passes on the branch (4/4), and the bun-types integration test goes from 6 TS2339 errors to 15/15 pass. The one prior reviewer comment (comment-cop on a src/ pointer comment) was resolved in c34a74b — the PR no longer touches src/. The pendingDeclarations entry for connected (#38677) is an intentional coordination point with a concurrent PR, matching the same mechanism in the RedisClient lint. The fixture's expectType<T>().is<U>() zero-arg form is the documented overload in utilities.ts and is already used elsewhere in the fixtures.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants