Skip to content

chore: sync upstream oven-sh/bun main (2026-10-16) - #177

Merged
steipete merged 115 commits into
mainfrom
sync/upstream-2026-10-16
Oct 17, 2026
Merged

steipete merged 115 commits into
mainfrom
sync/upstream-2026-10-16

Conversation

@steipete

@steipete steipete commented Oct 16, 2026 •

Copy link
Copy Markdown

Sync frozen oven-sh/bun main f2405c382d43b89d91a2e7c90d9c5f53505a1e71, including native bun lint and bun fmt, while preserving the fork's compatibility fixes. A normal merge also brings in fork main 785be5fef04bd4e4f26447eb2d1d531b28cc57fa (#176 and #178–#185). Both histories are retained; land with a merge commit.

The engine pin is now the published immutable WebKit release f8521bb45429b3e1df6bc6d9b98b8df4376a89c5. webkit-artifacts.json is the published manifest, including its matching Bun qualification provenance. The manifest and all ten archive entries match the release's SHA256SUMS; webkit.ts retains its pin, digest, URL, and ABI checks. Linux was built using the released archive, without a local-engine override.

Final integration and CI — qualified

Current head: 2ff6223de27d133bd4a4bea28391cfce1cddf69b. Normal merge 2b45d4b93a5dac2eebd3fa91dfef18362870baa6 incorporates fork main b76e4b4e6725dfc84eadc67059753a389e4b1c54, including #188. Only CHANGELOG.md conflicted; both entries were retained. The ESRCH fixture change and main integration each passed Codex P2 review. No rebase or force-push was used.

Exact-head native CI run 39026416585 is green on Linux x64 and Darwin arm64. Both summaries detect upstream parent f2405c382d43b89d91a2e7c90d9c5f53505a1e71 and report 6,254 changed paths / 275 fork-delta paths. The actual Actions merge tree matches this head exactly.

Gate Result
Linux x64 260/260 selected files pass
Darwin arm64 264/264 selected files pass; terminal file 23 pass / 2 existing skips / 0 fail
Format, JavaScript/source lints, TypeScript types, package checks, Clippy, Miri, lol-html, Mordant Pass

The preceding run 39023301103 passed Linux 260/260 and Darwin 263/264; its sole failure captured the loader error diagnosed below. The fixture correction was pushed before the fresh green run. No failed-job rerun was needed.

Darwin PTY ESRCH investigation

The missing marker is an environment-sensitive setup precondition. PROC_FLAG_INEXIT does not guarantee that the real EVFILT_PROC registration returns ESRCH: one instrumented candidate trial observed INEXIT and two successful registrations, with correct PTY output and status.

Twenty interleaved idle trials and twenty controlled-load trials ran on each checksum-verified binary:

Binary Idle trials reaching ESRCH Loaded trials reaching ESRCH Output/status failures
Candidate CI binary at 939b208f3 19/20 20/20 0/40
Published 785be5fef0 release 20/20 20/20 0/40
#188 CI binary 20/20 20/20 0/40

Every trial produced exactly 512 ASCII zero bytes and child exit code 3. Raw wait status was 768 (0x300): normal exit, no signal or core flag. Both fixture and controller Bun processes exited 0. No crash occurred in this matrix. Local measurements used macOS 27.0.1; Darwin CI uses macOS 15.7.9. Subsequent CI run 39023301103 captured a concrete cause for a new child exit 134: the system shell inherited the arm64-only test dylib, but required arm64e. dyld aborted before the shell could run its command. Follow-up commit 2ff6223de27d133bd4a4bea28391cfce1cddf69b removes DYLD_INSERT_LIBRARIES only from the PTY child, preserving the parent Bun interposer. The historical 134 / 1,011-byte payload was not saved, so it cannot be byte-matched to this diagnosis.

Commit 8ac6d330300c0494341219aeb0369eb2f4fd6468 compiles the interposer once and retries at most 20 fresh setups until real ESRCH is observed. Every attempt first checks the complete output, child code 3 and outer exit 0. A behavior failure is never retried. Exhaustion fails with an explicit precondition error. No timeout, skip, tolerance, native implementation or engine pin was changed. The bounded-retry version passed ten local full-file runs, each with 23 pass / 2 existing skips / 0 fail. After isolating the child environment, another ten full-file runs passed on the checksum-verified binary from run 39023301103, again with 23 pass / 2 existing skips / 0 fail each; maximum duration was 3.16 seconds. The two-line correction separately passed Codex P2 review.

Historical CI evidence remains part of the qualification:

Attempt Observation
Run 38984493994 Darwin callback-order race; PTY child returned 134 with 1,011 output bytes despite observing ESRCH; outer fixture Bun exited 0
Run 38987897763, attempt 1 Callback fixture passed after synchronization; two deep-format child-timing failures, VM 30-second timeout, and missing ESRCH marker
Run 38987897763, attempt 2 Linux 260/260; Darwin 263/264; only missing ESRCH marker remained

The earlier callback fixture synchronization, exact PTY byte comparison, local binary controls and VM/format diagnostics remain valid. Formatter and VM timing margins remain follow-ups. The corrected exact-head native gate is complete. No merge has been performed.

The complete upstream-changed qualification and classification tables remain accepted separately: all 1,689 files executed, with 0 merge bugs, 23 inherited divergences, 12 environment failures, and 25 intermittent/infrastructure files. Those historical counts and follow-ups are unchanged. Land with a merge commit.

Earlier runtime merge resolutions

The only textual conflict in the earlier runtime merge was CHANGELOG.md: retain both the upstream-sync entry and all new fork-fix entries. Eighteen paths changed in both histories were checked for semantic interactions. The merged tree retains the DONT_CONTEXTIFY receiver behavior, availableParallelism isolation, SourceMap origin coordinates, fs.watch listener registration, nonthrowing ENOTDIR stat behavior, IPC failure callbacks, bound FileHandle.close, Worker descriptor cleanup, and HTTP cork ordering/backpressure.

The cork implementation retains its uncorked empty-queue fast path. One deterministic test-fixture interaction was found: new test text placed an emoji across a 64 KiB read boundary, exposing per-chunk UTF-8 decoding in the FileHandle#createReadStream test. Node, fork main and the candidate all returned identical raw bytes and reproduced the old test accumulator's corruption. The test now joins Buffers and compares raw file bytes, preserving the default stream contract and strengthening the assertion.

Validation

Current head: 2ff6223de27d133bd4a4bea28391cfce1cddf69b. Exact-head native CI is green as recorded above. The complete Linux qualification and failure classifications are recorded below. The original CI run 38973008774 built Linux x64 and macOS arm64 but timed out at entry 478 of its broad selection. That historical run is not a green gate.

The broad selection and live-service mismatch is now fully characterized by the separate qualification below. The sync-aware bounded PR gate is now integrated and its current status is recorded above. Full qualification did not inject persistent credentials or widen limits. The earlier focused release/native/network/benchmark proof remains valid.

Required proof Result
Published WebKit manifest/URL/ABI consistency 13 tests pass; manifest and all 10 artifact entries verified against SHA256SUMS
Linux release build, no LTO Pass; linked against the published archive
Native parity selection 27/27 files pass across original selection and corrected filesystem fixture; standalone net 149/0
#176 and #178–#185 regressions All pass in native selection, affected regression files and full HTTP suites
Raw/duplex TLS 123 pass, 0 fail
HTTPS server 48 pass, 0 fail
Full node HTTP 1970 pass, 0 fail; 23 existing skips, 3 todos
Full node HTTP2 616 pass, 0 fail; 10 existing skips
Owner-exit stress 0 failures in 50 fresh-process trials; threshold remains 5 ordinary failures, abnormal exits always fail
Cork benchmark against fork main, 5B / 64KiB +0.95% / −0.14% mean paired RPS; neither exceeds the 3% regression limit

The benchmark used five alternating pairs per size on one machine with the same server/client CPU assignments and response validation. The baseline CI binary has the identical tree to fork main 785be5fef. The runtime build was made from the merge tree; the follow-up commit changes only the test accumulator, so the compiled runtime is unchanged.

The initial direct networking command omitted --expose-internals, producing module-load errors in fault-injection fixtures; the corrected full commands passed. The direct filesystem command also needed an isolated writable TMPDIR. Original results are retained. No runtime assertion, timeout, memory threshold, worker count, or existing skip was relaxed.

Codex and an independent second engine reviewed only the new merge interactions and published pin at P2; both completed all passes scoped-clean. The small stream-fixture correction received separate Codex and second-engine focused reviews, both P2 scoped-clean. The earlier complete review of the original 59 conflicts and engine adaptations remains applicable.

Known divergences and follow-ups

These preserve the previously qualified behavior and remain separate work:

  • Pre-connect TLSSocket.setServername() is lost on connect; preserve explicit-only SNI while fixing propagation.
  • Array methods over holes with shared process.env as prototype omit inherited indexed values, unlike Node.
  • Worker throwing-stack getters are read twice and trigger an early formatter call; Node reads once without that formatter call.
  • Early TLS close_notify differs from Node before ClientHello; after ClientHello, preserve Node/fork end. Related HTTP CONNECT fetch/WebSocket stalls remain narrowly annotated.
  • TLS 1.2 client-certificate disclosure before hostname rejection matches Node/fork, while the imported upstream assertion expects earlier rejection.
  • Fatal server TLS errors over a Duplex close its transport earlier than Node; preserve the new SSL error reporting.
  • Nested TLS reset notifications omit the Node-observed error notification.
  • The imported HTTP2 backpressure fixture does not deterministically establish its raw writableLength precondition.
  • Live HTTPS setSecureContext retains gaps in TLS1.3 cipher-name replacement, per-key passphrases, and default server cipher preference.
  • The original one-off install-staging second-marker failure remains unexplained and is not proven inherited. Whole-file and contention controls passed, but do not erase it. Follow up with an explicit overlap barrier; no runtime fix or expected-failure marker was added for it.

All existing narrow expected-failure annotations retain their original assertions. Node/fork/candidate controls and the original staging observation remain in the qualification evidence. The sync does not claim unrestricted broad-suite parity.

Reconciliation

The tables record the original fork behavior, upstream behavior, Node evidence where applicable, and the chosen resolution. The current validation results above apply to the historical per-hunk reconciliation below.

Path / hunk Fork behavior Upstream behavior Node 24 oracle / evidence Decision
LICENSE.md (1) Existing runtime dependency attributions Adds checker, lint and formatter attributions N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Upstream license table retains existing fork rows and adds required lint/format/type-check attribution.
scripts/build/AGENTS.md (1) AGENTS migration and fork build instructions Additional clang image validation guidance N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Keep fork AGENTS migration and upstream clang image validation documentation.
src/ast/expr.rs (1) Tracks opening-bracket source location Propagates recursive stack checks N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Keep fork opening-bracket source location and upstream recursive stack-check parameter.
src/clap/lib.rs (1) Observes each parsed option for environment provenance Adds unknown-flag modes and forwards ParseOptions N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Preserve per-option observation and add upstream unknown-flag modes.
src/clap/lib.rs (2) Observes each parsed option for environment provenance Adds unknown-flag modes and forwards ParseOptions N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Pass complete ParseOptions directly; this retains both new upstream modes and fork observer.
src/clap/lib.rs (3) Observes each parsed option for environment provenance Adds unknown-flag modes and forwards ParseOptions N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Pass complete ParseOptions directly; this retains both new upstream modes and fork observer.
src/codegen/generate-js2native.ts (1) ServerWebSocket native binding SemverObject native binding N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Keep fork ServerWebSocket binding and new upstream SemverObject binding.
src/js/builtins/CommonJS.ts (1) Separates builtin function name from mutable public require Refactors CommonJS builtin loading N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Preserve fork distinct builtin function name and overridden public require name; prevents builtin import collision.
src/js/node/diagnostics_channel.ts (1) Copy-on-write subscriber arrays Captured array push protects against tampering Retained HTTP/TLS suites exercised; exact results are recorded above. Upstream safe captured push handles tampered subscribers; copy-on-write behavior remains equivalent and tests retained.
src/js/node/diagnostics_channel.ts (2) Copy-on-write subscriber arrays Captured array push protects against tampering Retained HTTP/TLS suites exercised; exact results are recorded above. Upstream safe captured push handles tampered subscribers; copy-on-write behavior remains equivalent and tests retained.
src/jsc/RuntimeTranspilerCache.rs (1) Cache versions include fork transformations through 38 Numeric literal output changed; old cache must invalidate N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Preserve fork versions 34–38 and invalidate for upstream numeric-literal output.
src/jsc/VirtualMachine.rs (1) Decodes ESM specifiers without misreading literal delimiters; retains hooks Changes error binding in the same resolution flow N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Retain fork #167 literal delimiter/ESM resolution, package-scope diagnostics and hook boundaries; upstream error binding is equivalent.
src/jsc/bindings/ModuleLoader.cpp (1) Literal delimiter handling and module-hook scopes Strict-codegen macro and upstream loader changes N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Retain fork URL delimiter handling and module-hook scopes; adapt strict code-generation guard to upstream macro separately.
src/jsc/bindings/ModuleLoader.cpp (2) Literal delimiter handling and module-hook scopes Strict-codegen macro and upstream loader changes N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Retain fork URL delimiter handling and module-hook scopes; adapt strict code-generation guard to upstream macro separately.
src/jsc/bindings/webcore/JSFetchHeaders.cpp (1) Accepts Headers init across realms Reads new.target structure before allocating N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Keep fork cross-realm Headers-init recognition and upstream new.target structure read before allocation.
src/jsc/bindings/webcore/JSWorker.cpp (1) Validates inherited options, preloads and env before thread side effects Validates constructor prototype before irreversible setup Retained HTTP/TLS suites exercised; exact results are recorded above. Keep fork NODE_OPTIONS/BUN_OPTIONS/execArgv/preload validation before upstream prototype lookup and before any env-tree/thread side effects.
src/jsc/bindings/JSEnvironmentVariableMap.cpp (1) Header-owned map and inherited writes defined on receiver Inline map definition overlaps the fork implementation N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Fork has header-owned JSSharedEnvMap; do not duplicate class. Keep definePropertyOnReceiver for inherited environment writes.
src/jsc/bindings/JSEnvironmentVariableMap.cpp (2) Header-owned map and inherited writes defined on receiver Inline map definition overlaps the fork implementation N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Fork has header-owned JSSharedEnvMap; do not duplicate class. Keep definePropertyOnReceiver for inherited environment writes.
src/js_parser/parse/parse_prefix.rs (1) Tracks type stripping and source positions Propagates fallible parser recursion N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Retain fork type-strip/source-position recording and upstream fallible parser stack-limit propagation.
src/js_parser/parse/parse_prefix.rs (2) Tracks type stripping and source positions Propagates fallible parser recursion N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Retain fork type-strip/source-position recording and upstream fallible parser stack-limit propagation.
src/js_parser/parse/parse_suffix.rs (1) Tracks parenthesized runtime syntax and type-strip positions Propagates fallible parser recursion N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Retain fork parenthesized runtime syntax/source mapping and type-strip recording, plus upstream fallible parser propagation.
src/js_parser/parse/parse_suffix.rs (2) Tracks parenthesized runtime syntax and type-strip positions Propagates fallible parser recursion N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Retain fork parenthesized runtime syntax/source mapping and type-strip recording, plus upstream fallible parser propagation.
src/runtime/api/bun/Terminal.rs (1) Cancels deferred resume and releases its reference Releases ModuleGraph abort handle N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Retain fork deferred-resume cancellation/ref cleanup and upstream ModuleGraph abort-handle release.
src/sys/lib.rs (1) Existing reparse-point implementation Clarifies Windows reparse-point comment N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Use upstream clarified Windows reparse-point semantics comment; no behavior difference in this hunk.
src/resolver/resolver.rs (1) Hook tsconfig bypass and correct directory ownership Extracted loader and resolver-specific overrides N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Retain fork hook tsconfig-path bypass and upstream per-resolver override selection.
src/resolver/resolver.rs (2) Hook tsconfig bypass and correct directory ownership Extracted loader and resolver-specific overrides N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Retain fork hook tsconfig-path bypass and upstream per-resolver override selection.
src/resolver/resolver.rs (3) Hook tsconfig bypass and correct directory ownership Extracted loader and resolver-specific overrides N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Use upstream extracted loader (same errors); never read override through unrelated directory fd.
src/runtime/cli/mod.rs (1) Distinguishes absent and empty eval; tracks option provenance Captures lint program and multiple package fields N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Retain fork optional eval/empty-script distinction and add upstream lint program capture.
src/runtime/cli/mod.rs (2) Distinguishes absent and empty eval; tracks option provenance Captures lint program and multiple package fields N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Use parsed package-manager positionals, including upstream multiple field selection; parser already strips NODE_OPTIONS.
src/runtime/cli/package_manager_command.rs (1) Accounts for NODE_OPTIONS-injected argv prefix Refactors package manager argument handling N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Preserve fork NODE_OPTIONS-injected argv window in direct and pm invocations.
src/runtime/cli/run_command.rs (1) Deferred package-scope validation, loop start and secure node shim Resolver cache policy and pre-execution type checking N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Preserve deferred package-scope validation and upstream resolver global-cache policy.
src/runtime/cli/run_command.rs (2) Deferred package-scope validation, loop start and secure node shim Resolver cache policy and pre-execution type checking N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Preserve fork deferred loop-start ownership while adding upstream pre-execution type check.
src/runtime/cli/run_command.rs (3) Deferred package-scope validation, loop start and secure node shim Resolver cache policy and pre-execution type checking N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Retain actual selected secure node-shim directory and executable basename.
src/runtime/cli/run_command.rs (4) Deferred package-scope validation, loop start and secure node shim Resolver cache policy and pre-execution type checking N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Already set NODE/npm_node_execpath above; do not duplicate mutable loader alias.
src/runtime/node/node_process.rs (1) Deduplicates environment-provided process options Strict enum and compile-time mode handling Retained HTTP/TLS suites exercised; exact results are recorded above. Retain fork deduplication while adopting upstream strict enum and compile-time mode behavior.
src/spawn/process.rs (1) Unlinks completion backreference and avoids blocking macOS reap Dedicated nonblocking macOS reap thread and watch-failure cleanup N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Unlink fork completion-queue backreference before upstream platform-specific poller teardown.
src/spawn/process.rs (2) Unlinks completion backreference and avoids blocking macOS reap Dedicated nonblocking macOS reap thread and watch-failure cleanup N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Upstream nonblocking macOS reap thread supersedes fork nonblocking rewatch and handles ESRCH without blocking the event loop.
src/spawn/process.rs (3) Unlinks completion backreference and avoids blocking macOS reap Dedicated nonblocking macOS reap thread and watch-failure cleanup N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Upstream keeps fork no-block macOS ESRCH and adds cleanup when watch fails.
src/install/lib.rs (1) Private BUN_TMPDIR node shim and actual selected path Cross-drive Windows hard-link directory helper N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Preserve fork BUN_TMPDIR/secure private fallback and selected shim path; add upstream Windows cross-drive hard-link directory helper.
src/install/lib.rs (2) Private BUN_TMPDIR node shim and actual selected path Cross-drive Windows hard-link directory helper N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Preserve fork BUN_TMPDIR/secure private fallback and selected shim path; add upstream Windows cross-drive hard-link directory helper.
src/install/lib.rs (3) Private BUN_TMPDIR node shim and actual selected path Cross-drive Windows hard-link directory helper N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Preserve fork BUN_TMPDIR/secure private fallback and selected shim path; add upstream Windows cross-drive hard-link directory helper.
src/install/lib.rs (4) Private BUN_TMPDIR node shim and actual selected path Cross-drive Windows hard-link directory helper N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Preserve fork BUN_TMPDIR/secure private fallback and selected shim path; add upstream Windows cross-drive hard-link directory helper.
src/install/lib.rs (5) Private BUN_TMPDIR node shim and actual selected path Cross-drive Windows hard-link directory helper N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Preserve fork BUN_TMPDIR/secure private fallback and selected shim path; add upstream Windows cross-drive hard-link directory helper.
src/install/lib.rs (6) Private BUN_TMPDIR node shim and actual selected path Cross-drive Windows hard-link directory helper N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Preserve fork BUN_TMPDIR/secure private fallback and selected shim path; add upstream Windows cross-drive hard-link directory helper.
src/install/lib.rs (7) Private BUN_TMPDIR node shim and actual selected path Cross-drive Windows hard-link directory helper N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Preserve fork BUN_TMPDIR/secure private fallback and selected shim path; add upstream Windows cross-drive hard-link directory helper.
src/install/lib.rs (8) Private BUN_TMPDIR node shim and actual selected path Cross-drive Windows hard-link directory helper N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Preserve fork BUN_TMPDIR/secure private fallback and selected shim path; add upstream Windows cross-drive hard-link directory helper.
src/install/patch_install.rs (1) Path-bearing patch-read failures Reads patch bytes directly instead of metadata first N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Upstream reads patch bytes directly; preserve fork path-bearing diagnostic with accurate operation.
src/js/node/tls.ts (1) Direct tls.connect sends only explicit SNI Can derive missing SNI from host/context Node 24.21.0: 34 direct TLS/HTTPS/HTTP2 SNI scenarios pass; direct absent SNI stays absent. Node 24 SNI oracle: direct TLS uses explicit servername only; HTTPS and HTTP/2 keep their separate name selection.
packages/bun-usockets/src/crypto/openssl.c (1) Empty CA means empty trust; per-VM roots; live context replacement Shared client verification and replacing SNI entries Node 24.21.0 CA/requestCert oracle 4/4: empty CA rejects, valid CA succeeds, absent client cert rejected when requested. Preserve explicit empty CA trust store and fork verify policy; absent and empty are distinct.
packages/bun-usockets/src/crypto/openssl.c (2) Empty CA means empty trust; per-VM roots; live context replacement Shared client verification and replacing SNI entries Node 24.21.0 CA/requestCert oracle 4/4: empty CA rejects, valid CA succeeds, absent client cert rejected when requested. Preserve per-VM default CA variant and server requestCert verification.
packages/bun-usockets/src/crypto/openssl.c (3) Empty CA means empty trust; per-VM roots; live context replacement Shared client verification and replacing SNI entries Node 24.21.0 CA/requestCert oracle 4/4: empty CA rejects, valid CA succeeds, absent client cert rejected when requested. Use upstream shared client verification helper; carry per-VM CA selection into its implementation.
packages/bun-usockets/src/crypto/openssl.c (4) Empty CA means empty trust; per-VM roots; live context replacement Shared client verification and replacing SNI entries Node 24.21.0 CA/requestCert oracle 4/4: empty CA rejects, valid CA succeeds, absent client cert rejected when requested. Use upstream last-name replacement with old-entry release; adapt fork live-context caller to upstream equivalent setter.
packages/bun-usockets/src/quic.c (1) CA store selection includes fork variant Adds signature/certificate verification declarations N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Keep upstream verification helpers and fork CA selection; remove duplicate no-argument CA declaration because root_certs_header.h owns the per-VM signature.
src/runtime/socket/SSLConfig.rs (1) Per-VM CA and request-context isolation Default ciphers and server verification policy Same Node CA/requestCert oracle 4/4; per-VM root isolation still needs merged Bun proof. Retain per-VM CA isolation/custom request contexts while adding upstream default cipher and server verification policy.
src/runtime/socket/SSLConfig.rs (2) Per-VM CA and request-context isolation Default ciphers and server verification policy Same Node CA/requestCert oracle 4/4; per-VM root isolation still needs merged Bun proof. Retain per-VM CA isolation/custom request contexts while adding upstream default cipher and server verification policy.
src/runtime/socket/UpgradedDuplex.rs (1) Forceful destruction does not call transport end Adds pause ownership, backpressure and final-write guards Node 24.21.0: four forceful-destroy cases pass without transport end access; HTTP2 upgrade oracle 26/26. Keep upstream backpressure pause state.
src/runtime/socket/UpgradedDuplex.rs (2) Forceful destruction does not call transport end Adds pause ownership, backpressure and final-write guards Node 24.21.0: four forceful-destroy cases pass without transport end access; HTTP2 upgrade oracle 26/26. Node 24 destroy oracle requires no end() during close; retain pause cleanup.
src/runtime/socket/UpgradedDuplex.rs (3) Forceful destruction does not call transport end Adds pause ownership, backpressure and final-write guards Node 24.21.0: four forceful-destroy cases pass without transport end access; HTTP2 upgrade oracle 26/26. Use upstream pause/resume helpers and msg_more-aware write/end API.
src/runtime/socket/UpgradedDuplex.rs (4) Forceful destruction does not call transport end Adds pause ownership, backpressure and final-write guards Node 24.21.0: four forceful-destroy cases pass without transport end access; HTTP2 upgrade oracle 26/26. Use teardown guards only for local write shutdown, preserving response bytes after peer TLS/transport EOF. Forceful close still never invokes end. The Node/fork control passes and pre-fix candidate fails; fixed focused regression passes.
src/runtime/socket/UpgradedDuplex.rs (5) Forceful destruction does not call transport end Adds pause ownership, backpressure and final-write guards Node 24.21.0: four forceful-destroy cases pass without transport end access; HTTP2 upgrade oracle 26/26. Initialize upstream backpressure pause state.
src/uws/lib.rs (1) Peer-half-close and single handshake notification ownership Shared verification helper and changed close sequencing N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Share upstream native client verification helper; per-VM CA is preserved in the C helper.
src/uws/lib.rs (2) Peer-half-close and single handshake notification ownership Shared verification helper and changed close sequencing N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Retain fork peer-close notification and readable half-open behavior for owners with on_end.
src/uws/lib.rs (3) Peer-half-close and single handshake notification ownership Shared verification helper and changed close sequencing N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Preserve upstream fatal-alert flushing and error-reason delivery after the data callback; retain fork peer-half-close path for ZERO_RETURN.
src/uws/lib.rs (4) Peer-half-close and single handshake notification ownership Shared verification helper and changed close sequencing N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Use shared helper extern; old per-SSL root selection duplication is removed.
src/runtime/jsc_hooks.rs (1) Default context keyed by per-VM CA choice Default context also respects configured cipher list N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Use upstream cipher-aware cache lookup and preserve fork per-VM CA key in both branches.
src/runtime/jsc_hooks.rs (2) Default context keyed by per-VM CA choice Default context also respects configured cipher list N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Use upstream cipher-aware cache lookup and preserve fork per-VM CA key in both branches.
src/js/node/https.ts (1) Live HTTPS setSecureContext ALPN conversion, BlockList and TLS identity Retained HTTP/TLS suites exercised; exact results are recorded above. Keep fork live setSecureContext and upstream ALPN/BlockList/HTTPS identity handling.
src/js/node/https.ts (2) Live HTTPS setSecureContext ALPN conversion, BlockList and TLS identity Retained HTTP/TLS suites exercised; exact results are recorded above. Keep fork live setSecureContext and upstream ALPN/BlockList/HTTPS identity handling.
src/js/node/https.ts (3) Live HTTPS setSecureContext ALPN conversion, BlockList and TLS identity Retained HTTP/TLS suites exercised; exact results are recorded above. Keep fork live setSecureContext and upstream ALPN/BlockList/HTTPS identity handling.
src/js/node/_http_server.ts (1) Listen errors, half-open, partial chain, ALPN and live TLS settings IP helper, BlockList and normalized TLS defaults Retained HTTP/TLS suites exercised; exact results are recorded above. Retain fork immediate callback and upstream IP helper.
src/js/node/_http_server.ts (2) Listen errors, half-open, partial chain, ALPN and live TLS settings IP helper, BlockList and normalized TLS defaults Retained HTTP/TLS suites exercised; exact results are recorded above. Preserve fork listen-error normalization; upstream certificate request semantics replace old comment.
src/js/node/_http_server.ts (3) Listen errors, half-open, partial chain, ALPN and live TLS settings IP helper, BlockList and normalized TLS defaults Retained HTTP/TLS suites exercised; exact results are recorded above. Preserve fork half-open option and upstream BlockList.
src/js/node/_http_server.ts (4) Listen errors, half-open, partial chain, ALPN and live TLS settings IP helper, BlockList and normalized TLS defaults Retained HTTP/TLS suites exercised; exact results are recorded above. Use upstream normalized TLS defaults; retain fork partial-chain setting.
src/js/node/_http_server.ts (5) Listen errors, half-open, partial chain, ALPN and live TLS settings IP helper, BlockList and normalized TLS defaults Retained HTTP/TLS suites exercised; exact results are recorded above. Use upstream stripped cipher list and preserve fork ALPN forwarding.
src/js/node/_http_server.ts (6) Listen errors, half-open, partial chain, ALPN and live TLS settings IP helper, BlockList and normalized TLS defaults Retained HTTP/TLS suites exercised; exact results are recorded above. Retain fork SNI/handshake-timeout state for live HTTPS context updates.
src/js/node/net.ts (1) Completion waits, cancellation, forceful destruction and adopted transport tracking Centralized attach helpers and current-handle selection Node 24.21.0: forceful-destroy oracle 4/4, HTTP2 upgrade 26/26; final native and affected-network outcomes are recorded above. Combine upstream current-handle selection with fork wait-for-transport writable completion.
src/js/node/net.ts (2) Completion waits, cancellation, forceful destruction and adopted transport tracking Centralized attach helpers and current-handle selection Node 24.21.0: forceful-destroy oracle 4/4, HTTP2 upgrade 26/26; final native and affected-network outcomes are recorded above. Use upstream centralized attach helpers; preserve fork ownership tracking in those helpers.
src/js/node/net.ts (3) Completion waits, cancellation, forceful destruction and adopted transport tracking Centralized attach helpers and current-handle selection Node 24.21.0: forceful-destroy oracle 4/4, HTTP2 upgrade 26/26; final native and affected-network outcomes are recorded above. Retain in-flight write cancellation and forceful transport destruction.
src/js/node/net.ts (4) Completion waits, cancellation, forceful destruction and adopted transport tracking Centralized attach helpers and current-handle selection Node 24.21.0: forceful-destroy oracle 4/4, HTTP2 upgrade 26/26; final native and affected-network outcomes are recorded above. Pass current socket owner to reconciled final callback.
src/js/node/net.ts (5) Completion waits, cancellation, forceful destruction and adopted transport tracking Centralized attach helpers and current-handle selection Node 24.21.0: forceful-destroy oracle 4/4, HTTP2 upgrade 26/26; final native and affected-network outcomes are recorded above. Use upstream server attach helper including named pipes/connecting sockets; preserve fork read-start behavior in the helper.
test/cli/install/bun-install-lifecycle-scripts.test.ts (1) Existing fork regression assertions and fixtures Additional upstream assertions, fixture imports or test harness changes Retained HTTP/TLS suites exercised; exact results are recorded above. Keep fork secure node-shim tests and upstream lifecycle/.env/EXDEV regressions; merge imports without duplicates.
test/cli/install/bun-install-lifecycle-scripts.test.ts (2) Existing fork regression assertions and fixtures Additional upstream assertions, fixture imports or test harness changes Retained HTTP/TLS suites exercised; exact results are recorded above. Keep fork secure node-shim tests and upstream lifecycle/.env/EXDEV regressions; merge imports without duplicates.
test/cli/install/bun-install-lifecycle-scripts.test.ts (3) Existing fork regression assertions and fixtures Additional upstream assertions, fixture imports or test harness changes Retained HTTP/TLS suites exercised; exact results are recorded above. Keep fork secure node-shim tests and upstream lifecycle/.env/EXDEV regressions; merge imports without duplicates.
test/cli/install/bun-install-patch.test.ts (1) Existing fork regression assertions and fixtures Additional upstream assertions, fixture imports or test harness changes Retained HTTP/TLS suites exercised; exact results are recorded above. Keep fork ELOOP/nonempty-error regression; operation now reads directly, retain upstream concurrency for separate patch fixtures.
test/cli/install/bun-run.test.ts (1) Existing fork regression assertions and fixtures Additional upstream assertions, fixture imports or test harness changes Retained HTTP/TLS suites exercised; exact results are recorded above. Keep fork stat assertions and upstream Windows cross-drive helpers.
test/cli/run/run-autoinstall-abs-path.test.ts (1) Existing fork regression assertions and fixtures Additional upstream assertions, fixture imports or test harness changes Retained HTTP/TLS suites exercised; exact results are recorded above. Keep upstream local registry and fork explicit auto-install opt-in contract.
test/cli/run/run-autoinstall.test.ts (1) Existing fork regression assertions and fixtures Additional upstream assertions, fixture imports or test harness changes Retained HTTP/TLS suites exercised; exact results are recorded above. Keep fork default-off auto-install expectations and tests; add all upstream offline registry helpers.
test/cli/run/run-autoinstall.test.ts (2) Existing fork regression assertions and fixtures Additional upstream assertions, fixture imports or test harness changes Retained HTTP/TLS suites exercised; exact results are recorded above. Keep fork default-off auto-install expectations and tests; add all upstream offline registry helpers.
test/js/bun/http/bun-serve-ssl.test.ts (1) Existing fork regression assertions and fixtures Additional upstream assertions, fixture imports or test harness changes Retained HTTP/TLS suites exercised; exact results are recorded above. Upstream import set is a superset of fork imports; both test sets retained.
test/js/bun/resolve/esModule.test.ts (1) Existing fork regression assertions and fixtures Additional upstream assertions, fixture imports or test harness changes Retained HTTP/TLS suites exercised; exact results are recorded above. Retain fork Node-compatible immutable ESM namespace and require facade tests; upstream writable marker expectations contradict this named fork contract.
test/js/first_party/ws/ws.test.ts (1) Existing fork regression assertions and fixtures Additional upstream assertions, fixture imports or test harness changes Retained HTTP/TLS suites exercised; exact results are recorded above. Keep fork HTTPS/TLS request socket regressions and upstream proxy/TLS tests.
test/js/node/fs/cp.test.ts (1) Existing fork regression assertions and fixtures Additional upstream assertions, fixture imports or test harness changes Retained HTTP/TLS suites exercised; exact results are recorded above. Keep fork macOS copy regression and upstream DirectoryTree typing.
test/js/node/http/node-http.test.ts (1) Existing fork regression assertions and fixtures Additional upstream assertions, fixture imports or test harness changes Retained HTTP/TLS suites exercised; exact results are recorded above. Keep fork Node-compatible server.address() listen result and callback cleanup; upstream changes are type-only around Bun-specific listen arguments.
test/js/node/http/node-http.test.ts (2) Existing fork regression assertions and fixtures Additional upstream assertions, fixture imports or test harness changes Retained HTTP/TLS suites exercised; exact results are recorded above. Keep fork Node-compatible server.address() listen result and callback cleanup; upstream changes are type-only around Bun-specific listen arguments.
test/js/node/http2/node-http2-upgrade.test.mts (1) Existing fork regression assertions and fixtures Additional upstream assertions, fixture imports or test harness changes Retained HTTP/TLS suites exercised; exact results are recorded above. Keep fork session-destroy/raw-close ordering regression and all upstream transport/handshake/fatal-error tests.
test/js/node/process/process.test.js (1) Existing fork regression assertions and fixtures Additional upstream assertions, fixture imports or test harness changes Retained HTTP/TLS suites exercised; exact results are recorded above. Keep fork file-URL CLI tests and upstream root-user gating.
test/js/node/tls/node-tls-connect.test.ts (1) Existing fork regression assertions and fixtures Additional upstream assertions, fixture imports or test harness changes Retained HTTP/TLS suites exercised; exact results are recorded above. Keep fork half-close regression and upstream backpressure/first-record tests.
test/js/node/tls/node-tls-connect.test.ts (2) Existing fork regression assertions and fixtures Additional upstream assertions, fixture imports or test harness changes Retained HTTP/TLS suites exercised; exact results are recorded above. Keep fork half-close regression and upstream backpressure/first-record tests.
test/js/node/tls/node-tls-server.test.ts (1) Existing fork regression assertions and fixtures Additional upstream assertions, fixture imports or test harness changes Retained HTTP/TLS suites exercised; exact results are recorded above. Use upstream connection test with explicit accepted/error accounting; fork wait-for-server acceptance retained by stronger upstream structure.
test/js/node/tls/node-tls-server.test.ts (2) Existing fork regression assertions and fixtures Additional upstream assertions, fixture imports or test harness changes Retained HTTP/TLS suites exercised; exact results are recorded above. Use upstream connection test with explicit accepted/error accounting; fork wait-for-server acceptance retained by stronger upstream structure.
test/js/node/tls/node-tls-upgrade.test.ts (1) Existing fork regression assertions and fixtures Additional upstream assertions, fixture imports or test harness changes Retained HTTP/TLS suites exercised; exact results are recorded above. Keep fork paused transport ownership fixtures and all new upstream takeover/parent/ref/buffered handshake cases.
test/js/node/tls/test-use-system-ca.test.ts (1) Existing fork regression assertions and fixtures Additional upstream assertions, fixture imports or test harness changes Retained HTTP/TLS suites exercised; exact results are recorded above. Keep fork worker-specific CA cases and upstream certificate-source tests.
test/js/node/worker_threads/worker_threads.test.ts (1) Existing fork regression assertions and fixtures Additional upstream assertions, fixture imports or test harness changes Retained HTTP/TLS suites exercised; exact results are recorded above. Retain fork worker preloads/setup and upstream TLS environment helper.
test/js/web/fetch/fetch-leak.test.ts (1) Existing fork regression assertions and fixtures Additional upstream assertions, fixture imports or test harness changes Retained HTTP/TLS suites exercised; exact results are recorded above. Keep fork bounded isolated object-count leak regression; upstream annotation is part of the obsolete recursive attack body.
test/js/web/workers/structured-clone.test.ts (1) Existing fork regression assertions and fixtures Additional upstream assertions, fixture imports or test harness changes Retained HTTP/TLS suites exercised; exact results are recorded above. Keep fork v8 serialization/Node worker regressions and upstream reader typing.
scripts/build/deps/webkit.ts (1) Checksum manifest and current fork engine Direct download of newer upstream engine N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Retain the checksum-enforced published manifest. Qualify replayed engine f34b5797f9d404443df87470831926f0d4786356 through a build-time local-WebKit override; update the pin only after autobuild publication.
scripts/build/deps/webkit.ts (2) Checksum manifest and current fork engine Direct download of newer upstream engine N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Retain the checksum-enforced published manifest. Qualify replayed engine f34b5797f9d404443df87470831926f0d4786356 through a build-time local-WebKit override; update the pin only after autobuild publication.
src/runtime/node/path.rs (all 5) UTF-16/lone-surrogate and reentrant/replaced cwd behavior Extracts old path algorithms to shared bun_node_path crate Retained HTTP/TLS suites exercised; exact results are recorded above. Preserve complete fork #163 UTF-16/unpaired-surrogate and reentrant cwd implementation. Upstream only extracts old algorithms into bun_node_path for lint/format; keep that new crate for its new consumers without regressing runtime callbacks. Runtime/shared deduplication remains follow-up, no runtime behavior dropped.
CLAUDE.md (modify/delete) Guidance moved to AGENTS.md Additional upstream guidance in deleted CLAUDE.md N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Retain fork AGENTS.md migration; apply upstream guidance to AGENTS.md
src/CLAUDE.md (modify/delete) Guidance moved to src/AGENTS.md Additional upstream guidance in deleted src/CLAUDE.md N/A to internal implementation; Linux x64 compilation passes; runtime outcomes are recorded in Validation status above. Retain fork AGENTS.md migration; apply upstream guidance to src/AGENTS.md
src/js/node/_http2_upgrade.ts (modify/delete) Deleted adapter; unified TLS path owns upgrade Modifies the older separate adapter Node 24.21.0 HTTP2 upgrade oracle: 26 pass; no skipped/todo. Retain fork deleted adapter; unified TLS path owns HTTP2 upgrade/teardown. Existing and upstream tests retained.

Complete upstream-changed qualification

Qualified source head: 327cc34df914be87d78fbb4c5fcf0f2024f82667. The complete frozen selection from CI run 38973008774 was executed on Linux x64: 1,689/1,689 selected files produced results. This completes the accepted broad comparison work. The final bounded PR CI gate is not green; its remaining failure is recorded above, and the PR remains draft.

The candidate was freshly built with the committed published WebKit archive. Controls used the published fork-main prerelease at 785be5fef0 and a separate upstream build at f2405c38. Every initially failing file ran against both controls; relevant Node 24/26 oracles also checked descriptor, namespace, path and stack-coordinate contracts.

Corrected first sweep: 1,645 pass / 44 fail. After the initial inherited-divergence markings: 1,671 pass / 18 fail. Final full sweep after the review corrections: 1,657 pass / 32 fail, with both dependency installs passing in every sweep. All three complete result sets are retained. All normal runner per-file/per-test limits and thresholds were retained, with zero retries. The intermediate full sweep took 119.47 seconds; the final full sweep took 145.11 seconds. The 22 marked test files passed focused validation; both review-corrected files also passed in full (300 ModuleGraph tests; 332 HTTP tests with one existing skip); the crashing fuzzer was explicitly included in the full proof despite its ordinary Linux quarantine. The qualification runner only retained full output and bypassed that one quarantine for this proof.

Final per-file classification Files Still reported failing in full rerun
No observed qualification failure 1,629 0
MERGE BUG 0 0
INHERITED DIVERGENCE 23 1 (fuzzer crash)
ENV / shared upstream-environment failure 12 12
FLAKE / INFRA 25 19
Total 1,689 32

Inherited divergences

Fork behavior is preserved. Original upstream assertions remain under expected-failure markers; no snapshots or expectations were rewritten to the observed output. The bundler helper supports executing exact expected-failing cases while retaining serial API execution. Review identified two overbroad markings; the correction separates only the HTTP frame-position, ModuleGraph namespace-key, and package-diagnostic expectations. HTTP payload/build/resolve checks and ModuleGraph dynamic import/disposal/package-exports checks remain ordinary passing tests.

File Preserved difference
test/bundler/bun-build-compile.test.ts Call-start frame columns in compiled line-start cases
test/bundler/bundler_bytecode_portable.test.ts Fork engine bytecode fingerprints differ from upstream goldens
test/bundler/bundler_compile_prelinked.test.ts Three exact splitting variants retain fork namespace/export behavior
test/bundler/bundler_edgecase.test.ts Retained external-require output and call-start sourcemap segments
test/bundler/bundler_npm.test.ts Retained sourcemap segments
test/bundler/cli.test.ts Sourcemap byte-count snapshots differ by five bytes
test/bundler/compile-argv.test.ts Quoted strict floor remains visible in worker execArgv
test/cli/hot/hot.test.ts Retained call-start columns across reload
test/cli/install/bun-run.test.ts Existing package parser rejects comments/trailing commas
test/cli/test/bun-test.test.ts Literal percent filename causes the inherited URIError
test/internal/macos-cross-config.test.ts Fork manifest-pinned WebKit cache layout
test/js/bun/http/bun-serve-html.test.ts Retained HTML sourcemap segments
test/js/bun/http/serve.test.ts Retained call-start frame in dev error-page JSON
test/js/bun/module-graph/module-graph-matrix.test.ts Retained call-start columns in two stack permutations
test/js/bun/module-graph/module-graph.test.ts module.exports namespace key and package-exports diagnostic
test/js/bun/test/stack.test.ts Retained call-start columns
test/js/bun/test/test-error-code-done-callback.test.ts Retained stack columns in printer snapshot
test/js/bun/util/fuzzy-wuzzy.test.ts Inherited Linux SIGSEGV; exact-file ordinary-run quarantine
test/js/bun/util/inspect-error.test.js Retained call-start stack columns in five snapshots
test/js/bun/util/reportError.test.ts Retained call-start printer column
test/regression/issue/08757.test.ts Preserved invoked symlink spelling in argv[1]
test/regression/issue/12548.test.ts Two runtime virtual-TypeScript plugin fixtures fail on main too
test/regression/issue/29169.test.ts Node-compatible data-style ppid descriptor

Environment and shared upstream failures

Files Evidence / reason
test/js/bun/s3/{s3-list-objects,s3.leak,s3}.test.ts, test/regression/issue/27272.test.ts (4) All runtimes require missing S3 credentials
test/js/third_party/{postgres/postgres,prisma/prisma}.test.ts (2) All runtimes require missing live Postgres credentials
test/cli/run/{glob-on-fuse,run-file-on-fuse}.test.ts (2) FUSE Python dependency unavailable; upstream reproduces
test/integration/bun-types/fixture/serve-types.test.ts Custom IPv4 fixture is not assigned on the host; upstream EADDRNOTAVAIL
test/js/bun/symbols.test.ts Both locally built runtimes fail the glibc symbol-floor assertion; published main passes
test/js/third_party/grpc-js/test-resolver.test.ts Same localhost DNS expectations fail upstream in this environment
test/cli/install/bun-install-link-path.test.ts Same 0755-versus-0775 file-mode assertion fails upstream

Flakes and infrastructure observations

Every file below passed three subsequent candidate diagnostic trials at unchanged limits. Original failures remain in the full-run totals; diagnostic passes are not substituted for them. No intermittent test was skipped or marked expected-failing.

File Observation
test/bake/dev-and-prod.test.ts Development-server readiness / invalid-HTML fixture under load
test/bake/dev/react-spa.test.ts Readiness timeout; asynchronous crash remapping can also attribute the fuzzer crash to another file
test/cli/format/format.test.ts Special-character path fixture
test/cli/install/bun-install-git-deps.test.ts Concurrent git-dependency fixture timeouts
test/cli/install/bun-install-stalled-tls.test.ts Silent-server connection-count timing
test/js/bun/module-graph/module-graph-gc.test.ts One many-graphs liveness assertion; initial sweep and three isolated candidate trials pass
test/js/bun/spawn/spawn-maxbuf.test.ts Child exit versus kill-signal observation
test/js/bun/wasm/compile-rss.test.ts Idle compiler RSS assertion under full load
test/js/node/cluster.test.ts Worker disconnect exit 1 versus 0; isolated Node/main/upstream fixtures pass
test/js/valkey/reliability/error-handling.test.ts Shared Redis state; other file performs FLUSHALL
test/js/valkey/reliability/protocol-handling.test.ts Shared Redis state; other file performs FLUSHALL

The final fresh-host sweep exposed fourteen additional intermittent/infrastructure files. Each was run against main and upstream, then passed three candidate diagnostic trials at unchanged limits. Controls can fail different fork-specific assertions: those outcomes are retained and are not conflated with the candidate's original failure.

Additional file Full-sweep observation
test/cli/lint/js-plugins.test.ts Three 10-second fixture timeouts under load
test/cli/run/filter-workspace.test.ts Expected interleaving absent from parallel child output
test/cli/install/bun-install-lifecycle-scripts.test.ts Concurrent lifecycle-script fixture timeout
test/js/bun/net/localhost-loopback-contract.test.ts Default/localhost connect timeout; explicit IPv4/IPv6 worked; isolated candidate and Node 26 pass
test/js/node/worker_threads/worker_destruction.test.ts Extra tracing-GC observation in worker-exit case; three candidate trials pass
test/js/bun/module-graph/module-graph-isolation.test.ts Worker/scrypt progress did not settle within the existing bound
test/napi/napi.test.ts Native addon outputs missing during concurrent fixture setup; three complete candidate trials pass, including its Node comparisons
test/napi/napi-value-ffi.test.ts Native fixture install/build failed during concurrent setup; three complete candidate trials pass
test/regression/issue/21311.test.ts Shared PostgreSQL server returned an interrupted-system-call file error
test/js/sql/postgres-bytea-bind.test.ts Same shared PostgreSQL file error
test/js/sql/sql-postgres-datetime-roundtrip.test.ts Child failed to connect to the shared PostgreSQL service
test/js/valkey/integration/complex-operations.test.ts Shared Redis keys disappeared during transactions/leaderboard/lock checks
test/js/valkey/unit/hash-operations.test.ts Shared Redis key disappeared between write and read
test/js/valkey/valkey.test.ts RANDOMKEY observed another test's key

The three PostgreSQL and three additional Redis files pass isolated main/upstream controls. Main's N-API control hits a different pre-existing finalizer timeout; upstream hits fork-specific native-lifetime assertions. Those are distinct from the missing-addon failure in the full candidate run. No native-lifetime or GC root cause is inferred from these observations.

Separate follow-ups are recorded for source-position expectations, fork bytecode goldens, prelinked namespaces, ModuleGraph package diagnostics, strict worker argv normalization, annotation percent paths, package-JSON syntax, virtual TypeScript loaders, symlink argv, ppid descriptor testing, WebKit cache layout, the fuzzer crash, and intermittents.

Evidence includes per-file JSON and full output, exact selections and commands, release checksums, build/source identities, three-trial diagnostics, and Node controls. Final proof binary SHA256: 23f673f79713d05ea57a9cef253b45afe61d2cb0c0a7166409fa0f62d0e3b801 (built at 639f1fed6; the final correction changes only tests). The earlier proof binary SHA256 is c10c3be94e274322d98d627775713b9db2ca4e9979b72af7a003ed97e0ebcfc3. Runtime/engine sources are unchanged by the qualification commit. The initial semantic markings review was scoped-clean at P2. The exact-commit review found the two coverage gaps described above; both were fixed, and the correction is Codex P2 scoped-clean with no remaining accepted/actionable findings.

An earlier diagnostic sweep used CI=1, which does not select this runner's CI mode. Those diagnostics are retained separately and excluded from qualification counts. All three valid complete qualification sweeps use CI=true and the Docker coordinator. Full qualification changed only tests and expectations; the subsequent main merge supplies the separately reviewed selector and workflow guards. No deadline, service credential, runtime implementation or engine pin changed during finalization. No ready marking or merge was performed.

Jarred-Sumner and others added 30 commits October 5, 2026 11:08
Fixes oven-sh#34069. Fixes oven-sh#37968.

- Bun.spawnSync no longer swaps vm.event_loop_handle on POSIX. What a GC finalizes during the call finds the thread's loop; what the call makes for its child names spawnSync's loop through a SpawnSync EventLoopCtx.
- A child nobody is going to wait for is ended and reaped (Process::kill_and_reap, from on_wait_pid and watch_or_reap): Bun.spawn, Bun.spawnSync, bun run --parallel/--filter, install scripts, the shell.
- Bun.spawn reports an unwatched exit before it throws, so onExit no longer takes the pending exception with it.
- What a direct stream's pull() throws is a failure whether or not it is an Error: Bun.write no longer stays pending, Bun.spawn no longer leaves its child.
- spawn_maybe_sync uses RAII types instead of scopeguard.
Upgrades the WebKit fork to upstream WebKit `dbdca7545d` (2026-10-05):
1020 upstream commits since `7b485a76e9` (oven-sh#43882), 188 of them in
JavaScriptCore, WTF or bmalloc. Fork PR: oven-sh/WebKit#773.

### Pin

- `WEBKIT_VERSION` points at the preview build of the fork PR
(`autobuild-preview-pr-773-5dbf444e`). **Before this merges, bump it to
the `autobuild-<sha>` of the fork's merge commit**, once
oven-sh/WebKit#773 has landed.

### What changed in Bun

- `WTF::CString` is a class template upstream now, and `data()` of a
UTF-8 one is a `const char8_t*`. Declarations name `UTF8CString` (or
`Latin1CString`), and calls into C APIs (`execve`, `posix_spawn`,
`dlopen`, `dlsym`, `GetProcAddress`, libsecret, CoreFoundation, the
Windows credential API, sqlite, ncrypto) take `legacyCStringPointer()`.
Files: the Secrets backends, `BunProcess.cpp`, `BunString.h`,
`NodeTLS.cpp`, the sqlite bindings, `JSBundlerPlugin.cpp`,
`JSX509CertificatePrototype.cpp`, `CryptoGenEcKeyPair.cpp`, the V8 CPU
profiler shim, `JSCompressionStreamShared.cpp`, `ChromeBackend.cpp`,
`ObjCRuntime.h`.
- `JSONRowsToJS.cpp` uses `VM::jsonCache()` (`JSONAtomStringCache` is
gone). The fork keeps `tryMakeJSString()` there.
- `Zig::GlobalObject::visitChildrenImpl` takes the VM from the cell: a
slot visitor has no `vm()` any more.
- `makeDOMAttributeGetterTypeErrorMessage` takes a `StringView`;
`Yarr::FlagsString` has `span()` only (`SerializedScriptValue.cpp`);
`RemoteInspectorServer::start` takes a `UTF8CStringView`;
`StringView::fromLatin1(span)` is gone (`ChromeBackend.cpp`).
- Nothing changes for `JSC__VM__collectAsyncIdle` and the four-argument
microtasks: the fork keeps `GCRequest::didFinishEndPhase` and
`maxMicrotaskArguments = 4`, which upstream removed or does not have.
- `src/jsc/JSType.rs` needs no change: `JSType.h` is the same.
- `bundler_bytecode_portable.test.ts` has a new snapshot, because the
bytecode cache format changed. Caches written by an older Bun are
rejected and rebuilt, and `--compile --bytecode` executables must be
rebuilt.
- The macOS and Windows parts of these edits (`SecretsDarwin.cpp`,
`SecretsWindows.cpp`, `ObjCRuntime.h`, the `posix_spawn` and
`GetProcAddress` calls) were made by reading the code. They have not
been compiled: only Linux x64 was built locally.

### How was it verified

Debug + ASAN build against the local WebKit tree, Linux x64.

- `test/js/bun/jsc/webkit-upgrade-dbdca7545d.test.ts` (new, 9 cases):
passes. On Bun 1.3.13, 8 of the 9 fail; the ninth checks that for-of and
destructuring still close an Array or String iteration, which did not
change.
- These pass: `webkit-upgrade-7b485a76e9`, `webkit-upgrade-ccdcb8a026`,
`bun-jsc`, `capture-stack-trace`, `AsyncLocalStorage`,
`async-local-storage-thenable`, `jsonl-parse`, `json5`,
`xml-test-suite`, `ffi`, `cc`, `sqlite`, `node-sqlite`,
`process-execve`, `compression`, `x509`, `gc-controller-cadence`,
`error-stack-finalizer-exception`, `bundler_compile_prelinked`, `vm`,
`workers/structured-clone`, `process-nexttick`, `microtask`,
`inspect-error`, `test/stack`. Five of them (`AsyncLocalStorage`,
`json5`, `ffi`, `gc-controller-cadence`, the encoder cases of
`bundler_bytecode_portable`) need more than the default 5 s per test on
this debug build and were run with a longer timeout.
- `bundler_bytecode_portable.test.ts`: the two encoder cases pass with
the new snapshot. Its `--compile` cases were not completed locally (the
machine ran out of temporary disk space for the debug executables), so
CI is the first full run of that file.
- `capture-stack-trace.test.js` caught a mistake in the merge (a stale
flag after `Error.captureStackTrace` replaced a stack that a collection
had formatted). It is fixed in the fork PR.
- With the pin in place, a debug build against the prebuilt
`autobuild-preview-pr-773-5dbf444e` (`bun bd`) builds, and the new test
and `capture-stack-trace.test.js` pass on it.
- The fork's CI for oven-sh/WebKit#773 is green on every lane, including
its JavaScriptCore test run on Linux x64 and arm64.
- The full Bun test suite has not been run locally.

Upstream WebKit `7b485a76e9c..dbdca7545da` — 188 non-merge commits
touching `Source/JavaScriptCore`, `Source/WTF` or `Source/bmalloc`,
committed 2026-09-23 through 2026-10-05 (36 of them are WebCore/WebKit
changes that only touch `UnifiedWebPreferences.yaml`, `Platform*.h` or
similar in WTF).

### Notes for Bun

Bun usage below was found by grepping `src/jsc/bindings` and
`src/jsc/JSType.rs`. "What changed in Bun" above says what was done
about each item.

**`JSType.h`**

- `Source/JavaScriptCore/runtime/JSType.h` is unchanged in this range
(empty diff). No enum values added, removed or reordered, so
`src/jsc/JSType.rs` needs no change.

**API changes that Bun's bindings use**

- `WTF::CString` is now a class template. The untyped class was renamed
`CStringBase` with protected constructors, assignment, destructor and
`data()`; `CStringWithEncoding<T>` was renamed `CString<T>`
(`UTF8CString`, `Latin1CString`, `ASCIICString` aliases are unchanged).
`data()` on the typed strings returns `const char8_t*` / `const
Latin1Character*` / `const char*`; `legacyCStringPointer()` remains on
`UTF8CString` only. `CStringHash` / `DefaultHash` / `HashTraits` are now
templates over the typed string. Bun uses bare `CString` /
`WTF::CString` as a concrete type (locals without initializer,
parameters, members, `Vector<CString>`, `std::variant<WTF::CString, …>`)
and calls `.data()` on the sliced value in 19 files: `Secrets.h`,
`SecretsDarwin.cpp`, `SecretsLinux.cpp`, `SecretsWindows.cpp`,
`JSSecrets.cpp`, `BunProcess.cpp`, `BunString.h`, `bindings.cpp`,
`ZigGlobalObject.cpp`, `NodeTLS.cpp`, `JSX509CertificatePrototype.cpp`,
`JSBundlerPlugin.cpp`, `webcrypto/SubtleCrypto.cpp`,
`node/crypto/CryptoGenEcKeyPair.cpp`, `v8/shim/CpuProfiler.h`,
`sqlite/JSSQLStatement.cpp`, `sqlite/NodeSqlite.cpp`,
`webcore/streams/BunStreamConsumers.cpp`,
`webcore/streams/JSCompressionStreamShared.cpp`. `22e1cc5592e`
`f2ca04538ea` `4cd0b19673f` `2c5cd6d774d` `8a38069dceb` `165f9ed4d01`
- `AbstractSlotVisitor::vm()` and `heap()` were removed; visitors now
hold a `Collector&` (`visitor.collector().heap()`), plus
`collectionScope()` and `heapAnalyzer()`. Used at
`ZigGlobalObject.cpp:3301` (`WebCore::clientData(visitor.vm())`).
`101392f63ca`
- `GCRequest::didFinishEndPhase` was removed (upstream had no user), and
`GCRequest::subsumedBy()` no longer treats a request with a callback as
never subsumed. `JSC__VM__collectAsyncIdle` in `bindings.cpp:3184-3198`
sets `request.didFinishEndPhase` and relies on that non-coalescing rule
(see also `BunClientData.h:262`). `a5162492689`
- `JSONAtomStringCache.h` / `JSONAtomStringCacheInlines.h` were deleted
and `VM::jsonAtomStringCache` removed; the replacement is
`VM::jsonCache()` (`JSONCache.h`, `JSONCacheInlines.h`), whose
`makeIdentifier` / `makeJSString` take `VM&` as first argument.
`JSONRowsToJS.cpp` includes the deleted header and calls
`vm.jsonAtomStringCache.makeIdentifier(...)` and `tryMakeJSString(...)`
(the latter exists only in Bun's fork). `f62059d5478`
- `JSC::Yarr::FlagsString` changed from `std::array<char, N>` to a class
exposing only `span()` (`std::span<const Latin1Character>`).
`webcore/SerializedScriptValue.cpp:1188` calls
`flagsString(...).data()`. `4356bc5514b`
- `makeDOMAttributeGetterTypeErrorMessage` /
`makeDOMAttributeSetterTypeErrorMessage` take `StringView interfaceName`
instead of `const char*`; `StringView(const char*)` is private.
`ZigGlobalObject.cpp:1872` passes `utf8().legacyCStringPointer()`.
`JSDOMExceptionHandling.cpp:267` passes an `ASCIILiteral`, which
converts. `0f3178a96a1`
- `runInternalMicrotask` now takes `uint8_t payload, JSValue argument0,
argument1, argument2` by value instead of a `std::span<const JSValue,
maxMicrotaskArguments>`, and `JSGlobalObject::queueMicrotask` writes the
`QueuedTask` straight into the deque on the fast path. Bun's bindings do
not call `runInternalMicrotask`, but they build 4-argument `QueuedTask`s
(`bindings.cpp:4020`, `bindings.cpp:5871`,
`webcore/streams/JSStreamPipeToOperation.cpp:120`,
`webcore/streams/WritableStreamOperations.cpp:76`) and the fork sets
`maxMicrotaskArguments = 4`; upstream's new signature carries three.
`53d4af93f61`

**API changes checked and not used by Bun's bindings**

- `Heap::lastChanceToFinalize()` is private; `VM::~VM` calls the new
`Heap::shutDown()`. Bun only mentions it in comments. `1d03118dfe7`
- Moved from `Heap` to the new `Collector` class (`heap/Collector.h`,
reachable via `Heap::collector()`): `runTaskInParallel`,
`runFunctionInParallel`, `forEachSlotVisitor`, the request queue, phase
machine, marking constraint set, collector thread.
`Heap::phaseVersion()`, `increaseLastFullGCLength()` removed;
`didDiscoverPendingWasmCallee` renamed `markWasmCalleeIfPending`;
`collectorSlotVisitor()` and `isInPhase()` are now defined in
`HeapInlines.h`. `Heap::collectAsync/collectSync/collectNow(GCRequest)`
and `Heap::addMarkingConstraint(std::unique_ptr<MarkingConstraint>)`
keep their signatures. `SlotVisitor` is now `alignas(128)` and
constructed from a `Collector&`. `c2fa1927604` `5e53548c666`
`5297538c7da`
- `SourceProvider::codeBlockHashConcurrently` is no longer virtual (and
is `const`); the new virtual is `withSourceConcurrently(const
ScopedLambda<void(StringView)>&) const`, whose default calls `source()`.
`documentLineColumnForOffset()` (used by `ErrorStackFrame.cpp:69`) keeps
its signature and 1-based result but now goes through
`withSourceConcurrently`. `positionInfoForOffset()` /
`offsetForPosition()` assert they are not on a GC thread. No override of
`codeBlockHashConcurrently` found in Bun's bindings. `93ebfacfd7a`
- `ErrorInstance`: `setStackPropertyAlreadyMaterialized()` renamed
`setStackPropertyProvidedByCapturedStackTrace()`; new
`m_capturedStackTrace`, `m_hasErrorInfo`,
`trySaveCapturedStackTraceForLazyMaterialization()`. The methods Bun
calls that exist upstream (`stackTrace()`,
`materializeErrorInfoIfNeeded()`, `setNativeGetterTypeError()`,
`isStackOverflowError()`) are unchanged. `696c406fe52`
- `JSObject::setStructure(VM&, Structure*)` is now defined in
`StructureCreateInlines.h` (included by `JSObjectInlines.h`) instead of
`JSObject.h`. `JSDOMWrapperCache.cpp:49` calls it and already reaches
`JSObjectInlines.h` through `root.h` → `JSCInlines.h`. `720bc3a7c71`
- `PropertySlot::setCustom(…, DOMAttributeAnnotation)` overload removed;
`JSObject::fillCustomGetterPropertySlot` lost its `Structure*`
parameter. `96fed6d5b49`
-
`RegExpObject::isSymbol{Match,Search,MatchAll,Replace,Split}FastAndNonObservable()`
now take a `JSGlobalObject*`; `constructArrayBufferWithSize()` lost its
`JSGlobalObject*` parameter. `bffe36ec16e` `94de22268a7`
- `VM::fastStringValuesSentinel()` renamed `fastStringSentinel()`; new
`VM::fastArraySentinel()`. `5330149c555` `35637c64339`
- `Debugger::forEachBreakpointLocation` takes `const Function&` instead
of `Function&&`; new virtual
`Debugger::isPauseBlockedByAnotherDebugger()` (default false),
`Debugger::hasProfilingClient(const ProfilingClient&)`,
`InspectorEnvironment::forEachDebugger()` (default visits `debugger()`).
`ba688904ad4` `7c0df54a062`
- `StructuredCloneTags.h` gains `QuotaExceededErrorTag = 69`. Bun's
`SerializedScriptValue.cpp` does not include this header. `21862acf2c3`
- C API helpers: `createJSString()` in `JSStringRefCPP.h` returns
`RefPtr<OpaqueJSString>` instead of `JSRetainPtr<JSStringRef>`; new
`API/JSStringRefPtr.h`. `71d5b30126a`
- WTF: `wtf/text/CStringView.h` renamed `wtf/text/UTF8CStringView.h`
(`CStringView` → `UTF8CStringView`) `df3a6b749ba`;
`StringView::fromLatin1(std::span<const Latin1Character>)` removed (the
`const char*` overload Bun uses remains) `3fb615cd242`;
`ParkingLot::UnparkResult::didUnparkThread` replaced by `unparkedCount`,
`unparkOneImpl` replaced by `unparkCountImpl`, and `unparkCount(address,
0)` now release-asserts `5d43ab597d9` `35ba3364d35`;
`ReadWriteLock::ReadLock/WriteLock` renamed `ReadLockView/WriteLockView`
`5d43ab597d9`; `GuaranteedSerialFunctionDispatcher` gains pure virtual
`dispatchAfter(Seconds, Function<void()>&&)`, and
`RunLoop::dispatchAfter` returning a `DispatchTimer` was renamed
`RunLoop::scheduleTimer` `155da279924`; `WTF::MemoryStatus`,
`memoryStatus()`, `isUnderMemoryPressure()` removed and
`percentAvailableMemoryInUse()` is now an exported function
`7cc8fd9a7f6`; `WTFLogChannelByName`,
`WTFInitializeLogChannelStatesFromString`, `FilePrintStream::open`,
`stringFromFileSystemRepresentation` take typed strings `0f3178a96a1`;
`wtf/SwiftCXXThunk.h` and `HAS_SWIFTCXX_THUNK` removed `edbf6068f30`;
`SWIFT_ESCAPABLE*` / `SWIFT_NONESCAPABLE` renamed
`SWIFT_SELF_CONTAINED*` / `SWIFT_VIEW` `46495d17ae3`.
- Many functor parameters in WTF and JSC were annotated `NOESCAPE` and
changed from `F&&` to `const F&` (for example
`StringView::find/contains`, `WTF::map`, `handleWithAdapters`,
`UUID::handle`, `JSBigInt::absoluteBitwiseOp`,
`MarkedVector::fillWith`). A `mutable` lambda passed to one of these no
longer binds. A grep for `mutable` lambdas passed directly to these in
Bun's bindings found none; this was not checked exhaustively.
`b0aeb546671` `01e2533964d`

**Overlap with Bun's fork patches**

- `git diff --name-only 7b485a76e9c HEAD` and `7b485a76e9c
upstream/main` share 241 files under these three directories. Files
where upstream's change is structural and the fork also carries patches:
`heap/Heap.{h,cpp}`, `heap/GCRequest.{h,cpp}`, `heap/SlotVisitor.h`,
`heap/AbstractSlotVisitor{,Inlines}.h`, `runtime/JSMicrotask.{h,cpp}`,
`runtime/MicrotaskQueueInlines.h`,
`runtime/JSONAtomStringCache{,Inlines}.h` (deleted upstream),
`runtime/LiteralParser.{h,cpp}`, `runtime/ErrorInstance.{h,cpp}`,
`runtime/ErrorConstructor.cpp`, `parser/SourceProvider.{h,cpp}`,
`runtime/VM.{h,cpp}`, `runtime/OptionsList.h`,
`bytecode/BytecodeList.rb`, `runtime/JSArrayIterator{,Inlines}.h`,
`yarr/YarrFlags.h`, `wtf/RunLoop.h`, `wtf/AvailableMemory.{h,cpp}`,
`wtf/text/StringView.h`, `bmalloc/bmalloc.{h,cpp}`. The fork PR lists
how each one was resolved.

**Build system**

- New JSC sources: `heap/Collector.cpp`, `runtime/JSONCache.cpp`
(`Sources.txt`). New JSC headers: `heap/Collector.h`,
`heap/CollectorInlines.h`, `runtime/JSONCache.h`,
`runtime/JSONCacheInlines.h`, `API/JSStringRefPtr.h`,
`corpse/CorpseMemory*.h`. Removed:
`runtime/JSONAtomStringCache{,Inlines}.h`,
`runtime/JSONTransitionCache{,Inlines}.h` (added and removed within the
range).
- New WTF headers: `LayeredHashMap.h`, `MonotonicObjectIdentifier.h`,
`posix/POSIXExtras.h`, `glib/GLibExtras.h` (GLib ports and JSCOnly with
GLib). Removed: `SwiftCXXThunk.h`. Renamed: `text/CStringView.{h,cpp}` →
`text/UTF8CStringView.{h,cpp}`.
- bmalloc `CMakeLists.txt`: `pas_small_medium_bootstrap_free_heap.{c,h}`
and `pas_small_medium_bootstrap_heap_page_provider.{c,h}` replaced by
`pas_mte_bootstrap_*`; `pas_allocation_mode.h`,
`pas_compact_bitfit_global_directory_ptr.h`,
`pas_compact_segregated_heap_ptr.h` removed. `c8cb35beaca` `5b0cfbc518e`
- Top-level `CMakeLists.txt` sets `CMAKE_PCH_PROLOGUE ""` and a
`WEBKIT_ENABLE_LANGUAGE` macro replaces direct `enable_language()`, so
generated `cmake_pch.hxx` no longer starts with `#pragma clang
system_header` and warnings in prefix headers are reported. The 2003-era
`NULL` redefinition was dropped from `JavaScriptCorePrefix.h`.
`12dd44cb41c`
- `WebKitCompilerFlags.cmake` adds `-Wno-unused-template` globally for
GCC/Clang. `75930ee50a6` `fce5f763755`
- JSC `CMakeLists.txt` passes `--defines-file
${WEBKIT_PLATFORM_FEATURE_DEFINES_FILE}` to
`generate-combined-inspector-json.py` when that variable is set.
`71b89186dc0`
- New CMake options: `GENERATE_DSYM` (Apple only;
`LLIntSettingsExtractor` / `LLIntOffsetsExtractor` marked `SKIP_DSYM`)
`5bd718a24ea`, `ENABLE_SWIFT_BASE_CLASS_ANNOTATIONS` (default from
detected Swift version, off when not found) `5f4afbfb124`,
`ENABLE_CONNECTED_VOLUMETRIC_SCENE` (off) `a216b77387a`,
`USE_EXTENSIONKIT` (off).
- `OptionsJSCOnly.cmake`, `OptionsCommon.cmake` and `WebKitCommon.cmake`
are unchanged. The remaining `Source/cmake` commits are Cocoa/GTK/WPE
packaging (Info.plists, entitlements, XPC services, OpenSSL crypto
backend option for GLib, Cog removal, symbol visibility for ObjC, `-O3`
for bmalloc in Cocoa Debug builds).
- `PlatformEnable.h`: adds `ENABLE_CONNECTED_VOLUMETRIC_SCENE` (default
0) and `ENABLE_SWIFT_BASE_CLASS_ANNOTATIONS` (Apple SDK 27+).
`PlatformHave.h`: removes `HAVE_WK_SECURE_CODING_NSURLREQUEST`, adds
`HAVE_UI_HINGE_INTERACTION`. No `ENABLE_`/`USE_` default relevant to
JSCOnly changed.

**Runtime options and defaults**

- `useWasmWideArithmetic` default changed from false to true (defined
via `jscOptionName` in `UnifiedWebPreferences.yaml`). `ba4ffde8464`
- `useWasmFastMemory` is no longer forced off on Windows in
`Options::notifyOptionsChanged()`. No reference to this option was found
in Bun's source. `653b77d5bdd`
- New: `useCachedAssemblerDataCapacityLimit` (default false, from
`UnifiedWebPreferences.yaml`) and `maximumCachedAssemblerDataCapacity`
(4 MB). `6e9eb5b8025` `edb44235bc7`
- New: `maxB3WasmGCEpochSnapshotEntries` (1048576). Removed:
`useB3ReduceStrengthFixpoint`. `33ca66ce3ff` `1ffb8e9a913`
- No reference to any of these option names was found in Bun's `src`,
`scripts` or `cmake` directories.

**Bytecode and bytecode cache**

- New opcode `op_iterator_close_check` (operands `iterator`, `next`,
`iterable`, `targetLabel`; metadata `seenModes`), inserted before the
metadata-less opcodes, so later opcode IDs shift. `35637c64339`
`5330149c555`
- `op_iterator_open` / `op_iterator_next` operand meaning changed in
`FastArray` and the new `FastString` iteration modes: `iterator` holds a
per-VM sentinel and `next` holds the Int32 index.
- `Reflect.construct(F, [a, b])` with a hole-free, spread-free array
literal now emits `op_construct` instead of `op_construct_varargs`.
`8ae122a8481`
- Upstream's `runtime/CachedTypes.cpp` is unchanged. Bytecode produced
by the previous WebKit is not valid for this one because of the opcode
changes: the fork's `cachedTypesFormatRevision` goes from 12 to 13, and
Bun keys its cache version on `BUN_WEBKIT_VERSION`
(`ZigGlobalObject.cpp:264-271`), which changes with the upgrade.

### New features / spec changes

- WebAssembly wide arithmetic enabled by default. `ba4ffde8464` (325219)
- `(a?.b)(x)` now parses as a call outside the optional chain: when `a`
is nullish the arguments are evaluated and a TypeError is thrown instead
of returning undefined. `(a?.b)() = 1` in sloppy mode becomes a runtime
ReferenceError instead of a SyntaxError. `63b7d629dd1` (325104)
- Tagged templates keep `this` for a parenthesized optional-chain tag
(`(o?.b)\`x\``) and use the current `this` for a `super.tag\`x\`` tag.
`7fbe85cfbfd` (325401) `24e354459de` (325756)
- `await` is accepted as an identifier in the body of a non-async
function nested in an async function's parameters. `09c4cfc7d73`
(325954)
- `catch (await)` is rejected in async functions, modules and static
blocks, including the escaped spelling. `edb17fbf37d` (325957)
- `new Array(len)` reads `newTarget.prototype` before throwing
RangeError for an invalid length; `new ArrayBuffer(len)` applies ToIndex
to length and `maxByteLength` before reading `newTarget.prototype`.
`e4e335cf8df` (325494) `1b1238d3e39` (325496)
- TypedArray construction from a Proxy array-like performs a real `Get`
of `length`; a module namespace export named `length` is not used as the
length. `71d329770b1` `240fe1fc636` (325491)
- `Intl.DurationFormat`: keeps the minus sign for fractions between -1
and 0, and keeps a sub-second fraction folded into a zero-valued unit.
`501d1f661f9` (325247) `ee8e5dbbcc2` (325238)
- `Intl.Segmenter` `containing()` returns the right segment when the
index is a lead surrogate. `3e27303e85c` (324036)
- Temporal: invalid calendar identifier errors use one message format
and ellipsize very long ids. `3141e1c6626` (325305)
- Wasm ESM integration: `wasm:` / `wasm-js:` reserved-name LinkErrors
skip string-constant imports and recognized builtins. `caa67084218`
(325162)

### Performance

**Bytecode, LLInt, Baseline**

- `for-of` and array destructuring over an Array no longer allocate a
`JSArrayIterator` in LLInt/Baseline/DFG; state is kept in registers.
`35637c64339` (324224)
- The same for Strings (`JSStringIterator`). `5330149c555` (325452)
- `op_iterator_next` FastArray path is inlined in Baseline and LLInt for
Int32/Contiguous arrays. `bb73af4a4bc` (325235) `39f7f217cda` (325774)
- LLInt gets an integer TypedArray fast path for `put_by_val`.
`41302976307` (326248)
- `Reflect.construct` with an array literal avoids allocating the array.
`8ae122a8481` (325232)

**DFG / FTL**

- `CheckStructure` with several structures compares recently seen
structures first. `ebb9bf5fa08` (326249)
- String equality with a rope operand compares lengths inline before
calling `operationCompareStringEq`. `bd3aa0eab70` (326312)
- `ValueSub` reads its arith profile in `makeSafe`, avoiding repeated
OSR exits on double results. `e3ff851cf04` (326000)
- `CompareEq(String, StringOrOther)` is converted to `CompareStrictEq`;
`MakeRope` accepts `StringOrOther` operands outside `String#concat`.
`4f03ef594d4` (325583) `db86e0961a4` (325751)
- BigInt comparisons against `0n` become pointer / sign-bit checks.
`73af864d11c` (325432)
- Int52: architecture-specific `tryConvertToInt52`, more precise Int52
overflow profiling, B3 SShr+Mul range optimization. `be0094718dc`
(325457)
- FTL OSR exits are stored as a byte stream (reported 56 B → about 12 B
per exit on JetStream 3), with their `ValueRep`s inline. `ee4792cb07c`
(324517) `fb512fb75f6` (325463)
- ICs flatten an uncacheable dictionary when they see a getter on it, as
they already did for data properties. `96fed6d5b49` (325245)

**B3 / Air / OMG**

- Value-numbering based CSE and WasmGC load forwarding in the first
`ReduceStrength` run, using a new `WTF::LayeredHashMap`; forward store
elimination and Phi synthesis for WasmGC reads were dropped.
`33ca66ce3ff` (324906) `8e2d66bfd16` (325885)
- Constant-condition branches are pruned when FTL/OMG generate B3;
`Switch` on a constant is folded. `c3148b98af6` (325097)
- `simplifyCFG` no longer iterates to a fixpoint; the fixpoint debugging
mode was removed. `33bc312ad4c` (325484) `1ffb8e9a913` (325459)
- `fixSSA` runs at every opt level so `Get`/`Set` never reach Air
lowering. `d9fb479bd09` (325079)
- OMG: no predecessor wiring in the frontend, no redundant address `Add`
for stack arguments/results; `BasicBlock::appendNewControlValue`
removed. `a8c7000ca69` `5e551b0a24e` `6edd3cc7294`

**Runtime**

- `JSON.parse`: single-transition and transition caches for object keys
(8-bit and 16-bit sources), an embedded name / prefixed-name cache
compared with SIMD, one-character token fast paths, more SIMD scanning
in string lexing, a long-string cache, a 64-entry cache for repeated
short string values, bulk copy when materializing arrays. The caches
live in a new heap-allocated `JSONCache`. `b718c65267c` `12ea13fb7c8`
`1e80c1e3efb` `0506f92aff1` `ec88ec5ecbe` `7fb452f1a4b` `e5517023f3e`
`f62059d5478` `26fbddd98c8` `07a51e24339` `615e180ef2c`
- `JSArray::fastSplice` for Int32/Contiguous/Double arrays.
`d29218b6deb`
- BigInt division uses Burnikel-Ziegler for divisors of 24+ digits and
quotients of 48+; the schoolbook leaf no longer heap-allocates.
`b071cfab287` (325444) `13a1f291693` (326174)
- `parseInt` avoids resolving rope arguments; `ToNumber` on a string
rejects non-numeric input by its first character. `1df0dac22df` (325765)
`bef9d7e6329` (325591)
- `Error.captureStackTrace()` on an `ErrorInstance` without an own
`stack` captures frames only and formats the string on first access.
`696c406fe52` (325578)
- Decoded `ExpressionInfo` entries are cached per bytecode offset (under
a lock); line start tables are built in one SIMD pass. `c59d70e267e`
(325559)
- Microtask queueing and draining avoid copying `QueuedTask` through the
stack; `clearExceptionExceptTermination()` returns early when no
exception is pending. `53d4af93f61` (326306) `346b6ce3d9f` (325968)
- `setPrototypeDirect` skips `haveABadTime()` when the old prototype
chain already intercepted indexed access. `9ef3e2eb29b` (325587)
- Per-thread `AssemblerBuffer` storage above
`maximumCachedAssemblerDataCapacity` can be freed instead of cached
(behind `useCachedAssemblerDataCapacityLimit`, default off; landed,
reverted, relanded). `6e9eb5b8025` `06d713139e1` `edb44235bc7` (325503)

**GC (refactoring, stated as no behavior change)**

- Collection-cycle state and machinery moved from `Heap` into a new
`Collector` class, in preparation for one collector marking several
heaps: marking machinery, phase machine, collector thread, request
queue, constraint set, setup/teardown, and the begin/end-of-collection
boundary. `c2fa1927604` `59be6c12449` `36d446cb4f9` `5e53548c666`
`1d03118dfe7` `648d26d0bfa` `152ae41b47b` `5297538c7da` `d3c92619498`
- `SlotVisitor` no longer holds a `Heap`; `visitChildren`
implementations and constraints take the VM/heap from the cell or
capture it. `6730df1f851` `101392f63ca`
- Renames: conservative scan "current thread" → conductor; Wasm callee
cleanup functions named for marking. `dfdac830e36` `868ef794075`

### Bug fixes

**JIT correctness**

- B3 `specializeSelect` cloned `Phi`s between the `Select` and the
`Check`, leaving a Phi slot unwritten and producing a garbage value in
exit state. `6dd325c7d06` (325312)
- DFG fixup cleared `NodeMustGenerate` on `ArithFloor/Ceil/Round/Trunc`
in Int32 rounding modes, so DCE could remove an operation that must exit
on overflow or negative zero. `f325c4f3adb` (325499)
- DFG constant folding skips CFA-unreachable blocks (debug assertion).
`64baa915314` (325835)
- DFG assertion on a `for-of` site that sees both an Array and a generic
iterator. `45d9af505ef` (325416)

**Cross-realm**

- Promise `then`/`catch`/`finally` fast paths and DFG inlining of
`Promise.prototype.then` / `Promise.resolve` / `Promise.reject` are no
longer taken for another realm's promise or function. `1f47117433c`
(325110)
- The same for String/RegExp fast paths (`match`, `matchAll`, `replace`,
`replaceAll`, `search`, `split`, `RegExp.prototype.test`). `bffe36ec16e`
(325134)
- DFG `ArrayBuffer` construction uses the realm of the baked-in
structure. `94de22268a7` (326133)

**Memory safety, races, platform**

- `ReadWriteLock`: the last reader could write to the lock after the
writer it drained for had acquired and freed it. `e71d94bd49e` (325611)
- Wasm `RTT` is allocated with the compact allocator, matching its use
in `CompactPointerTuple` (affects MTE-enabled configurations).
`03befd703d6` (323513)
- `fastMallocSize` handles allocations from the tagged bmalloc heap
under MTE. `88167d46efc` (325834)
- Line/column lookups (`SourceProvider::documentLineColumnForOffset`)
are safe off the main thread. `93ebfacfd7a` (324838)
- Windows: creating or growing a Wasm memory, resizable `ArrayBuffer` or
growable `SharedArrayBuffer` at the commit limit reports out of memory
instead of crashing. `987be10500c` (325406)
- Windows: the Wasm guard region is reserved rather than committed, and
Wasm fast memory is re-enabled. `653b77d5bdd` (304154)
- `AvailableMemory` on iOS reads the jetsam limit via `task_info`; the
previous sandboxed syscall made it always report 1024 MB. `7cc8fd9a7f6`
(325827)

**Debugger / inspector**

- `Debugger::isPauseBlockedByAnotherDebugger()` lets a subclass hold off
pausing; checked by `breakProgram()` and `didReachDebuggerStatement()`.
`ba688904ad4` (325815)
- Script profiler installs its client on every debugger of the
environment. `7c0df54a062` (323477)
- GLib and Socket `RemoteInspector::setup` ignore a Setup message for a
target that already has a connection. `2dc02450f27` (326023)
- Wasm debugger: `qMemoryRegionInfo` always describes a region; no null
`CodeBlock` read for host-function frames. `840c8efcbd1` (325534)
`3087a955392` (325518)

### WTF

- `CString` typing series (see Notes): typed C strings everywhere,
`UTF8CStringView`, `UTF8CString::unsafeFromUTF8()` / `fromUTF8()`,
`StringView(const ASCIICString&)`, `StringView::codePointCount()`,
`SAFE_*` printf macros in more call sites, logging passes `utf8()`
instead of lossy `ascii()`. `165f9ed4d01` `8a38069dceb` `0f3178a96a1`
`df3a6b749ba` `f2ca04538ea` `22e1cc5592e` `1bb8f8abf2d` `2c5cd6d774d`
`4b3653d2ed3` `4cd0b19673f` `4356bc5514b` `3fb615cd242` `ce9608ae41d`
- New `wtf/posix/POSIXExtras.h` (`posixOpen`, `posixStat`, `posixFopen`,
… taking `UTF8CStringView`) and `wtf/glib/GLibExtras.h` wrappers and
`SAFE_G_*` macros. `a506af486d6` `6448e98fedb` `b46c2cfc57b`
`bdea79f67f1`
- `ReadWriteLock` reimplemented as a 16-byte phase-fair lock with
`Locker { lock.read() }` / `Locker { lock.write() }`;
`ParkingLot::unparkCount(address, count, callback)` added and
`unparkOne` built on it. `5d43ab597d9` (325091)
- `find8()` compares the first 16 bytes with SIMD before falling back to
`memchr`. `046a5a8d15c` (325953)
- `Deque::constructAndAppend()`. `c06ff4e9930` (325441)
- `URL::string() &&` overload. `b31697dfe26` (325669)
- `CheckedPtr` / `RefPtr` constructible from `std::unique_ptr`,
`CheckedRef` / `Ref` from `UniqueRef`; `protect()` on those returns the
matching smart pointer. `31571413598` (325911)
- `GuaranteedSerialFunctionDispatcher::dispatchAfter()` with
implementations in `RunLoop`, `WorkQueue`, `MainThreadDispatcher`.
`155da279924` (324711)
- `MonotonicObjectIdentifier` moved into WTF. `28001e7f661`
- Several reused vectors switch from `clear()` to `shrink(0)`.
`9679818e585` (325059)
- Swift interop: reference-counting annotations on WTF ref-counted base
classes behind `ENABLE(SWIFT_BASE_CLASS_ANNOTATIONS)`; annotation
renames; unused `SwiftCXXThunk` macros removed. `5f4afbfb124`
`46495d17ae3` `edbf6068f30`

### bmalloc / libpas

- MTE allocations are served from a separate `tagged_bmalloc_heap`; the
compact / non-compact distinction is removed from libpas internals and
dispatch happens in `bmalloc.h`. `c8cb35beaca` (317893)
- Compact heap reservation grows to 256 MiB with two allocation fronts
and an over-aligned compact pointer type. `5b0cfbc518e` (325689)
- bmalloc and libpas check `__SANITIZE_THREAD__`-style macros as well as
`__has_feature`. `4558079bcd4` (326161)
- `BCRASH()` static-analyzer annotation uses the checker's new name.
`f4478d48f89` (325557)

### Other

- Corpse tooling: `Corpse::Memory` for mapped views of remote corpse
memory; unused export flag removed. `9cf419e5c4b` `ce61c78c8b8`
- GLib API: `JSCException` and `JSCClass` use typed strings /
`GMallocString`; `jsc_exception_report()` decodes UTF-8 correctly;
`JSCCallbackFunction::construct()` sizes its argument span by
`argumentCount`. `780fb0070b8` `a5815bc9797` `a7c1eda41a7`
- Build fixes and gardening (one line): `720bc3a7c71` `5869341e6cc`
`523f2e2ae26` `48ab96aa6b2` `7c9c2207d0c`, Xcode filelist check landed
and reverted (`b77e70c813e` `5ce175f5a3c`), Safer C++ expectation
updates (`8591160211b` `18e3f9366c7` `ceba48c3cdf` `ce1fa925b83`), CMake
packaging for Cocoa (`c456b2841c0` `b98997507b7` `2ea49531186`
`d14a3b7571a`).
- 36 commits are WebCore/WebKit features whose only change in these
directories is a preference entry in `UnifiedWebPreferences.yaml` or a
platform macro (for example Document-Isolation-Policy, Device Posture,
CSS `calc-size()`, `corner-*` shorthands, threaded animations); they do
not change JSC or WTF behavior.
### What does this PR do?

This adds a TypeScript type checker to Bun.

```sh
bun check                        # type check the project
bun --check src/index.ts         # type check, then run
bun build --check src/index.ts   # type check, then bundle
bun test --check                 # type check, then run the tests
```

It's a port of the type checker in
[typescript-go](https://github.com/microsoft/typescript-go) 7.0.2. You
get the same errors as `tsc`, with the same error codes, messages, lines
and columns. If `bun check` and `tsc` 7 disagree about an error, that's
a bug in Bun.

`packages/next` in the Next.js repo, 2,881 files:

| | CPU time | Peak memory |
| --- | --- | --- |
| `tsc` 6.0.2 | 14.8 s | 2.33 GB |
| `tsc` 7.0.2 (typescript-go) | 6.9 s | 1.78 GB |
| `bun check` | 2.4 s | 0.69 GB |

(How that was measured is under Performance.)

It only type checks. It doesn't emit JavaScript, `.d.ts` files,
sourcemaps or `.tsbuildinfo`. There's no language server, so your editor
keeps using TypeScript.

Most of this description is about how we tested it. What's still wrong
is at the bottom.

## Usage

### Check a project

`bun check` looks for the nearest `tsconfig.json` and checks whatever
`files`, `include` and `exclude` select. It uses all of your CPU cores.

```sh
$ bun check
src/index.ts(3,25): error TS2322: Type 'string' is not assignable to type 'number'.
Found 1 error in 1 file, checked 2 files [14.00ms]
$ echo $?
1
```

```sh
$ bun check
✓ No type errors in 2 files [14.00ms]
$ echo $?
0
```

Errors go to stdout. The summary goes to stderr.

If your `package.json` already has a `check` script, `bun check` still
runs that script, like it does today. `bun --check` with no file is the
type checker in every project. `"check": "bun check"` works too: inside
that script, and in the scripts it runs, `bun check` is the type
checker.

```sh
bun --check
```

`bun --filter '*' check` and `bun --workspaces check` still run each
package's `check` script.

### Check some files

```sh
bun check src/index.ts
bun check src/server packages/shared
```

Bun checks those files and everything they import. Each file is checked
in the project your editor uses for it. That's the nearest
`tsconfig.json`, or, if that one doesn't include the file but has
`references`, the referenced project that does. So it works in the
layout `create vite` gives you.

A directory means the part of the project that's in it, so `bun check .`
is the same as `bun check`. If the project has no files there, like `bun
check scripts` when `include` is `["src"]`, Bun checks every file in the
directory that isn't excluded, each with the `tsconfig.json` nearest to
it.

### Choose a tsconfig

```sh
bun check -p packages/server
bun check -p tsconfig.test.json
```

### Without a tsconfig

`bun check index.ts` works in an empty directory. These are the
defaults:

```json
{
  "lib": ["ESNext"], "target": "ESNext", "module": "Preserve", "moduleDetection": "force",
  "jsx": "react-jsx", "allowJs": true, "moduleResolution": "bundler",
  "allowImportingTsExtensions": true, "verbatimModuleSyntax": true,
  "noEmit": true, "strict": true, "skipLibCheck": true
}
```

In a monorepo with no `tsconfig.json` at the root, `bun check` checks
everything below the current directory. Each file gets the options of
the `tsconfig.json` nearest to it, and each file is only checked once.

### Project references

`references` are followed like `tsc -b` does, except you don't have to
build anything first and nothing gets written to disk. Each project is
checked with its own compiler options. Errors are printed one project at
a time, in build order. A file that 2 projects include is checked in
both, and its errors are printed once for each, like `tsc -b` does.

```sh
$ bun check
packages/app/src/index.ts(2,14): error TS2322: Type 'number' is not assignable to type 'string'.
Found 1 error in 1 file, checked 176 files across 2 projects [544.00ms]
```

### `--check`

`--check` type checks the entry point and everything it imports before
doing anything else. If there's a type error, Bun prints it, exits with
code 1, and your code doesn't run.

```sh
$ bun --check src/good.ts
hello Ada

$ bun --check src/bad.ts
src/bad.ts(3,21): error TS2322: Type 'string' is not assignable to type 'number'.
Found 1 error in 1 file, checked 2 files
$ echo $?
1
```

With `--watch`, every restart is checked first. After a type error Bun
waits for the next change in any file of the program, including files
that only have types. `--hot --check` restarts the process like
`--watch` does, so the code is checked again before it runs.

A file without an extension, `-e`, `-p` and a script on stdin are
checked as TypeScript, which is how Bun runs them. If a file imports an
HTML page, the scripts of that page are checked too. `--conditions` and
`--loader` apply to the check: with `--loader .js:ts`, every `.js` file
of your project is TypeScript.

`--check` doesn't cover files loaded with `--preload`. `bun check` does,
if your `tsconfig.json` includes them.

```sh
bun --watch --check src/index.ts
bun test --watch --check
```

```sh
$ bun build --check src/good.ts --outdir out
Bundled 2 modules

  good.js  131 bytes  (entry point)

$ bun build --check src/bad.ts --outdir out
3 | console.log(greet({ age: "36" }));
                        ^
error: TS2322: Type 'string' is not assignable to type 'number'.
    at /app/src/bad.ts:3:21

1 | export function greet(user: { name?: string; age: number }) {
                                                 ^
note: TS6500: The expected type comes from property 'age' which is declared here on type '{ name?: string | undefined; age: number; }'
   at /app/src/greet.ts:1:46
$ ls out
ls: out: No such file or directory
```

```sh
$ bun test --check
src/bad.test.ts(3,39): error TS2322: Type 'string' is not assignable to type 'number'.
Found 1 error in 1 file, checked 3 files
```

A `package.json` script has no entry point to start from, so Bun checks
the whole project first, like `bun check`. Same with `--filter`,
`--parallel` and `--sequential`.

```sh
bun run --check dev
bun --check --filter '*' build
```

`--tsconfig-override` applies to the check too, so the type checker
reads the same `tsconfig.json` as the bundler and the runtime. So does
`tsconfig` in `Bun.build`.

`Bun.build` takes `check: true`. A type error fails the build like any
other build error, and nothing is written. The check uses the
`conditions` and `loader` of the build.

```ts
const result = await Bun.build({
  entrypoints: ["./src/index.ts"],
  outdir: "./out",
  check: true,
  throw: false,
});

for (const log of result.logs) {
  console.error(`${log.position?.file}:${log.position?.line}: ${log.message}`);
}
// /app/src/index.ts:3: TS2322: Type 'string' is not assignable to type 'number'.
```

### Compiler options as flags

Any compiler option works as a flag, like in `tsc`. Flags override
`tsconfig.json`.

```sh
bun check --noUncheckedIndexedAccess
bun check --strict false
bun check --target es2022
```

### Output

In a terminal:

```
1 | import { greet } from "./user";
2 |
3 | const message = greet({ id: "1", name: "Ada" });
                            ^
error: TS2322: Type 'string' is not assignable to type 'number'.
    at src/index.ts:3:25

2 |   id: number;
      ^
note: The expected type comes from property 'id' which is declared here on type 'User'
   at src/user.ts:2:3

Found 1 error in 1 file, checked 2 files [14.00ms]
```

When there are more than 50 errors, identical errors are grouped and
sorted by how often they happen. `--all` prints every one.

```
1 | export const v0: number = "0";
                 ^
error: TS2322: Type 'string' is not assignable to type 'number'.
    at m1.ts:1:14
    60 times in 2 files
      30  m1.ts:1
      30  m2.ts:1
```

When stdout isn't a terminal, you get the same format as `tsc --pretty
false`, so problem matchers and scripts written for `tsc` keep working.

```
src/index.ts(3,25): error TS2322: Type 'string' is not assignable to type 'number'.
```

In GitHub Actions, errors are also printed as workflow commands so they
show up as annotations on the diff, also when the step runs in a
subdirectory.

```
::error file=src/index.ts,line=3,col=25,endLine=3,endColumn=27,title=TS2322::Type 'string' is not assignable to type 'number'.
```

With `AGENT=1`:

```
<error file="src/index.ts" line="3" column="25" code="TS2322">
Type 'string' is not assignable to type 'number'.
<source>
1 | import { greet } from "./user";
2 |
3 | const message = greet({ id: "1", name: "Ada" });
                            ^^
</source>
<related file="src/user.ts" line="2" column="3">The expected type comes from property 'id' which is declared here on type 'User'</related>
</error>
```

### What you need installed

```sh
bun add -d @types/bun
```

That's for `console`, `fetch`, `Bun` and `bun:test`. `bun init` installs
it for you.

You don't need the `typescript` package. TypeScript's `lib.*.d.ts` files
(`Array`, `Promise`, the DOM and so on) are built into Bun. They're
always the ones from TypeScript 7.0.2, no matter which version your
project has installed. Your editor may still want the package.

```sh
$ bun -p process.versions.typescript
7.0.2
```

### Flags

| Flag | What it does |
| --- | --- |
| `-p`, `--project <path>` | A `tsconfig.json`, or the directory that
holds one |
| `--pretty` / `--no-pretty` | Source around each error, or one line per
error |
| `--all` | Show every error instead of grouping identical ones above 50
|
| `--threads <n>` | Number of threads. Default: one per core |
| `--timing` | Print how long loading and checking took |
| `--cwd <path>` | Set the working directory |
| `--strict`, `--target`, ... | Any compiler option, as for `tsc` |

Docs are in `docs/runtime/check.mdx`.

## Also fixed

3 bugs that aren't in the type checker. We ran into them while testing
it, and they're on main too.

- `Bun.build({ tsconfig: "./custom.json" })` ignored the option. The 3
tests for it passed anyway, because in each of them the `tsconfig.json`
that Bun finds by itself says the same thing. Now that build reads the
file in place of every `tsconfig.json`, like `--tsconfig-override`. For
both, a directory means the `tsconfig.json` in it, like `tsc -p`. Other
builds in the same process, and the program's own imports, aren't
affected, even while it's running.
- `--tsconfig-override` printed `Internal error: directory mismatch for
directory "/app/custom.json", fd 3. You don't need to do anything, but
this indicates a bug.` every time, for `bun build` and for `bun run`.
- On Linux without `pidfd_open`, a child process that exits could
interrupt a system call of the main thread with `EINTR`. Code that
doesn't retry, like an addon or `bun:ffi`, saw a failed `read()`. It
also made `spawn.test.ts` fail in about half of the ASAN builds of this
branch.

## Intentional differences from `tsc`

- No emit. `noEmit`, `declaration` and `incremental` don't change
anything.
- `bun check` has no `--watch`. `bun --watch --check <file>` checks
before every restart.
- Language service plugins (`plugins` in `compilerOptions`) aren't
loaded.
- `listFiles`, `listFilesOnly` and `traceResolution` work. Other options
that only print things, like `explainFiles`, are ignored.
- Messages that tell you to run `npm i --save-dev` say `bun add -d`.
- The `lib.*.d.ts` files are built in. A path in one of them is printed
as `bundled:///libs/lib.dom.d.ts`.
- A misspelled compiler option gets a suggestion: `Unknown compiler
option 'strct'. Did you mean 'strict'?`
- If an import fails because a package in `package.json` isn't
installed, a note names the `package.json` and says how to run `bun
install` for it.
- If a syntax error or an invalid option stops the type check, like it
does in `tsc`, a note says how many files weren't checked: `Stopped
before type checking 2 files. Fix the errors above to see the rest.`
- TS2309 (`export =` in a module that has other exports) is also
reported when the other export is in a `declare module` block in another
file. `tsc` 6.0.2 reports it too. `tsc` 7.0.2 only does when both are in
one file.
- `acc = apply(run, ["x", acc])` in a loop, where the parameter is a
tuple: `tsc` 7.0.2 reports TS2345 because the array literal loses its
tuple type. `tsc` 6.0.2 doesn't, and `bun check` doesn't.
- You get the same errors no matter how many threads it uses or what
order the files are in. That isn't always true of `tsc`.

You can see it with 2 files:

```js
// a_user.js
const p = new Packet()
p.a = 1
```
```js
// z_packet.js
class Packet {
  constructor () {
    this.a = null
    this.c = null
  }
  own () { return this.c }
}
```

With `allowJs`, `checkJs` and `strict`:

| | TS7008 on `a` | TS7008 on `c` |
| --- | --- | --- |
| tsc 6.0.2 | yes | yes |
| tsgo 7.0.2 `--singleThreaded` or `--checkers 1` | yes | yes |
| tsgo 7.0.2, default | no | yes |
| all of the above, with `a_user.js` renamed to `zz_user.js` | no | yes
|
| `bun check`, either name | yes | yes |

### How did you verify your code works?

This is about 150,000 lines of new code, so there's a lot of room for
mistakes. A type checker that's wrong once in a thousand files is worse
than not having one, and "the tests pass" isn't a good enough reason to
trust it. Most of the work here went into trying to prove it wrong.

Everything below compares `bun check` against the real `tsc` from
TypeScript 7.0.2 on the same files. None of it compares against what we
think the answer should be.

#### Real projects

Each of these repos is checked with `tsc` 7.0.2 and with `bun check`,
once for every `tsconfig.json` in it. "Identical" means both report the
same set of (file, line, column, error code).

| Project | `tsconfig.json` files | Identical | Errors in `tsc` | Errors
in `bun check` | Messages worded differently |
| --- | ---: | ---: | ---: | ---: | ---: |
| [vercel/ai](https://github.com/vercel/ai) | 52 | 52 | 9,742 | 9,742 |
0 |
|
[anthropics/anthropic-sdk-typescript](https://github.com/anthropics/anthropic-sdk-typescript)
| 16 | 16 | 596 | 596 | 0 |
|
[apollographql/apollo-client](https://github.com/apollographql/apollo-client)
| 7 | 7 | 559 | 559 | 0 |
| [arktypeio/arktype](https://github.com/arktypeio/arktype) | 5 | 5 |
389 | 389 | 0 |
| [withastro/astro](https://github.com/withastro/astro) | 12 | 12 |
1,996 | 1,996 | 0 |
| [axios/axios](https://github.com/axios/axios) | 1 | 1 | 105 | 105 | 0
|
| [vuejs/core](https://github.com/vuejs/core) | 6 | 5 | 1,359 | 1,358 |
0 |
| [date-fns/date-fns](https://github.com/date-fns/date-fns) | 3 | 3 | 1
| 1 | 0 |
| [discordjs/discord.js](https://github.com/discordjs/discord.js) | 2 |
2 | 3 | 3 | 0 |
|
[drizzle-team/drizzle-orm](https://github.com/drizzle-team/drizzle-orm)
| 3 | 3 | 10,786 | 10,786 | 0 |
| [Effect-TS/effect](https://github.com/Effect-TS/effect) | 10 | 10 | 2
| 2 | 0 |
| [elysiajs/elysia](https://github.com/elysiajs/elysia) | 4 | 4 | 53 |
53 | 0 |
| [excalidraw/excalidraw](https://github.com/excalidraw/excalidraw) | 7
| 7 | 11,048 | 11,048 | 0 |
| [fastify/fastify](https://github.com/fastify/fastify) | 1 | 1 | 0 | 0
| 0 |
| [TanStack/form](https://github.com/TanStack/form) | 13 | 13 | 124 |
124 | 0 |
| [gcanti/fp-ts](https://github.com/gcanti/fp-ts) | 2 | 2 | 3 | 3 | 0 |
| [sindresorhus/got](https://github.com/sindresorhus/got) | 1 | 1 | 2 |
2 | 0 |
| [graphql/graphql-js](https://github.com/graphql/graphql-js) | 3 | 3 |
24 | 24 | 0 |
| [unjs/h3](https://github.com/unjs/h3) | 1 | 1 | 0 | 0 | 0 |
| [honojs/hono](https://github.com/honojs/hono) | 6 | 6 | 14,239 |
14,239 | 0 |
| [immerjs/immer](https://github.com/immerjs/immer) | 1 | 1 | 5 | 5 | 0
|
| [gcanti/io-ts](https://github.com/gcanti/io-ts) | 2 | 2 | 3 | 3 | 0 |
| [pmndrs/jotai](https://github.com/pmndrs/jotai) | 2 | 2 | 7 | 7 | 0 |
| [sindresorhus/ky](https://github.com/sindresorhus/ky) | 2 | 2 | 52 |
52 | 0 |
| [kysely-org/kysely](https://github.com/kysely-org/kysely) | 6 | 6 |
1,185 | 1,185 | 0 |
|
[langchain-ai/langchainjs](https://github.com/langchain-ai/langchainjs)
| 41 | 41 | 5,273 | 5,273 | 0 |
| [lit/lit](https://github.com/lit/lit) | 34 | 34 | 2,588 | 2,588 | 0 |
| [mikro-orm/mikro-orm](https://github.com/mikro-orm/mikro-orm) | 17 |
17 | 87,981 | 87,981 | 0 |
| [mobxjs/mobx](https://github.com/mobxjs/mobx) | 3 | 3 | 186 | 186 | 0
|
| [mswjs/msw](https://github.com/mswjs/msw) | 10 | 10 | 5,878 | 5,878 |
0 |
| [nestjs/nest](https://github.com/nestjs/nest) | 30 | 30 | 27,345 |
27,345 | 0 |
| [vercel/next.js](https://github.com/vercel/next.js) | 356 | 356 |
3,690 | 3,690 | 0 |
| [unjs/nitro](https://github.com/unjs/nitro) | 5 | 5 | 742 | 742 | 0 |
| [nuxt/nuxt](https://github.com/nuxt/nuxt) | 3 | 3 | 11 | 11 | 0 |
| [openai/openai-node](https://github.com/openai/openai-node) | 4 | 4 |
6 | 6 | 0 |
| [microsoft/playwright](https://github.com/microsoft/playwright) | 4 |
4 | 22 | 22 | 0 |
| [preactjs/preact](https://github.com/preactjs/preact) | 1 | 1 | 1 | 1
| 0 |
| [prisma/prisma](https://github.com/prisma/prisma) | 44 | 44 | 24,201 |
24,201 | 0 |
| [puppeteer/puppeteer](https://github.com/puppeteer/puppeteer) | 10 |
10 | 2,701 | 2,701 | 0 |
| [TanStack/query](https://github.com/TanStack/query) | 15 | 15 | 686 |
686 | 0 |
|
[react-hook-form/react-hook-form](https://github.com/react-hook-form/react-hook-form)
| 2 | 2 | 3 | 3 | 0 |
| [remix-run/react-router](https://github.com/remix-run/react-router) |
14 | 14 | 6,735 | 6,735 | 0 |
| [reduxjs/redux](https://github.com/reduxjs/redux) | 1 | 1 | 0 | 0 | 0
|
| [reduxjs/redux-toolkit](https://github.com/reduxjs/redux-toolkit) | 8
| 8 | 142 | 142 | 0 |
| [remeda/remeda](https://github.com/remeda/remeda) | 7 | 7 | 13 | 13 |
0 |
| [rollup/rollup](https://github.com/rollup/rollup) | 9 | 9 | 107 | 107
| 0 |
| [TanStack/router](https://github.com/TanStack/router) | 29 | 29 |
4,853 | 4,853 | 0 |
| [ReactiveX/rxjs](https://github.com/ReactiveX/rxjs) | 10 | 10 | 22,271
| 22,271 | 7 |
| [sequelize/sequelize](https://github.com/sequelize/sequelize) | 16 |
16 | 6,768 | 6,768 | 0 |
| [solidjs/solid](https://github.com/solidjs/solid) | 7 | 7 | 740 | 740
| 0 |
| [storybookjs/storybook](https://github.com/storybookjs/storybook) | 19
| 19 | 1,401 | 1,401 | 0 |
| [stripe/stripe-node](https://github.com/stripe/stripe-node) | 4 | 4 |
42 | 42 | 0 |
| [sveltejs/svelte](https://github.com/sveltejs/svelte) | 5 | 5 | 186 |
186 | 0 |
| [vercel/swr](https://github.com/vercel/swr) | 4 | 4 | 314 | 314 | 0 |
| [TanStack/table](https://github.com/TanStack/table) | 20 | 20 | 574 |
574 | 0 |
| [tldraw/tldraw](https://github.com/tldraw/tldraw) | 4 | 4 | 0 | 0 | 0
|
| [gvergnaud/ts-pattern](https://github.com/gvergnaud/ts-pattern) | 2 |
2 | 2 | 2 | 0 |
|
[total-typescript/ts-reset](https://github.com/total-typescript/ts-reset)
| 2 | 2 | 4 | 4 | 0 |
| [millsp/ts-toolbelt](https://github.com/millsp/ts-toolbelt) | 1 | 1 |
3 | 3 | 0 |
| [sindresorhus/type-fest](https://github.com/sindresorhus/type-fest) |
2 | 2 | 0 | 0 | 0 |
| [typeorm/typeorm](https://github.com/typeorm/typeorm) | 4 | 4 | 959 |
959 | 0 |
| [microsoft/TypeScript](https://github.com/microsoft/TypeScript) | 4 |
4 | 613 | 613 | 0 |
|
[typescript-eslint/typescript-eslint](https://github.com/typescript-eslint/typescript-eslint)
| 35 | 35 | 18,392 | 18,392 | 0 |
| [urql-graphql/urql](https://github.com/urql-graphql/urql) | 2 | 2 | 17
| 17 | 0 |
| [fabian-hiller/valibot](https://github.com/fabian-hiller/valibot) | 2
| 2 | 1 | 1 | 0 |
| [pmndrs/valtio](https://github.com/pmndrs/valtio) | 1 | 1 | 0 | 0 | 0
|
| [vitest-dev/vitest](https://github.com/vitest-dev/vitest) | 18 | 18 |
2,111 | 2,111 | 0 |
| [microsoft/vscode](https://github.com/microsoft/vscode),
`src/tsconfig.json` | 1 | 1 | 0 | 0 | 0 |
| vscode's extensions, tests and base configs | 105 | 105 | 5,685 |
5,685 | 0 |
| [statelyai/xstate](https://github.com/statelyai/xstate) | 8 | 8 | 545
| 545 | 0 |
| [jquense/yup](https://github.com/jquense/yup) | 1 | 1 | 1 | 1 | 0 |
| [colinhacks/zod](https://github.com/colinhacks/zod) | 10 | 10 | 191 |
191 | 0 |
| [pmndrs/zustand](https://github.com/pmndrs/zustand) | 1 | 1 | 0 | 0 |
0 |
| **all 72** | **1,103** | **1,102** | **286,267** | **286,266** | **7**
|

The error counts show how much there was to compare. They aren't what
you'd get building these projects properly. Each row adds up all of its
configs, each checked by itself, and most of the projects are installed
but not built, so imports of their own `dist` folders don't resolve. A
project with no errors only shows that `bun check` raises no false
alarms. One with thousands shows that it finds the same problems in the
same places. 82 of the configs have no errors in either.

VS Code's own build is `src/tsconfig.json`: 9,795 files, 0 errors in
both. The other 105 configs in that repo are its extensions, its tests
and base configs that only exist to be extended. The extensions'
dependencies aren't installed here, which is where 5,272 of those errors
come from.

There's no config that passes in one and fails in the other.

Not every one of those is a comparison of types. Of the 637 configs of
the 69 repos, `tsc` reports type errors in 494 (276,703 errors) and
nothing in 82. In the other 61 it stops at an error about the config
itself, most often `baseUrl`, which TypeScript 7 removed, and checks no
types at all. `bun check` stops there too, so those 61 only compare that
error. For 7 repos that's every config we have: discord.js, fp-ts,
io-ts, preact, ts-toolbelt, urql and yup.

The table counts an error as the same if the file, line, column and code
are. Comparing the whole text of all 277,975 errors, with the lines
under them, 9 are worded differently: the 7 in rxjs that are under Known
problems, and 1 in sequelize that 2 configs report, where `tsc` names
the type `string | (object & { url?: string; })` and `bun check` names
its alias.

For vscode, next.js and elysia that's every `tsconfig.json` in the repo.
The other 69 repos have 1,311 between them, and we dropped the ones that
give the same answer as another config in the same repo. Configs with
`references` (138 of them) are compared against `tsc -b`, the rest
against `tsc -p`.

That comparison is about which errors there are. For order and repeats
we took every config with `references`, 317 in 21 of the repos, and
compared the error lines one by one as they're printed. `tsc -b` repeats
a line in 26 of them, 1,047 lines in all, because 2 projects include the
same file. 316 are identical. The other one is the vercel-ai config
below, and it's identical to the second build.

2 places where the comparison is lenient:

- typescript-go's TS6059 and TS6307 (a file isn't under `rootDir`, or
isn't listed in the project) change from run to run. We ran it 6 times
per config. `bun check` is allowed to report what any of the 6 runs
reported, and has to report what all 6 did.
- One config (in vercel-ai) is compared against typescript-go's second
`-b` build instead of its first. Its first build caches "this file
doesn't exist" for outputs that it then writes, and reports 167 errors
that go away if you run it again.

Elysia's [Eden Treaty](https://elysiajs.com/eden/overview) turns the
type of a whole server into a typed client, which is about as hard on a
type checker as real code gets. Besides the repo, we check apps with 17,
50 and 200 routes, and a program that uses `edenFetch`, WebSockets,
macros, guards, file uploads, cookies, SSE and validators from zod,
valibot and arktype, each with correct and incorrect calls. Every error
and every message is identical. One of the apps is a test in this PR.

These are npm packages that publish their `.ts` sources and a
`tsconfig.json`, checked where they sit in `node_modules`:

| Package | Version | Files | Errors in `tsc` | Errors in `bun check` |
Messages worded differently |
| --- | --- | ---: | ---: | ---: | ---: |
| `<%= dasherize(name) %>` | 0.0.0 | 6 | 1 | 1 | 0 |
| `@actions/github-script` | 7.0.1 | 7 | 0 | 0 | 0 |
| `@arrows/array` | 1.4.1 | 97 | 0 | 0 | 0 |
| `@arrows/composition` | 1.2.2 | 28 | 4 | 4 | 0 |
| `@arrows/dispatch` | 1.0.3 | 11 | 0 | 0 | 0 |
| `@arrows/error` | 1.0.2 | 3 | 2 | 2 | 0 |
| `@arrows/multimethod` | 1.4.1 | 12 | 4 | 4 | 0 |
| `atomically` | 1.7.0 | 11 | 26 | 26 | 0 |
| `@aws-crypto/sha1-browser` | 5.2.0 | 5 | 14 | 14 | 0 |
| `@aws-crypto/sha256-browser` | 5.2.0 | 5 | 14 | 14 | 0 |
| `@aws-crypto/sha256-js` | 5.2.0 | 5 | 15 | 15 | 0 |
| `@aws-crypto/util` | 5.2.0 | 5 | 15 | 15 | 0 |
| `@azure/arm-appservice` | 15.0.0 | 69 | 12 | 12 | 0 |
| `@azure/arm-resources` | 5.2.0 | 25 | 2 | 2 | 0 |
| `@bcoe/v8-coverage` | 0.2.3 | 8 | 6 | 6 | 0 |
| `benny` | 3.7.1 | 13 | 10 | 10 | 0 |
| `@braintree/sanitize-url` | 7.1.1 | 4 | 1 | 1 | 0 |
| `@braintree/sanitize-url` | 7.1.2 | 4 | 1 | 1 | 0 |
| `comment-parser` | 1.2.4 | 35 | 25 | 25 | 0 |
| `comment-parser` | 1.4.1 | 35 | 27 | 27 | 0 |
| `comment-parser` | 1.4.5 | 35 | 27 | 27 | 0 |
| `comment-parser` | 1.4.6 | 35 | 27 | 27 | 0 |
| `comment-parser` | 1.4.7 | 35 | 27 | 27 | 0 |
| `comment-parser` | 1.4.8 | 35 | 1 | 1 | 0 |
| `comment-parser` | 1.4.9 | 35 | 1 | 1 | 0 |
| `@compodoc/ngd-core` | 2.1.1 | 3 | 0 | 0 | 0 |
| `@compodoc/ngd-transformer` | 2.1.3 | 3 | 36 | 36 | 0 |
| `convex` | 1.28.2 | 174 | 135 | 135 | 0 |
| `convex-helpers` | 0.1.104 | 30 | 1 | 1 | 0 |
| `csp_evaluator` | 1.1.1 | 22 | 0 | 0 | 0 |
| `devcert` | 1.2.3 | 11 | 14 | 14 | 0 |
| `dexie-react-hooks` | 4.4.0 | 23 | 0 | 0 | 0 |
| `editions` | 6.22.0 | 4 | 6 | 6 | 0 |
| `@electric-sql/pglite-socket` | 0.0.6 | 8 | 2 | 2 | 0 |
| `@electric-sql/pglite-socket` | 0.1.3 | 9 | 2 | 2 | 0 |
| `@electric-sql/pglite-socket` | 0.2.7 | 10 | 3 | 3 | 0 |
| `@electric-sql/pglite-tools` | 0.2.7 | 7 | 2 | 2 | 0 |
| `@electric-sql/pglite-tools` | 0.3.3 | 7 | 2 | 2 | 0 |
| `@embroider/reverse-exports` | 0.2.0 | 3 | 86 | 86 | 0 |
| `example-typescript` | 1.0.0 | 7 | 13 | 13 | 0 |
| `expo-asset` | 11.1.5 | 25 | 80 | 80 | 0 |
| `expo-asset` | 12.0.13 | 25 | 74 | 74 | 0 |
| `expo-asset` | 57.0.18 | 26 | 94 | 94 | 0 |
| `expo-constants` | 17.1.6 | 6 | 69 | 69 | 0 |
| `expo-constants` | 18.0.13 | 6 | 67 | 67 | 0 |
| `expo-constants` | 57.0.19 | 6 | 87 | 87 | 0 |
| `expo-file-system` | 18.1.10 | 18 | 79 | 79 | 0 |
| `expo-file-system` | 19.0.22 | 22 | 52 | 52 | 0 |
| `expo-file-system` | 57.0.7 | 36 | 70 | 70 | 0 |
| `expo-font` | 13.3.1 | 19 | 18 | 18 | 0 |
| `expo-font` | 14.0.11 | 21 | 56 | 56 | 0 |
| `expo-font` | 57.0.4 | 24 | 2 | 2 | 0 |
| `expo-haptics` | 15.0.8 | 4 | 53 | 53 | 0 |
| `expo-image` | 3.0.11 | 32 | 94 | 94 | 0 |
| `expo-keep-awake` | 14.1.4 | 4 | 5 | 5 | 0 |
| `expo-keep-awake` | 15.0.8 | 4 | 52 | 52 | 0 |
| `expo-keep-awake` | 57.0.2 | 4 | 70 | 70 | 0 |
| `expo-linking` | 8.0.12 | 13 | 69 | 69 | 0 |
| `expo-modules-autolinking` | 2.1.10 | 22 | 3 | 3 | 0 |
| `expo-modules-autolinking` | 3.0.25 | 40 | 2 | 2 | 0 |
| `expo-modules-autolinking` | 57.0.13 | 46 | 1 | 1 | 0 |
| `expo-modules-core` | 2.3.13 | 41 | 25 | 25 | 0 |
| `expo-modules-core` | 3.0.30 | 44 | 57 | 57 | 0 |
| `expo-modules-core` | 57.0.18 | 46 | 75 | 75 | 0 |
| `expo-splash-screen` | 31.0.13 | 4 | 53 | 53 | 0 |
| `expo-sqlite` | 14.0.6 | 14 | 34 | 34 | 0 |
| `expo-status-bar` | 3.0.9 | 5 | 56 | 56 | 0 |
| `expo-status-bar` | 57.0.1 | 6 | 72 | 72 | 0 |
| `expo-symbols` | 1.0.8 | 5 | 55 | 55 | 0 |
| `expo-system-ui` | 6.0.9 | 8 | 52 | 52 | 0 |
| `expo-web-browser` | 15.0.11 | 7 | 52 | 52 | 0 |
| `@expo/devcert` | 1.2.0 | 11 | 25 | 25 | 0 |
| `@expo/devcert` | 1.2.1 | 11 | 84 | 84 | 0 |
| `@expo/dom-webview` | 57.0.1 | 8 | 76 | 76 | 0 |
| `@expo/log-box` | 57.0.4 | 42 | 74 | 74 | 0 |
| `feed` | 4.2.2 | 12 | 0 | 0 | 0 |
| `@glimmer/component` | 2.1.1 | 4 | 5 | 5 | 0 |
| `@huggingface/jinja` | 0.3.4 | 6 | 0 | 0 | 0 |
| `human-id` | 4.1.1 | 3 | 0 | 0 | 0 |
| `human-id` | 4.1.3 | 3 | 0 | 0 | 0 |
| `human-id` | 4.2.0 | 3 | 0 | 0 | 0 |
| `human-id` | 4.2.1 | 3 | 0 | 0 | 0 |
| `import-in-the-middle` | 1.14.0 | 3 | 2 | 2 | 0 |
| `import-in-the-middle` | 1.15.0 | 3 | 2 | 2 | 0 |
| `import-path-rewrite` | 0.0.1 | 4 | 5 | 5 | 0 |
| `junit-xml` | 1.2.0 | 5 | 1 | 1 | 0 |
| `khroma` | 2.1.0 | 50 | 148 | 148 | 0 |
| `@layerup/layerup-security` | 1.6.0 | 5 | 0 | 0 | 0 |
| `@lokalise/node-api` | 12.8.0 | 226 | 1 | 1 | 0 |
| `merge-anything` | 2.4.4 | 3 | 6 | 6 | 0 |
| `mimetext` | 3.0.28 | 8 | 7 | 7 | 0 |
| `@monaco-editor/react` | 4.7.0 | 3 | 4 | 4 | 0 |
| `mongodb` | 6.21.0 | 131 | 64 | 64 | 0 |
| `mongodb` | 7.5.0 | 131 | 1 | 1 | 0 |
| `mongodb` | 7.6.0 | 132 | 1 | 1 | 0 |
| `mongodb` | 7.7.0 | 132 | 62 | 62 | 0 |
| `moo-color` | 1.0.3 | 12 | 33 | 33 | 0 |
| `next-mdx-remote-client` | 1.1.2 | 15 | 1 | 1 | 0 |
| `npx-import` | 1.1.4 | 4 | 1 | 1 | 0 |
| `oblivious-set` | 2.0.0 | 3 | 14 | 14 | 0 |
| `onigasm` | 2.2.5 | 5 | 33 | 33 | 0 |
| `openapi3-ts` | 2.0.2 | 12 | 11 | 11 | 0 |
| `opencontrol` | 0.0.6 | 5 | 52 | 52 | 0 |
| `path-data-parser` | 0.1.0 | 4 | 0 | 0 | 0 |
| `pickleparser` | 0.2.1 | 19 | 278 | 278 | 0 |
| `pino` | 10.3.0 | 17 | 3 | 3 | 0 |
| `pino` | 10.3.1 | 17 | 3 | 3 | 0 |
| `pino` | 8.17.2 | 17 | 42 | 42 | 0 |
| `pino` | 9.14.0 | 17 | 42 | 42 | 0 |
| `piscina` | 3.1.0 | 28 | 34 | 34 | 0 |
| `piscina` | 4.8.0 | 46 | 46 | 46 | 0 |
| `pkg-pr-new` | 0.0.53 | 4 | 8 | 8 | 0 |
| `pkg-pr-new` | 0.0.71 | 4 | 44 | 44 | 0 |
| `pkg-pr-new` | 0.0.72 | 4 | 12 | 12 | 0 |
| `pkg-pr-new` | 0.0.75 | 4 | 12 | 12 | 0 |
| `pkg-pr-new` | 0.0.87 | 4 | 48 | 48 | 0 |
| `pkg-pr-new` | 0.0.88 | 4 | 48 | 48 | 0 |
| `@pnpm/config.env-replace` | 1.1.0 | 3 | 11 | 11 | 0 |
| `@pnpm/network.ca-file` | 1.0.2 | 3 | 11 | 11 | 0 |
| `protractor` | 7.0.0 | 7 | 1 | 1 | 0 |
| `react-static-example-typescript` | None | 10 | 17 | 17 | 0 |
| `@redocly/openapi-core` | 1.34.20 | 278 | 49 | 49 | 0 |
| `resolve-package-path` | 1.2.7 | 4 | 116 | 116 | 0 |
| `resolve-package-path` | 2.0.0 | 4 | 116 | 116 | 0 |
| `resolve-package-path` | 3.1.0 | 4 | 104 | 104 | 0 |
| `rxjs` | 7.3.0 | 250 | 2 | 2 | 0 |
| `rxjs` | 7.8.1 | 251 | 1 | 1 | 0 |
| `rxjs` | 7.8.2 | 251 | 1 | 1 | 0 |
| `signal-polyfill` | 0.2.2 | 25 | 14 | 14 | 0 |
| `@stablelib/base64` | 1.0.1 | 3 | 15 | 15 | 0 |
| `@statelyai/inspect` | 0.4.0 | 11 | 22 | 22 | 0 |
| `@streamparser/json` | 0.0.21 | 29 | 1 | 1 | 0 |
| `@supabase/ssr` | 0.5.2 | 12 | 4 | 4 | 0 |
| `template` | 0.0.0 | 35 | 63 | 63 | 0 |
| `tunnel-rat` | 0.1.2 | 3 | 0 | 0 | 0 |
| `unfurl.js` | 6.4.0 | 14 | 16 | 16 | 0 |
| `uri-js` | 4.2.2 | 10 | 1 | 1 | 0 |
| `@vercel/agent-eval-playground` | 0.1.3 | 40 | 106 | 106 | 0 |
| `@verdaccio/core` | 8.0.0-next-8.1 | 19 | 3 | 3 | 0 |
| `@verdaccio/logger-prettify` | 8.0.0-next-8.0 | 9 | 15 | 15 | 0 |
| `@verdaccio/url` | 13.0.0-next-8.1 | 6 | 2 | 2 | 0 |
| `@verdaccio/utils` | 7.0.1-next-8.1 | 11 | 7 | 7 | 0 |
| `vite-plugin-externalize-deps` | 0.10.0 | 4 | 0 | 0 | 0 |
| `vscode-tas-client` | 0.3.3 | 7 | 0 | 0 | 0 |
| `@wdio/xvfb` | 9.27.2 | 5 | 16 | 16 | 0 |
| `webpod` | 0.0.2 | 4 | 8 | 8 | 0 |
| `with` | 7.0.2 | 3 | 10 | 10 | 0 |
| `workbox-core` | 7.3.0 | 30 | 34 | 34 | 0 |
| `workbox-core` | 7.4.1 | 30 | 34 | 34 | 0 |
| `@workflow/world` | 5.0.0-beta.27 | 29 | 4 | 4 | 0 |
| `yaml-ast-parser` | 0.0.43 | 38 | 167 | 167 | 0 |
| **all 151** |  | **4,096** | **4,496** | **4,496** | **0** |

We developed against most of the repos in the first table, so try it on
your own project:

```sh
bunx -p typescript@7.0.2 tsc --noEmit --pretty false > tsc.txt
bun check --no-pretty > bun.txt
diff tsc.txt bun.txt
```

If there's a difference, please open an issue.

#### Compiler options the projects don't use

Projects only exercise the options they've chosen. So the 69 repos are
checked again under 11 other sets of options.

| Options | Projects | Identical | Errors in `tsc` | Errors in `bun
check` |
| --- | ---: | ---: | ---: | ---: |
| every strictness flag on | 69 | 69 | 100,756 | 100,756 |
| `strict` off | 69 | 68 | 76,960 | 76,958 |
| `skipLibCheck` off | 69 | 69 | 90,609 | 90,609 |
| `checkJs` | 69 | 69 | 84,122 | 84,122 |
| `declaration` | 69 | 69 | 80,495 | 80,495 |
| `isolatedModules` with `verbatimModuleSyntax` | 69 | 69 | 80,622 |
80,622 |
| `isolatedDeclarations` | 62 | 62 | 34,982 | 34,982 |
| `erasableSyntaxOnly` with legacy decorators | 69 | 69 | 71,577 |
71,577 |
| `nodenext` | 69 | 69 | 85,238 | 85,238 |
| `"types": []` | 69 | 69 | 105,818 | 105,818 |
| `"lib": ["es5"]` | 69 | 69 | 83,872 | 83,872 |

typescript-go crashes or times out on 7 repos with
`isolatedDeclarations`, so those aren't compared. Of the 896,192 errors
both report across all 11 sets, 21 are worded differently.

#### TypeScript's own tests

typescript-go runs TypeScript's compiler and conformance tests on itself
(12,762 files). Some tests ask for several sets of compiler options,
which makes 13,101 runs. For each one it commits what it expects, as
files. `bun check` has to produce the same files, byte for byte.

| Baseline | What it holds | Tests | Identical |
| --- | --- | ---: | ---: |
| `.errors.txt` | every error, with the source around it | 13,101 |
13,101 |
| `.types` | the printed type of every expression and name | 12,463 |
12,463 |
| `.symbols` | the symbol of every name, with where it is declared |
12,463 | 12,463 |
| `.js` | the declaration files that are emitted, and the errors in them
| 13,032 | 13,032 |
| `.trace.json` | the log of how every import was resolved, with
`traceResolution` | 151 | 151 |

Not every test has every kind of baseline. The `.errors.txt` baselines
contain 966 different error codes.

A `.js` baseline also has the JavaScript that `tsc` emits. That part
isn't compared, because Bun has its own transpiler. The same goes for
the `.js.map` and `.sourcemap.txt` baselines. `bun check` doesn't write
declaration files for you either. It generates them in memory for
projects that other projects reference, and that's the code these
baselines test.

typescript-go's test runner skips 45 tests, and 8 more when it compares
what's emitted. This one skips the same ones.

The tests and the baselines are vendored in this PR as one file
(`test/cli/check/typescript-go/bundle.zst`, 8 MB, written by `sync.ts`
from a typescript-go tag). All 5 kinds run in CI:

```sh
bun bd test test/cli/check/conformance
```

#### Generated programs

TypeScript's tests only have a few forms of some things. Take this:

```ts
let s: S | null = mk();
while (s) {
  const cur = { v: s };
  s = cur.v;
}
```

`tsc` reports TS7022 on `cur`. Its type needs the type of `s`, which
needs what the loop assigns at the bottom, which needs `cur`. There are
thousands of ways to write that loop, and a type checker can pass every
one of TypeScript's tests and still say nothing about most of them.

So there are generators. Each one writes a cross product with one
function per line, runs `tsc` and `bun check` on it, and compares.
There's no expected output stored anywhere. `tsc` is the oracle.

These are in `test/cli/check/differential.test.ts`, and all of them
pass:

| What it generates | Functions | What has to match |
| --- | ---: | --- |
| narrowing: the reference, the guard, the control flow around it, what
happens before the use | 46,080 | every error and its message |
| variables without a type annotation | 29,484 | every error and its
message |
| calls with callbacks: the callee, the form of the callback, the
context of the call | 9,984 | every error and its message |
| circular references, directly and through a second declaration | 9,196
| every error and its message |
| a variable in a loop that's assigned back, in 12 kinds of loop |
20,304 | which loops have an error |
| index signatures: 14 kinds of key like `string & {}`, what they're
indexed by, how a name is written in a destructuring or an object
literal | 4,522 | every line `tsc` prints |
| index signatures: the key of the source and the key of the target, in
assignments, inference and redeclarations | 1,248 | every line `tsc`
prints |
| a circular reference next to a call with overloads and a callback |
3,888 | every line `tsc` prints |
| an interface that extends a mapped type of itself, in a `.d.ts` that
`skipLibCheck` hides: 13 forms of base type, 9 kinds of declaration, 36
uses | 4,212 | every line `tsc` prints |
| the same in a file that's checked, with the declaration before its use
| 4,212 | every line `tsc` prints |
| a conditional type inside another one that checks the same type, with
`infer` | 480 | every line `tsc` prints |
| a boolean: 13 ways to get one, 16 types it isn't assignable to, 8 ways
to assign it | 1,664 | every line `tsc` prints |
| a package with `export =`: what it exports, what `declare module` adds
to it, how it's imported, 3 messages | 144 | every line `tsc` prints |
| a parameter property that is a pattern: 5 modifiers, 5 patterns, 3
sets of options | 75 | every line `tsc` prints |
| a function from an index signature that is tested and not called: 11
receivers, 11 tests | 119 | every line `tsc` prints |
| a generator with literals where any type is expected: 14 contexts, 10
generators | 140 | every line `tsc` prints |
| a conditional or a mapped type over `this`: 9 types, 28 uses | 252 |
every line `tsc` prints |
| a type that is generic as an object, as an index, as both or as
neither: 13 types, 12 places | 156 | every line `tsc` prints |
| what a project with references imports: 5 imports, 5 sets of options,
3 sets of references | 75 | every line `tsc -b` prints, in order |

It also generates small projects, for file names that differ only in
case, like `import "./Button"` for `button.ts`. macOS and Windows take
those for the same file and Linux doesn't, so it runs on both kinds of
file system. Every case runs with `forceConsistentCasingInFileNames` on,
off and unset.

| What it generates | Cases | What has to match |
| --- | ---: | --- |
| how a file is referred to (8 forms of import and reference, packages,
`files`), in which spellings, in which order, from where | 150 | every
line `tsc` prints |
| the same where case matters, plus 2 real files like `Button.ts` and
`button.ts` | 167 | every line `tsc` prints |
| a project reference that's spelled differently from the directory, or
in 2 spellings | 27 | every line `tsc -b` prints |
| `tsconfig.json` with a value that isn't JSON, like `"strict": tru`: 28
kinds of value in 30 places | 840 | every line `tsc` prints |
| `tsconfig.json` whose root is a list, like `[{ "compilerOptions": {}
}]` | 9 | every line `tsc` prints |
| 3 projects with `references` and files that all of them include: the
order at the top, who references whom, a project that doesn't exist, a
type library that's missing | 189 | every line `tsc -b` prints, in order
|
| `import()` and `require()` with `import` or `require` somewhere in
their text, which changes what `tsc` says about why a file is in the
program | 44 | every line `tsc` prints |

The index signature and overload programs end with a line that has a
type error, and `tsc` has to report it. After a syntax error neither of
them checks anything, and then they'd agree about everything. CI runs 70
of the 840 `tsconfig.json` files, because each one takes 2 processes.
All 840 were compared on a laptop.

It runs in CI against TypeScript 7.0.2, which `test/package.json`
installs next to 6 as `typescript7`. You can also point it at another
`tsc`:

```sh
TSC=/path/to/typescript-7/tsc bun bd test test/cli/check/differential.test.ts
```

These run locally against `tsc` and aren't in the repo yet:

| What it generates | Programs | Different from `tsc` |
| --- | ---: | ---: |
| classes: access, overriding, initialization, heritage, `super`,
references to itself, 2 members with one name | 290,000 | 0 |
| overloads, inference, variance (21 groups) | 1,300,000 | 0 |
| JSX: 48 tags, 48 lists of attributes, 23 forms of children | 63,770 |
0 |
| where a long type is cut off in a message | 1,060,000 | 0 |
| 2 declarations of one variable or property | 123,891 | 0 |
| module augmentation through re-exports and aliases | 210 | 1 |
| JSDoc tags on every kind of host, in checked JavaScript | 68,334 | 0 |
| class expressions that refer to what holds them | 40,128 | 21 |
| type annotations inside an expression that refer to what's being
declared | 50,688 | 95 |
| symbols that `tsc` creates lazily | 15,444 | 25 |
| immediately invoked functions | 19,200 | 21 |
| type aliases that refer to themselves without end, through 36 kinds of
type | 517 | 28 |

The 1 in module augmentation is the wording of a message. In 28 of the
messages that are cut off, `tsc` doesn't agree with itself from run to
run, and those aren't counted.

The 4 rows before the last are all code that refers to itself while it's
being declared, like `class A { x = { a: null! as { p: A["x"] } } }`. In
3 of those 162, `tsc` reports an error and `bun check` reports nothing
for that declaration.

In the 28 of the last row, both report errors, but not the same list:
TS2589 is at another position or in another order, or the TS2538 next to
it is missing. `tsc` crashes with a stack overflow on 23 more of those
programs, which aren't counted. `bun check` reports an error in all 23.

#### Is it overfit to the tests?

- We searched the type checker's code for the names of TypeScript's
tests. Of the 12,271 names that are 12 characters or longer, 19 appear
in code. All 19 are also the name of a compiler option or a builtin
(`esModuleInterop`, `noUncheckedIndexedAccess`, `defineProperty` and so
on).
- It's a port. 1,791 of the 2,770 functions in typescript-go's
`internal/checker` are named in comments, so you can read the two side
by side.
- A bug found in a real project or by a generator gets reduced to a few
lines and added to `test/cli/check/check.test.ts`, with the output of
`tsc` as the expected output. A test is only added if it fails without
the fix. There are 431 tests in that file.

#### Read side by side with typescript-go

Passing tests doesn't show that a port behaves like the original.
TypeScript's tests guard `tsc` against its own regressions, and `tsc`
never had this port's bugs. So every function of typescript-go's
checker, binder, module resolver, config parser, program and build mode
was read next to its counterpart here. Wherever the two could behave
differently, the reader had to write a small program that shows it and
run both tools on it. A second reader ran each program again and tried
to refute it.

1,706 of those programs are in `test/cli/check/differential-cases.json`.
`differential-cases-1.test.ts` to `differential-cases-4.test.ts` run
`tsc` 7.0.2 and `bun check` on each, on every platform where CI runs
Bun's tests, and the output has to be the same. Windows skips 3 that
need a directory named `c:`.

| What the program is about | Programs |
| --- | ---: |
| an error that `tsc` reports | 671 |
| an error that `tsc` doesn't report | 538 |
| the words of a message | 419 |
| the position of an error | 55 |
| the order of the errors | 22 |
| a crash | 1 |

The programs on which the two disagree are under "Known problems".

#### Threads

It uses all of your cores, so the answer could depend on which thread
gets somewhere first.

One of the tests generates 60 modules that all enter the same cycles
(variance of type parameters, recursive type aliases, functions and
constants without annotations) and checks them on 1, 2, 3, 8 and 16
threads. The output has to be byte for byte the same. typescript-go
reports 518, 519, 521 and 525 errors for that program with 1, 2, 4 and 8
checkers.

#### Broken code

You run a type checker on code you're in the middle of writing. The
worst thing it can do there is give up on a file and say everything is
fine.

One of the tests takes 268 valid files from TypeScript's tests and
damages each in 7 ways (truncate, delete a token, insert a stray token,
swap 2 tokens, replace a token, cut at a random byte, delete a line),
then appends a type error. With 17 more written by hand, that's 1,893
files. Every one of them has to report something.

#### Breaking real code

Most of the code in the projects above compiles. To see what happens to
real code with errors in it, a script takes `packages/effect` from
[Effect](https://github.com/Effect-TS/effect) (496 files, and about as
hard on a type checker as TypeScript gets), breaks 4 files at a time,
runs `tsc` and `bun check`, and compares every error with its whole
message. There are 32 ways to break a file: swap 2 arguments, drop a
type argument, turn `never` into `unknown`, delete an overload, reverse
a conditional type, remove `readonly`, and so on.

| Mutations | Errors in `tsc` | Different error codes | Missing in `bun
check` | Only in `bun check` | Worded differently |
| ---: | ---: | ---: | ---: | ---: | ---: |
| 913 | 2,925 | 67 | 0 | 0 | 8 |

#### Module resolution

With `traceResolution`, `tsc` logs every step of resolving every import:
each file it looks for, each `package.json` it reads, each condition in
`exports` it tries. `bun check` prints the same log. For 69 of the
projects above that's 8,463,167 lines, and all 69 logs are identical.

typescript-go resolves on several threads, so which lookup of a
`package.json` says "cached" changes between runs. Both logs go through
the normalization that typescript-go's own test runner uses for that.

#### Overflow checks and assertions

A release build doesn't check for integer overflow, so an index that
wraps around can go unnoticed as long as the output looks right. All of
TypeScript's tests and all 637 `tsconfig.json` files from the survey
also run on a build with overflow checks and debug assertions turned on.
Nothing panics and the output is the same.

#### Big inputs

16 tests generate programs that grow in one direction: 400 `if`
statements in a `finally` block, 400 nested loops, a chain of 400 method
calls, a union of 100 object types narrowed one member at a time, 400
overloads, and so on. An exponential algorithm doesn't finish at those
sizes.

4 more are long instead of deep: 40,000 top-level lines of `export const
v = o.a.b.c + k.y.y.x`, of template expressions, and of calls with
callbacks. 16,000 lines of the first take 0.15 seconds. `tsc` 7.0.2
takes 53.

#### Everything else

Adding a type checker shouldn't make `bun run` or `bun build` slower
when you aren't type checking. Parsing 7,012 TypeScript files (37 MB)
takes 1.003x the instructions it does on `main`, and `typescript.js` (9
MB) takes 0.995x.

#### Performance

`tsc` 7.0.2 with default settings against `bun check` on 16 threads.
16-core Apple silicon, 5 rounds, alternating between the two. Wall clock
time is the fastest round, CPU time and memory are medians. Both report
exactly the same errors in every row.

Wall clock time:

| Project | Files | `tsc` 7.0.2 | `bun check` | Faster |
| --- | ---: | ---: | ---: | ---: |
| vscode `src` | 9,795 | 5.98 s | 1.24 s | 4.8x |
| mikro-orm | 2,883 | 5.97 s | 1.20 s | 5.0x |
| next.js `packages/next` | 2,881 | 1.82 s | 0.28 s | 6.4x |
| next.js, root | 3,547 | 1.28 s | 0.42 s | 3.1x |
| storybook `scripts` | 1,039 | 0.96 s | 0.27 s | 3.5x |
| nuxt | 839 | 0.80 s | 0.27 s | 3.0x |
| playwright | 706 | 0.64 s | 0.15 s | 4.2x |
| lit `packages/react` | 6 | 2.12 s | 0.46 s | 4.6x |

CPU time:

| Project | Files | `tsc` 7.0.2 | `bun check` | Less |
| --- | ---: | ---: | ---: | ---: |
| vscode `src` | 9,795 | 32.1 s | 15.2 s | 2.1x |
| mikro-orm | 2,883 | 29.4 s | 11.2 s | 2.6x |
| next.js `packages/next` | 2,881 | 6.8 s | 2.8 s | 2.5x |
| next.js, root | 3,547 | 5.2 s | 4.2 s | 1.2x |
| storybook `scripts` | 1,039 | 4.8 s | 1.8 s | 2.6x |
| nuxt | 839 | 4.2 s | 1.7 s | 2.5x |
| playwright | 706 | 3.6 s | 1.3 s | 2.8x |
| lit `packages/react` | 6 | 2.8 s | 0.5 s | 5.6x |

Peak memory:

| Project | Files | `tsc` 7.0.2 | `bun check` | Less |
| --- | ---: | ---: | ---: | ---: |
| vscode `src` | 9,795 | 7.97 GB | 2.14 GB | 3.7x |
| mikro-orm | 2,883 | 6.67 GB | 1.35 GB | 4.9x |
| next.js `packages/next` | 2,881 | 1.81 GB | 0.71 GB | 2.5x |
| next.js, root | 3,547 | 1.33 GB | 0.55 GB | 2.4x |
| storybook `scripts` | 1,039 | 1.39 GB | 0.56 GB | 2.5x |
| nuxt | 839 | 1.14 GB | 0.51 GB | 2.2x |
| playwright | 706 | 1.06 GB | 0.46 GB | 2.3x |
| lit `packages/react` | 6 | 0.63 GB | 0.24 GB | 2.6x |

Measured with other work running on the machine (load average 6 to 13),
and with a release build of the type checker on its own, not a release
build of Bun. The `tsc` 6.0.2 number at the top is the better of 2 runs
on Node 25.6, on the same machine under the same load.

#### Long-running processes

`Bun.build({ check: true })` can run many times in one process. A test
runs repeated builds and checks that memory doesn't grow.

#### A first run

`bun init`, then `bun check --skipLibCheck false`, which also checks
every declaration file in `@types/bun`, `@types/node` and React against
the built-in `lib.*.d.ts` files:

| Template | Files checked | Errors |
| --- | ---: | ---: |
| `bun init -y` | 244 | 0 |
| `bun init --react` | 254 | 0 |
| `bun init --react=tailwind` | 256 | 0 |
| `bun init --react=shadcn` | 279 | 0 |

Projects that have an older `typescript` installed. `bun check` reports
the same errors as it does with the files of the TypeScript 7.0.2
package:

| Project | Installed `typescript` | Errors |
| --- | --- | ---: |
| dayjs | 2.9.2 | 0 |
| immer | 5.0.2 | 5 |
| svelte | 5.5.4 | 62 |
| zod | 5.5.4 | 31 |
| drizzle-orm | 5.6.3 | 10,845 |
| solid | 5.7.3 | 352 |
| vercel/ai | 5.8.3 | 1,561 |
| lit | 5.9.2 | 1,891 |
| apollo-client | 5.9.3 | 461 |

A test compares all 108 built-in files with the `typescript` package,
byte for byte.

Also compared with `tsc` 7.0.2, with the same output: 150 valid,
outdated, misspelled, empty and wrongly typed values in `tsconfig.json`.
Windows line endings, a byte order mark, UTF-16 and emoji in source
files. Paths with spaces, Cyrillic, Japanese and accents. A
`tsconfig.json` that extends 1 package, 2 packages or a missing one.
Both kinds of decorators. The config files that `create vite` writes. A
workspace whose packages import each other's sources.

`bun check | head -1` with 24,000 errors exits normally. Ctrl-C in a
terminal ends it in 35 ms with exit code 130.

#### What hasn't been tested

- The comparisons against `tsc` on real projects only ran on macOS
arm64. CI builds this for Windows x64 and arm64, macOS, Linux glibc and
musl, Android and FreeBSD, and runs `test/cli/check` wherever it runs
Bun's tests.
- ThreadSanitizer hasn't run on this version.
- Most of the `unsafe` code doesn't run under Miri.
- Only TypeScript 7.0.2. We haven't tried syncing to a newer
typescript-go.

## Known problems

Where it differs from `tsc` 7.0.2:

- typescript-go's answer for a file can depend on which other files are
in the program, in what order, and on how many checkers it uses. `bun
check` gives the same answer at any thread count, and everything here is
compared against typescript-go with 1 checker. It shows in 1 of the
1,103 configs above: in vuejs/core's `packages-private/tsconfig.json`, 1
of typescript-go's 940 errors is missing, a TS2345. Reduced to 3 files,
typescript-go reports that error with `"files": ["global.ts",
"model.ts"]` and doesn't with `["model.ts", "global.ts"]` or
`["model.ts"]`. Neither file imports the other.
- In rxjs, 7 of 22,030 messages explain the error along a shorter path:
`The types returned by '[bufferTime](...)[groupBy]' are incompatible`
where typescript-go has `'[bufferTime](...)[bufferTime](...)[groupBy]'`.
The position, the code and the first line of the message are the same.
It has the same cause: the order the files are checked in.
- nuxt with `strict` off: 7 of typescript-go's 74 errors are missing,
and there are 5 that it doesn't report.
- Single-quoted strings in `package.json` are accepted. typescript-go
rejects them.
- `interface Foo extends Partial<Foo>`, or any other mapped type of
`Foo`, is an error in both (TS2310). What `tsc` says after it depends on
what's asked for first, `Foo` or `Partial<Foo>`. Within a file `bun
check` says the same. Across files it can differ, because `tsc` goes by
the order of the whole program. With `skipLibCheck` off, an interface of
the library that's extended like that can have members in `bun check`
that it doesn't have in `tsc`.
- More than 332 variables without annotations in one loop, each
initialized with the one before it: `bun check` says it couldn't finish
the file and exits with code 1.
- On 254 of the small programs written while reading the two side by
side, the output differs. 1 is ordinary code: after `if (t === null || t
=== undefined) return;` on a generic `t: T`, `for (const [k, v] of
Object.entries(t)) v.foo` gets TS18046, `'v' is of type 'unknown'`,
where `tsc` accepts it. 38 are in unusual code or unusual command lines.
For example, `function f({ a = 1, b = g(a) }) {}` gets a false TS7022,
and in `enum E { A = 1, B = next(E.A) }` with `function next(e: E)` a
false TS2502. In a directory with a `jsconfig.json` and no
`tsconfig.json`, `bun check` checks that project where `tsc` goes on to
the parent's `tsconfig.json`. The other 215 are next to another error,
without the default library, or in code nobody writes on purpose.
- The order of the members of a union of anonymous object types in a
message can be different.
- A type alias that refers to itself without end gets TS2589 from both,
but it can be at another position, and a TS2538 next to it can be
missing. That's 28 of 517 generated programs.
- The lines under an error that explain it can differ: 8 of 2,925 errors
after breaking Effect's source.
- `for (!function () { n in e; }(); ;) break;` is a syntax error in
typescript-go 7.0.2. It isn't in TypeScript 6 or here. Minified jQuery 3
has that code.

This adds 5.94 to 7.21 MB to the binary, depending on the platform.

None of this would exist without typescript-go. It's the code this is
ported from, the baselines that say what the type of every expression
is, and a binary to compare against on any project.

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Follow-up to oven-sh#44361. Fixes cases where `bun check` and `tsc` 7.0.2 do
different things.

### `bun check` checks what `tsc` checks

`bun check <args>` now reports the same projects as `tsc <args>
--noEmit`.

| In a project with `references` | `tsc` | `bun check` before | `bun
check` now |
|---|---|---|---|
| no flags | its own files | its own files and every referenced project
| its own files |
| `-b` | every referenced project | every referenced project | every
referenced project |
| `--strict` and other compiler options | apply to the project | applied
to every referenced project | apply to the project |

Referenced projects are still read from their source files, so nothing
has to be built first.

Where `tsc` checks nothing, `bun check` does something useful and says
so:

| | `tsc` | `bun check` |
|---|---|---|
| a `tsconfig.json` with `"files": []` and `references`, like `create
vite` writes | checks nothing, exits 0 | follows the references and
prints a note |
| no `tsconfig.json` here or above, some below | prints its help | lists
them, with the command for one and for all |
| `bun check src/index.ts` or `bun check .` next to a `tsconfig.json` |
TS5112 | checks those files with that `tsconfig.json` |

`-b` is in `bun check --help` and in the docs now.

### False errors

**`ReturnType` of a generic method that returns a graphql-zeus thunk.**
`tsc` resolves it to `any`. `bun check` resolved it to the precise
function type, so every vitest mock of that method was an error:

```ts
class Client {
  zeus<O extends "query" | "mutation">(op: O) {
    return Thunder(fetchFn)(op, { scalars });
  }
}
let client: MockedObject<Client>;
client.zeus.mockImplementationOnce(() => () => Promise.resolve({ viewer: { login: "" } }));
//                                 ~~ TS2345, not reported by tsc
```

**`!` on a type parameter:**

```ts
function f<T extends string | undefined>(x: T) {
  return x!.toUpperCase(); // TS2339
}
```

**A variable assigned from a callback in a loop:**

```ts
let last: string | null = null;
for (const n of names) {
  if (n === last) continue;
  last = run(() => n); // TS7024
}
```

**A computed key in an object literal passed to a generic function**
(ts-pattern):

```ts
declare function f<P extends Record<string, (value: number) => void>>(p: P): void;
f({ [key]: value => {} }); // TS7006 at `value`
```

Also: two union variables narrowed in one loop, a type parameter
narrowed by `=== null` and passed to `Object.entries`, `export =` next
to other exports across files, a tuple assigned inside a loop, an index
signature of a generic interface with two base types, and a generic
method of `C<any>` whose parameter is `T & F<U>`.

### Crashes and hangs

| Program | Before |
|---|---|
| a union of 21 or more members, two of them aliases with one name |
abort |
| `if {} else if {} else ..` 6,000 times | stack overflow |
| a ring of 101 or more import aliases | hang |
| index signatures nested 40 deep | time doubled with every level |
| a dependency whose `exports` is nested 8,000 deep | stack overflow |
| a parameter pattern with defaults on several levels whose leaves are
functions | exponential time |

### Printed types

An imported generic alias is the same type as in the file that declares
it. `Rule<T> | Message<T>` (react-hook-form) prints as `Rule<number>`,
like in `tsc`.

### Case-sensitive file systems

`tsc` ignores case in a few path comparisons on every system, Linux
included: the suggestion of TS5102, TS5011, TS6059 and `--explainFiles`.
`bun check` asked the file system.

### The command line

| | `tsc` | `bun check` before |
|---|---|---|
| `--quiet` | prints no diagnostics | printed them |
| `@args.txt` | reads arguments from the file | took it for a path |
| `--strict null` | unsets the option | rejected it |
| `-b missing` | TS6053 | another error |
| `-p dir` without a tsconfig.json | TS5081 | TS5057 |

### Review findings

- Text of yours that looks like a path with a drive, like `"/c:/x"` in a
string literal type or a module specifier, is printed as you wrote it.
- A project in a directory like `/c:/work` or `/^/work` on macOS and
Linux is read from there.
- A private member that is only read from a file earlier in the program
is not reported as unused.
- After `Bun.build({ check: true })`, every thread of the pool gives its
free pages back to the system, the I/O threads too.

### How it was verified

| | |
|---|---|
| TypeScript's own tests, errors | 13,101 of 13,101, byte for byte |
| TypeScript's own tests, types | 12,463 of 12,463 |
| TypeScript's own tests, symbols | 12,463 of 12,463 |
| TypeScript's own tests, declaration files | 13,032 of 13,032 |
| Small programs that once differed, run through both tools in CI |
1,807, 105 of them new |
| The same programs on a case-sensitive volume | 1,807 the same |
| 7 project layouts x 8 command lines, run through both tools in CI |
same wherever `tsc` checks something |
| tsconfigs of 70 open source projects with the same errors as `tsc` |
636 of 637 |
| tsconfigs with project references, same lines in the same order as
`tsc -b` | 316 of 317 |
| Instructions executed on 8 projects (VS Code, mikro-orm, next.js
twice, storybook, nuxt, playwright, lit), against main | between 6.6%
fewer and 0.7% more |

Every fix has its program in `test/cli/check/differential-cases.json`.
The test runs `tsc` and `bun check` on each and compares the whole
output.
Follow-up to oven-sh#44665. Fixes more cases where `bun check` and `tsc` 7.0.2
report different things.

### False errors

**A named union where one member extends another.** `c ? a : b` had the
type of the base member alone:

```ts
interface Animal { name: string }
interface Dog extends Animal { bark(): void }
type Pet = Animal | Dog;
declare const a: Pet, b: Pet, c: boolean;

const x = c ? a : b;       // Pet in tsc, was Animal
if ("bark" in x) x.bark(); // TS18046, not reported by tsc
```

The same for `[a, b]`, `a ?? b` and `a || b`.

**A class that passes a conditional type of itself to its base class:**

```ts
type Json<T> = T extends Primitive ? T : { [K in keyof T]: Json<T[K]> };

class User extends Model<Json<User>> { // TS2310
  name = "";
}
```

**A rest element in a loop that assigns from it:**

```ts
let item: Item | undefined = start;
while (item) {
  const { id, ...rest } = item; // TS7022 at `rest`
  item = rest.parent;
}
```

**A default in a destructured parameter that uses an earlier name of the
pattern:**

```ts
function f({ a = 1, b = g(a) }) {} // TS7022 at `a`
```

**A `switch` over a destructured variable in a loop:**

```ts
for (let i = 0; i < 2; i++) {
  const [m, n] = [mode, 1]; // TS7022 at `m`
  switch (m) {
    case "r":
      mode = "w";
      break;
  }
}
```

**Two comparisons that look like type arguments:**

```ts
g(x as number < b, c > (d)); // TS2349, TS2749
```

Also: a file named `x.TSX` or `z.JSX` is parsed as TSX, `{ [await (p)]:
await (q) }` at the top level of a module, a decorator of an inner class
that uses a private name of the outer class, a mixin over `Base<number>`
with an optional property under `target: es2022`, overloads whose
parameter type is the `ReturnType` of a function that calls them, and
`type F = (Foo) => void` counts as a use of `Foo` under
`noUnusedLocals`.

### Missed errors

**An imported type that is only used as a value** is unused:

```ts
import { Event } from "./t"; // TS6133. `Event` is an interface there
new Event("x");              // this one is the global
```

**A recursive comparison went one level less deep** if a class above it
extends the target:

```ts
declare class K<D> { d: D; e(): K<[D]>; x(): string }
declare class S<D> { d: D; e(): S<[D]>; x(): D extends [[any]] ? number : string }
declare class R extends K<number> {}
declare function mk<X>(): S<X>;

const k: K<number> = mk<number>(); // TS2322
```

Also: a reserved word used as a name in the children or attributes of a
JSX element, `enum E { B }` that continues a `declare enum E` from a
`.d.ts` under `skipLibCheck`, a getter after a generic spread is
read-only, `@augments` that names another class than `extends` in a
TypeScript file, a checked JavaScript file named `y.JS` or `m.MJS`, and
TS2875 for `App.TSX` under `jsx: react-jsx` without the runtime.

### A tsconfig.json that cannot be read

| | `tsc` | `bun check` before |
|---|---|---|
| `tsconfig.json` cannot be read | TS5083, nothing is checked | TS5083,
and checked everything below with default options |
| the same with `-b` | TS6053 | TS5083, and checked everything below |
| a referenced `tsconfig.json` cannot be read | TS6053 at the reference
| TS6306 |

### How it was verified

| | |
|---|---|
| TypeScript's own tests, errors | 13,101 of 13,101, byte for byte |
| TypeScript's own tests, types | 12,463 of 12,463 |
| TypeScript's own tests, symbols | 12,463 of 12,463 |
| TypeScript's own tests, declaration files | 13,032 of 13,032 |
| Small programs that once differed, run through both tools in CI |
1,931, 124 of them new |
| The same programs on a case-sensitive volume | 1,931 the same |
| The new programs in a debug build | 124 the same |
| tsconfigs of 70 open source projects with the same errors as `tsc` |
636 of 637 |
| tsconfigs with project references, same lines in the same order as
`tsc -b` | 316 of 317 |
| Instructions executed on 9 projects (VS Code, mikro-orm, next.js
twice, storybook, nuxt, playwright, xstate, drizzle-orm), against main |
between 0.8% fewer and 1.1% more, 0.5% more on average |

The first example, the recursive comparison and the unused import were
found by breaking the source of 47 open source projects in 6,399 small
ways, comparing every error of both tools, and reducing what differed.

Every fix has its program in `test/cli/check/differential-cases.json`.
The test runs `tsc` and `bun check` on each and compares the whole
output.
…with the buffer (oven-sh#38003)

### Problem

- A native byte stream first read after its producer failed ends clean
after 0 bytes. Sources: `fetch` body, `req.body`, `S3File.stream()`,
`HTMLRewriter` output. `Bun.serve` answers an empty `200`. An S3 copy
stores an empty object.
- If a reader took part of the buffer, the process aborts: `panic: range
end index 262144 out of range for slice of length 0`
(`ByteStream::take_buffer`).
- Cause: `ByteStream::append(Err)` stores the error, drops `buffer` and
keeps `offset`. `on_start`, `to_any_blob` and `Bun.serve` read that as
"ended, empty".

### Fix

- A stored error makes `on_start` return `Start::Ready` and
`to_any_blob` return `None`, so the first pull delivers it. `Bun.serve`
hands it to its sink, and `end_chunk` calls `error()`.
- `buffer` and `offset` become one `Buffered` value. Every method that
empties the bytes resets the index.
- Verified: six test files (Notes). 24 new tests fail without the
change.
- Self-reviewed: 2 concerns raised, 2 addressed.

### Background

- `ByteStream` is the native source of a streamed body.
- A pull copies into a view. Bytes that do not fit stay buffered.
`offset` counts the ones already taken.
- Weighed a store in `append(Err)` and a clamp in `take_buffer`. Six
sites empty the buffer, and a clamp hides a wrong index.

### Downsides

- A handler that returned such a body got a silent `200`. It now gets
`error()` or the default 500.
- One test of `pending.result` per stream start and per `Bun.serve`
attach. Release `text`: +768 bytes.
- Still open: `Bun.write(path, stream)` rejects after it truncated the
file. A started body that did not fail loses its queued bytes (oven-sh#33461,
oven-sh#42281).

<details><summary>Notes</summary>

**Origin and reach.** No user reported either symptom. A fuzzer found
the panic, and a differential run against node v26.3.0 found the empty
body. The panic ships since 1.4.0: oven-sh#38656 gave `take_buffer` its
`drain(..consumed)`. The review thread at
oven-sh#43410 (comment) named
the stale `offset` on 09-19 and got the answer that the crash would have
its own change with its own test. This is that change, inside this PR so
that `ByteStream.rs` has one pull.

**Three routes reach `take_buffer` with an index left behind an emptied
buffer.**

1. `Bun.serve` renders a `Source::Bytes` body that a consumer started
and let go (`res.body.getReader().releaseLock()`), after the producer
failed. `do_render_with_body` calls `drain()`.
2. `Readable.fromWeb()` on such a stream. `on_start` took the buffer and
kept the index, then `native-readable.ts` calls `drain()`. This one
needs no failure.
3. The stream's own pull loop in text mode (`res.textStream()`). The
adapter keeps its pull view there, so `nativeSourcePullImpl`
(`BunStreamSource.cpp`) calls `handle.drain()` before each pull. A body
that fails after a partial read is enough. No second consumer is
involved.

Sites that emptied `buffer` and kept `offset` on main: `append(Err)`,
the buffer-action error arm of `on_data`, `on_cancel`, the stored-error
arm of `to_buffered_value`, the `OwnedAndDone` arm of `on_start`,
`to_any_blob`. `Buffered` offers `take`, `clear`, `copy_out`, `adopt`
and `extend`, and nothing else writes the two fields.

**Before and after** (release build of main, debug build of this
branch):

| script | main | this PR |
| --- | --- | --- |
| `HTMLRewriter` output, probed with `getReader().releaseLock()`,
400,007 bytes of output, then a handler throws, returned from
`Bun.serve` | panic, exit 134 | `500`, `error()` got `boom` |
| the same with exactly one pull view of output (262,144 bytes) | `200
""` | `500`, `error()` got `boom` |
| the same after only `void res.body` | `200 ""` | `500`, `error()` got
`boom` |
| `const body = res.body`, upstream resets, `for await (const c of
body)` | ends cleanly after 0 of 100,000 bytes | rejects `ECONNRESET`
(node: rejects) |
| `res.textStream()` reader, one view read, upstream resets, next read |
panic, exit 134 | the read rejects `ECONNRESET` |
| `Readable.fromWeb()` on a probed rewriter output, clean end | panic,
exit 134 | the bytes the native source still holds |
| `req.body` read after the uploading client went away | `{ done: true
}` | rejects `AbortError` |
| `new Response(body).arrayBuffer()` / `bytes()` / `blob()` / `json()`
on a body that failed while idle | resolve empty | reject `ECONNRESET` |
| `new Bun.S3Client({}).file(key).stream()` (no credentials), then `for
await`, a reader, `pipeTo`, `tee`, `new Response(s).bytes()` or
`.blob()` | end cleanly after 0 bytes | reject
`ERR_S3_MISSING_CREDENTIALS` |
| `client.file(key).stream()` read after the download failed: a body cut
short, a missing key (404), a denied key (403) | ends cleanly after 0
bytes | rejects `ConnectionClosed`, `NoSuchKey`, `AccessDenied` |
| `client.write(key, new Response(s))` or `Bun.write(s3file, new
Response(s))` with such a stream `s` | resolves 0, a `PUT` of 0 bytes
replaces the object | rejects, no request goes out |
| `fetch(url, { method: "POST", body: s })` | resolves 200, the server
gets a complete POST with `Content-Length: 0` | rejects, the server gets
no complete request |
| `HTMLRewriter` output `.body` taken, a handler throws or the upstream
resets, then the first read | `{ done: true }` | rejects with the
handler's or the upstream's error |

**Four sources, one script.** Each transfer announces 100 bytes, sends
50, and is cut by a FIN 20 ms later. The stream is taken at once and
read 100 ms later: `(await fetch(url)).body`, `req.body` in `Bun.serve`,
`s3.file(key).stream()`, `new HTMLRewriter().transform(await
fetch(url)).body`. A release canary (367d939) and a debug build of
main bd599f5 end all four cleanly with 0 bytes, 3 of 3 runs each. A
debug build of this branch rejects all four, 3 of 3 runs, with the error
the same read gets when it is made at once: `ECONNRESET`, `AbortError`,
`S3Error ConnectionClosed`, `ECONNRESET`.

**What stays open.** For a stream source, `Bun.write(path, source)`
opens the destination with `O_TRUNC` before it reads the stream
(`pipe_readable_stream_to_blob`, `Blob.rs:1448`). With a failed stream,
or a `Response` or `Request` that wraps one, the write now rejects, and
a file that held 777 bytes holds 0. Main does the same for a bare
stream, and resolved 0 for the wrappers. Only `Bun.write(path, res)`
with the fetch `Response` itself keeps the old bytes. An S3 destination
keeps the old object, because no request goes out. This PR does not
change when `Bun.write` truncates.

Two behaviours change only in the state that used to abort: `on_start`
and `resume` hand over the bytes no consumer took, not the whole buffer,
so a prefix a reader already holds is not sent twice. The `to_write ==
0` arm of `on_pull` is gone: it handled a buffer whose bytes were all
taken but not dropped, which cannot exist now.

The `HTMLRewriter` paces its input by the whole buffer, the taken prefix
included (oven-sh#38656). `Buffered::held_len()` keeps that. Counting only the
untaken bytes made `html-rewriter.test.js` "a locked reader paces the
input by its reads" fail, which is how the difference showed.

**Self-review.** Two concerns survived: that the change may claim
severity but not demand (so this body states that no user reported it
and lists the routes), and that a rename in `ByteStream.rs` races the
other open pulls on the file (it now ships inside this one).

**Since the first revision of this PR.** Merged up to main. The
`Blob.rs` hunk is gone: main rewrote `pipe_readable_stream_to_blob` and
marks the pump's rejected promise handled there.
`s3-write-to-file-sync-close.test.ts` now asserts the rejection instead
of accepting both outcomes. `end_sink_with_pending_error` uses main's
`detach_sink` plus `sink.end`. The fixture variants sit in main's
`nativeBodies` table (oven-sh#40596). The "failed after attach" control is
dropped: main's `proxied-fetch-after-status` row covers it with oven-sh#40596's
contract (the connection closes without a complete message). One variant
is new: the probed rewriter output from the first row above.

**Tests.** `bun bd test` on Linux x64:

- New or ported, failing without the change (24):
`serve-stream-body-error.test.ts` (8), `body-mixin-errors.test.ts` (9),
`bun-server.test.ts` (1), `fetch.stream.test.ts` (2),
`s3-stream-error-gc.test.ts` (2), `html-rewriter.test.js` (2). Each was
run against a debug build of main bd599f5, the first file against the
release canary.
- `s3-write-to-file-sync-close.test.ts` passes with and without the
change: main already rejects that write, and the test now pins it.
- After the rebase onto main (bd599f5): `serve-stream-body-error` 34
pass, `body-mixin-errors` 23, `s3-stream-error-gc` 4,
`s3-write-to-file-sync-close` 1, `html-rewriter.test.js` 188,
`bun-server` 81, `fetch.stream` 121.
- This container gives the debug build about two cores. `bun-server` and
`fetch.stream` were run with `--max-concurrency` lowered: with the
default, their concurrent subprocess tests pass the 5 s limit, with and
without the change. The "Content-Length response works (multiple parts)"
rows of `fetch.stream` take 2.1 s to 4.9 s each on both builds, and in
one run two of them passed the limit.
- Earlier, before the rebase: `fetch-backpressure` 127, `body-stream`
9086, `body` 774, `streams` 624, `node-stream` 112. `serve.test.ts`: 325
pass. "root range port" and "/bun:info to loopback" fail on the release
build of main too (the container runs as root).

`bytestream-partial-read-error-fixture.ts` stages the failure with an
`HTMLRewriter` whose handler throws on the second chunk, so every step
runs on the JS thread in order. It waits for the handler to have run,
checks that the first read is exactly one pull view, and requires the
next read to reject with the handler's own error. A text-mode reader is
required: a byte reader's full view retires the adapter's pending view,
and the next pull then skips `drain()`. Against the released bun the
fixture aborts with the panic in its header.

`s3-stream-stored-error-fixture.ts` has two modes. With no credentials
the download fails inside `stream()`, so every reader and sink runs
against a stored failure and no timing is involved. For the failures on
the wire, the stub closes the connection behind each answer. The read
starts after the client has closed its end and one more request has made
the round trip through the HTTP thread. The fixture writes to an
absolute path: `Bun.write("out.bin", new Response(""))` with a relative
path to a file that does not exist rejects with `ENOENT` from
`truncate`, on main too. That is a separate defect.

**Size.** `size` of the release binary: `text` 80,676,945 on main,
80,677,713 with the change. `data` and `bss` are equal. Measured before
the rebase.

**History.** The branch is rebased onto main at bd599f5: one commit for
the change, and one for the tests of the S3 and `HTMLRewriter` sources.

**Related open pulls.** oven-sh#33461 and oven-sh#36110 give opposite answers for a
started body that did not fail (send everything, or refuse). oven-sh#42281
keeps `Readable.fromWeb()` from taking a started native source. None of
them touches `ByteStream.rs`. oven-sh#43410 rewrites how `append(Err)` stores a
JS error and needs a rebase after this.

</details>

<!-- robobun:evidence:begin -->

---

**no test proof** · iteration 0 · platform-specific test(s) that do not
run on this machine, deferring to CI, which covers all platforms:
test/js/workerd/html-rewriter.test.js,
test/js/web/fetch/fetch.stream.test.ts,
test/js/bun/http/bun-server.test.ts

<!-- robobun:evidence:end -->
…re (oven-sh#43750)

### Problem
- With React Fast Refresh on, `const [a, setA] = useState(0)` and `const
[b, setB] = useState(0)` both print `_s(C, "9DqC+93o+Nw=")`. React keeps
the state across the rename. `react-refresh/babel` resets it.
- `visit_decls` (`src/js_parser/visit/mod.rs:369`) hashes the binding
only when `react_refresh.last_hook_seen` is the initializer call.
Nothing sets that field.
- `B::write_to_hasher` (`src/ast/b.rs:139`) writes the length of an
identifier name, not the name.

### Fix
- `handle_react_refresh_hook_call` records the call in `last_hook_seen`,
after the hook context exists, so the `unwrap`s in `visit_decls` hold.
`B::write_to_hasher` writes the name bytes.
- `Data::write_to_hasher` recursed with no bound: `useState(x+x+…)` with
100,000 terms is a segmentation fault on bun 1.4.3, and a binding
default now reaches it. It now walks a binary chain on the heap, like
the visitor and the printer. A `StackCheck` guards the other recursion.
- Verified: `test/bake/dev/react-spa.test.ts`. Both new tests fail on
bun 1.4.3. Also `25716.test.ts` and `bundler_jsx.test.ts`.
- Self-reviewed: 6 concerns raised, 3 addressed, 3 rejected (see Notes).

### Background
- The transform wraps each function that calls hooks: `_s(Component,
"<signature>")`. If an edit changes the signature, React remounts the
component and its state resets.
- Bun feeds a per-function Wyhash in the visit pass: the name of each
`/^use[A-Z]/` call, and the initial state argument of `useState` and
`useReducer`.
- `B` is the AST node of a binding pattern: identifier, array or object.

### Downsides
- A rename or reshape of a destructured hook result now resets the state
of that component, as with Babel. `const { data } = useQuery()` to
`const { data, error } = useQuery()` remounts.
- Under a TypeScript wrapper (`useState(0) as T`) Bun hashes the binding
and Babel does not. A rename there resets in Bun only.

<details><summary>Notes</summary>

**Upstream.** `react-refresh/babel` builds the key as `useState{[a,
setA](0)}`: hook name, source text of the declarator id, initial state
(`ReactFreshBabelPlugin.js:422-436`). React tests it: "resets state when
renaming a state variable" (`ReactFreshIntegration-test.js:593`). The
Zig sources had the same two gaps since the transform landed in oven-sh#13215.

**No remount on upgrade.** Signatures are not stored. A hook call that
is not a whole initializer keeps its signature (`useState(0);` stays
`9DqC+93o+Nw=`).

**Which declarations take the binding.** Only a hook call that is the
whole initializer: `const x = useA()`, `const [a, b] = useA()`, `const {
a: b } = useA()`, each declarator of `const a = useA(), b = useB()`, and
the outer call of `const x = useA(useB())`. `const x = [useA()]`, `const
x = foo(useA())`, `const x = await useA()` and `x = useA()` do not, as
in Babel (`path.parent.type === 'VariableDeclarator'`,
`ReactFreshBabelPlugin.js:423`).

**Why the `unwrap`s hold.** `visit_decls` sets `last_hook_seen` to
`None` before it visits each initializer.
`handle_react_refresh_hook_call` sets it only after it created the
`HookContext` of the current function. A hook call outside a function
returns before that. The value is compared by address only. It is never
read through. Equal addresses mean the call was handled during the visit
of this initializer, with no function boundary between, so the hook
context is the same one.

**Hash bytes.** Per hook call: name, initial state argument, `\0`, then
the binding. An identifier binding is tag, length, name. The first byte
after `\0` is a binding tag (0 to 2) or the `u` of the next hook name,
so the stream is not ambiguous.

**Checks.**
- Suites on the debug (ASAN) build: `test/bake/dev/react-spa.test.ts` (8
pass), `test/bake/dev/hot.test.ts` (11 pass),
`test/regression/issue/25716.test.ts` (2 pass),
`test/bundler/bundler_jsx.test.ts` (57 pass, 5 todo).
- 78 `.tsx`/`.jsx` fixtures from `test/` through `bun build
--react-fast-refresh --external '*'` with bun 1.4.3 and with this branch
(debug, ASAN). Exit codes are equal. The outputs are identical after the
signature strings are masked. 50 files have a new signature. The 28
files with only unassigned hook calls keep theirs.
- A differential run against `react-refresh/babel` over the 42
render/patch pairs of upstream `ReactFreshIntegration-test.js` (69
function/pair rows). Rows that disagree with Babel: 21 on main, 15 on
this branch. No row regresses. The 6 fixed rows are the "resets state
when renaming a state variable" tests (plain, in a HOC, with
indirection, direct call), "does not get confused when custom hooks are
reordered" and "does not get confused when component is called early".
- Signatures are equal with and without `--minify`, `--minify-syntax`,
`--minify-identifiers` and `--react-compiler`.
- Deep input (debug, ASAN): a 100,000 term chain as `useState` argument,
`useReducer` argument, binding default and computed key builds, and the
whole chain is hashed. Two chains that differ only in the first term get
different signatures. With the stack check alone they got the same one,
because the hash stopped part way down. A 100,000 deep `x.a.a.a…`
argument gives `error: Maximum call stack size exceeded` from the visit
pass. bun 1.4.3 gives a segmentation fault for the argument cases.
Nested binding patterns stop at the parser limit with the same error,
below the depth where the hash could overflow.
- The heap walk writes the same bytes as the recursion did. A function
with nine unassigned hook calls over mixed binary expressions (left
deep, right deep, `??`, comma, assignment, `**`, unary) gives
`OUTRQVtcIug=` on bun 1.4.3 and on this branch.

**Self-review, rejected concerns.**
- TypeScript wrappers (`as`, `!`, `satisfies`): the parser drops the
wrapper, so Bun hashes the binding where Babel does not. A rename is
still a rename. A reset is the safe direction.
- `for (let i = useA(); ; )`: the binding is not hashed, because
`visit_for_loop_init` does not go through `visit_decls`. A hook in a
loop header breaks the rules of hooks.
- An initializer that folds to a hook call (`null ?? useA()`, `false ?
useB() : useA()`) takes the binding only when the surviving call was
visited last. These are conditional hooks.

**Not changed here (the same on main).**
- The initial state is hashed by tag only for objects, arrows, calls,
templates, `new`, JSX and conditionals. `useState({ count: 0 })` to
`useState({ count: 1 })` keeps the state.
- `// @refresh reset` has no effect (`force_reset` is never set).
- `.ts` files get no signatures, so a custom hook in a `.ts` file is not
tracked.
- `useA?.()` counts as a hook call in Bun, not in Babel.

**Bugs found outside this change.** No open PR or issue covers them.
- `bun build --react-fast-refresh` on `export default function App() {
const [a] = useState(0); return a; }` prints `const App = _s(function
App() { … }, "…"); var App = App;`. The output does not parse:
`SyntaxError: Identifier 'App' has already been declared`. The dev
server path is not affected.
- A class method that calls a hook (`class K { m() { useState(1) } }`)
panics in `lower_class` (`src/js_parser/p.rs:7399`, `internal error:
entered unreachable code`) for `.ts` and `.tsx`. For `.jsx` it prints
`m: _s(function() { … }, "…")` inside the class body, which does not
parse.

</details>

<!-- robobun:evidence:begin -->

---

**[human-review]** gate passed · iteration 0 · 4 files touched

<details><summary>fails on main (without fix)</summary>

```console
ASAN without fix: 2 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/bake/dev/react-spa.test.ts
bun test v1.4.3 (367d939)

test/bake/dev/react-spa.test.ts:
Dev server testing directory: /tmp/bun-dev-test-mYwZnq
bun install v1.4.3 (367d939)

+ react@19.2.4
+ react-dom@19.2.4

3 packages installed [120.00ms]
�[0;30mdev|�[0m Started development server: http://localhost:41787
�[0;30mdev|�[0m �[32mBundled page in 1992ms�[0m�[2m:�[0m index.html
�[0;30mweb|�[0m [I] [Bun] Hot-module-reloading socket connected, waiting for changes...
�[0;30mdev|�[0m �[32mReloaded in 53ms�[0m�[2m:�[0m App.tsx
�[0;30mweb|�[0m [I] [Bun] Hot-module-reloading socket connected, waiting for changes...
(pass)  DEV:react-spa-1: react in html [6086.87ms]
�[0;30mdev|�[0m Started development server: http://localhost:44675
�[0;30mdev|�[0m �[32mBundled page in 90ms�[0m�[2m:�[0m index.html
�[0;30mweb|�[0m [I] [Bun] Hot-module-reloading socket connected, waiting for changes...
�[0;30mdev|�[0m �[32mReloaded in 46ms�[0m�[2m:�[0m App.tsx
�[0;30mdev|�[0m �[36m[x2]�[0m �[32mReloaded in 51ms�[0m�[2m:�[0m App.tsx
(pass)  DEV:react-spa-2: react
... (truncated)

release without fix: 2 FAILED
bun test v1.4.3-canary.1 (367d939)

test/bake/dev/react-spa.test.ts:
Dev server testing directory: /tmp/bun-dev-test-DwNaaK
bun install v1.4.3-canary.1 (367d939)

+ react@19.2.4
+ react-dom@19.2.4

3 packages installed [3.00ms]
�[0;30mdev|�[0m Started development server: http://localhost:41197
�[0;30mdev|�[0m �[32mBundled page in 51ms�[0m�[2m:�[0m index.html
�[0;30mweb|�[0m [I] [Bun] Hot-module-reloading socket connected, waiting for changes...
�[0;30mdev|�[0m �[32mReloaded in 2ms�[0m�[2m:�[0m App.tsx
�[0;30mweb|�[0m [I] [Bun] Hot-module-reloading socket connected, waiting for changes...
(pass)  DEV:react-spa-1: react in html [1225.24ms]
�[0;30mdev|�[0m Started development server: http://localhost:38837
�[0;30mdev|�[0m �[32mBundled page in 3ms�[0m�[2m:�[0m index.html
�[0;30mweb|�[0m [I] [Bun] Hot-module-reloading socket connected, waiting for changes...
�[0;30mdev|�[0m �[32mReloaded in 1ms�[0m�[2m:�[0m App.tsx
�[0;30mdev|�[0m �[36m[x2]�[0m �[32mReloaded in 1ms�[0m�[2m:�[0m App.tsx
(pass)  DEV:react-spa-2: react refresh should register and track hook state [691.80ms]
�[0;30mdev|�[0m Started development server: http://localhost:35519
�[0;30mdev|�[0m �[32mBundled p
... (truncated)
```

</details>

<details><summary>passes on PR (with fix)</summary>

```console
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/bake/dev/react-spa.test.ts
bun test v1.4.3 (367d939)

test/bake/dev/react-spa.test.ts:
Dev server testing directory: /tmp/bun-dev-test-Kx2Mrh
bun install v1.4.3 (367d939)

+ react@19.2.4
+ react-dom@19.2.4

3 packages installed [87.00ms]
�[0;30mdev|�[0m Started development server: http://localhost:44623
�[0;30mdev|�[0m �[32mBundled page in 1288ms�[0m�[2m:�[0m index.html
�[0;30mweb|�[0m [I] [Bun] Hot-module-reloading socket connected, waiting for changes...
�[0;30mdev|�[0m �[32mReloaded in 34ms�[0m�[2m:�[0m App.tsx
�[0;30mweb|�[0m [I] [Bun] Hot-module-reloading socket connected, waiting for changes...
(pass)  DEV:react-spa-1: react in html [4819.43ms]
�[0;30mdev|�[0m Started development server: http://localhost:33215
�[0;30mdev|�[0m �[32mBundled page in 73ms�[0m�[2m:�[0m index.html
�[0;30mweb|�[0m [I] [Bun] Hot-module-reloading socket connected, waiting for changes...
�[0;30mdev|�[0m �[32mReloaded in 31ms�[0m�[2m:�[0m App.tsx
�[0;30mdev|�[0m �[36m[x2]�[0m �[32mReloaded in 33ms�[0m�[2m:�[0m App.tsx
(pass)  DEV:react-spa-2: react 
... (truncated)

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     9a553d5
  features     lto, baseline

23 deps, 136 codegen, 1176 objects in 932ms

ninja: Entering directory `/workspace/bun/build/release'
[1/4] fetch lolhtml
[lolhtml] up to date
[2/4] fetch rust-argon2
[rust-argon2] up to date
[2/4] cargo plan → /workspace/bun/build/release/rust-target/plan.json
244 units: 172 lib, 16 proc-macro (host), 19 custom-build (host), 15 run custom-build, 17 lib (host), 4 run custom-build (host), 1 rlib
[3/4] reconfigure
[1/1499] mkdir stamps
[2/1499] mkdir codegen
[3/1499] rustc unicode_ident 
[4/1499] install /workspace/bun
bun install v1.4.3-canary.1 (367d939)

Checked 26 installs across 65 packages (no changes) [104.00ms]
[5/1499] gen bake.{client,server,error}.js
-> bake.client.js, bake.server.js, bake.error.js
[6/1499] install /workspace/bun/packages/bun-error
bun install v1.4.3-canary.1 (367d939)

Checked 1 install across 2 packages (no changes) [1.00ms]
[7/1499] rustc unicode_xid 
[8/1499] install /workspace/bun/src/node-fallbacks
bun instal
... (truncated)
```

</details>

<details><summary>diff hotspot</summary>

```
src/ast/b.rs                    |   1 +
 src/ast/expr.rs                 |  78 +++++++++++++++++----
 src/js_parser/p.rs              |   2 +
 test/bake/dev/react-spa.test.ts | 150 ++++++++++++++++++++++++++++++++++++++++
 4 files changed, 219 insertions(+), 12 deletions(-)
```

</details>

**gate history** · 1 passed · 0 rejected · iteration 0

<details><summary>evidence per changed file</summary>

```
file                             reads  edits  tests
src/ast/b.rs                         2      1     25
src/ast/expr.rs                      4      2     25
src/js_parser/p.rs                   2      2     25
test/bake/dev/react-spa.test.ts      3      1     25
```

</details>

<!-- robobun:evidence:end -->
…`, and the cost of copies of a package (oven-sh#44726)

Follow-up to oven-sh#44685. Fixes nine kinds of program on which `bun check`
reported an error and `tsc` 7.0.2 reports none, one on which it missed
an error, long runs of `!`, and the cost of several copies of one
package.

Fixes oven-sh#44706. Fixes oven-sh#44730. Fixes oven-sh#44729. Fixes oven-sh#44689. Fixes oven-sh#44727.
Fixes oven-sh#44660. Fixes oven-sh#44686. Fixes oven-sh#44656.

### A narrowed `NoInfer` value spread inside a generic call

`bun check` left the spread's properties out of the object and then
reported TS2322.

```ts
type Item = { done: boolean; title: string | undefined };
declare function useData<T>(select: () => T): { data: NoInfer<T> | undefined };
declare function selectItem(): Item | undefined;
declare function memo<T>(factory: () => T): T;

export function withId(id: string): { id: string; done: boolean; title: string | undefined } | undefined {
  const { data: item } = useData(selectItem);
  return memo(() => (item !== undefined ? { id, ...item } : undefined));
}
```

### An empty object next to a spread, under a type like vitest's
`DeeplyAllowMatchers`

`expect.objectContaining({ input: {}, ...rest })` reported TS2615, once
for every member of the union but the first.

```ts
interface Matcher { match(other: unknown): boolean }
type Deep<T> = T extends Array<infer E>
  ? T | Matcher | Deep<E>[]
  : T extends object
    ? T | Matcher | { [K in keyof T]: Deep<T[K]> }
    : T | Matcher;
declare function objectContaining<T = any>(expected: Deep<T>): any;
declare const extra: { name: string } | { name?: undefined };

objectContaining({ input: {}, ...extra });
```

### Arrow functions that return objects in an `as const` table

`it.each([...] as const)` with rows like these reported TS7024.

```ts
type Args<T extends ReadonlyArray<any>> = {
  2: [T[0], T[1]];
  fallback: Array<T extends ReadonlyArray<infer U> ? U : any>;
}[T extends Readonly<[any, any]> ? 2 : "fallback"];
interface Run<T extends any[]> {
  (name: string, fn: (...args: T) => void): void;
}
interface Each {
  <T extends any[] | [any]>(cases: ReadonlyArray<T>): Run<T>;
  <T extends ReadonlyArray<any>>(cases: ReadonlyArray<T>): Run<Args<T>>;
}
declare const each: Each;
let handler: () => { n: number } = () => ({ n: 1 });
const TWO = 2;

each([
  ["two", () => ({ n: TWO })],
  ["zero", () => ({ n: 0 })],
] as const)("%s", (_name, make) => {
  handler = make;
});
```

### oven-sh#44706

```ts
type Hydrated<T> = T extends { x: {} }
  ? T
  : T & {
      [P in keyof T]: T[P] extends Lazy<infer U> ? Lazy<U> : T[P];
    };

declare class Lazy<T> {
  brand: T;
}

declare class Model {
  isHydrated(): this is {};
  assertHydrated(): this is Hydrated<this>;
}
declare class Collection<T> {
  hydrate(): Hydrated<T>;
}
class ChildCollection extends Collection<Model> {}

declare function relation<T extends Model>(ctor: new () => Collection<NoInfer<T>>): void;

relation(ChildCollection);
```

TypeScript 6.0.2 reports TS2615 on this one. TypeScript 7.0.2 does not,
and it does again if `isHydrated` is removed or moved below
`assertHydrated`. `bun check` follows 7.0.2 in all of those.

All four print nothing now, like `tsc`.

### Types reached through `globalThis`, with `bun-types` next to
`@types/node` 24

Every type written `globalThis.X` failed with TS2694. One report had 29
of these in a Next.js monorepo (oven-sh#44729). `NodeJS.BuiltinIteratorReturn`
became `unknown` for the same reason (oven-sh#44689).

```ts
export const direct: ResponseInit = { status: 204 };
export const qualified: globalThis.ResponseInit = { status: 204 };

export interface Extended extends globalThis.RequestInit {
  retries?: number;
}
```

### A function through `useCallback`, then given to a generic function

This one came in with oven-sh#44685. It reported TS7024 on the arrow function.
The result can be a template literal, an object, an array or a function.
If the arrow function is itself generic, any receiver will do.

```ts
declare function useCallback<T extends Function>(callback: T, deps: readonly unknown[]): T;
declare function useList<T>(options: { rows: readonly T[]; label?: (row: number) => string }): void;

export function list(rows: readonly string[]) {
  const label = useCallback((row: number) => `row ${row}`, []);
  useList({ rows, label });
}
```

### Many layers of an object with a spread (oven-sh#44727)

From 34 layers on this reported TS2589. `tsc` accepts 99.

```ts
const id = <T>(x: T) => x;
const nest = <T>(x: T) => id({ nested: { tag: 0, ...x } });
const eight = <T>(x: T) => nest(nest(nest(nest(nest(nest(nest(nest(x))))))));
const value = eight(eight(eight(eight(eight({ value: 0 })))));

declare function consume<T>(x: typeof value): T;
consume(value);
```

### Polymorphic components (oven-sh#44656)

Kobalte with solid-js. It reported TS2589 on the first component of a
file.

```tsx
import type { ValidComponent } from 'solid-js'
import type { PolymorphicProps } from '@kobalte/core/polymorphic'
import * as TooltipPrimitive from '@kobalte/core/tooltip'

export const First = <T extends ValidComponent = 'button'>(
  props: PolymorphicProps<T, TooltipPrimitive.TooltipTriggerProps<T>>
) => <TooltipPrimitive.Trigger {...props} />
```

### "Type 'A' is not assignable to type 'A'" (oven-sh#44660)

An arrow function given where a generic function is expected, in a call
that is generic itself.

```ts
interface Box<A, R> { readonly a: A; readonly r: R }
declare function run<A, R>(box: Box<A, R>): Promise<A[]>;
declare function make<R>(runner: <A>(box: Box<A, R>) => Promise<A[]>, tag: R): void;

make((box) => Promise.resolve(0).then(() => run(box)), "tag");
```

### A missing TS2345 (oven-sh#44686)

`tsc` reports that `"a"` is not assignable to `never`. `bun check`
reported nothing.

```ts
type Client<K extends string> = { db: K } & { [P in K]: (data: P) => void };

export function f<T extends "a" | "b">(c: Client<T>, name: T) {
  c[name]("a");
}
```

### Long runs of `!`

```sh
echo "declare const x: { a: number } | undefined; export const y: number = x$(printf '!%.0s' {1..60000});" > a.ts
bun check a.ts --strict
```

```
a.ts(1,58): error TS2322: Type '{ a: number; }' is not assignable to type 'number'.
Found 1 error in 1 file, checked 1 file [5.76ms]
```

That is what `tsc` reports. The current canary crashes on this file.
With 5,000 `!` it takes 23 seconds, says it ran out of stack and does
not report the error. The length of the run does not matter now.

### Several copies of one package

A `node_modules` tree can hold the same version of a package many times.
`tsc` keeps one copy in the program, after it has read and parsed all of
them, and so did `bun check`. Now the other copies cost little. The
errors are the same.

A 3.5 MB package of 61 declaration files, installed under 40 dependents:

| | Time | Memory |
|---|---|---|
| `tsc` 7.0.2 | 380 ms | 3,433 MB |
| `bun check`, the current canary | 190 ms | 1,974 MB |
| `bun check`, this branch | 140 ms | 195 MB |

### How it was verified

| What | Result |
|---|---|
| TypeScript's own tests, errors | 13,101 of 13,101, byte for byte |
| TypeScript's own tests, types | 12,463 of 12,463 |
| TypeScript's own tests, symbols | 12,463 of 12,463 |
| TypeScript's own tests, declaration files | 13,032 of 13,032 |
| Small programs that once differed, run through both tools in CI |
2,346, 48 of them new |
| 1,180 combinations of a way to narrow a `NoInfer` value, a use of it
and a context | 1,172 the same, 1,100 before |
| `NoInfer` around 32 kinds of type, each spread in 8 ways | 256 the
same, 250 before |
| 25 variations of the program in oven-sh#44706 | 25 the same, 7 before |
| 240 variations of the `useCallback` program: the kind of result, the
wrapper, the receiver, a generic function | 240 the same, 92 false
TS7024 before |
| 120 variations of the program in oven-sh#44660 | 120 the same, 179 lines of
false errors before |
| 60 functions that index an intersection or a mapped type with a
generic key | the same 226 errors, 122 lines differed before |
| Runs of 1, 2 and 3 `!` in 60 contexts | 180 the same, as before |
| Runs of 60,000 `!` in 10 contexts | 10 the same, none before |
| 14 layouts of several copies of a package (other text in one copy, a
dependency that resolves differently from each copy, a file missing from
one copy, ..) under 5 sets of options, at 1, 4 and 16 threads | 70 the
same |
| tsconfigs of 70 open source projects with the same errors as `tsc` |
636 of 637 |
| tsconfigs with project references, same lines in the same order as
`tsc -b` | 316 of 317 |
| Inferred types of local variables in 47 open source projects, as both
tools print them | 113,371 compared, no new difference |
| 56 open source projects checked with the files cut into tasks in 8
different ways | same output as before this PR |

In the 8 combinations that still differ, `tsc` also reports TS2698 for
the spread and `bun check` does not. Both infer `any` there.

Every fix has its program in `test/cli/check/differential-cases.json`.
The test runs `tsc` and `bun check` on each and compares the whole
output.
…n-sh#44748)

Follow-up to oven-sh#44726.

Fixes oven-sh#44747.

### A name exported by conflicting declarations

Generated declaration files sometimes declare one name twice. Under
`skipLibCheck` nothing reports the conflict, and `tsc` 7.0.2 takes
`Method` for `any`. `bun check` took it for the type of the `const` and
reported TS2322 in the code that uses it.

`node_modules/lib/index.d.ts`

```ts
export type Method = typeof Method;
export declare const Method: { a: number };
export type Method = "get" | "post";
export type Fn = (method: Method) => void;
```

`index.ts`

```ts
import type { Fn } from "lib";

export const f: Fn = (method: number) => {};
```

### How it was verified

| What | Result |
|---|---|
| The program above | no errors, like `tsc` |
| 229 modules that export one name three times, as ordered triples of 7
kinds of declaration (two type aliases, `const`, interface, function,
class, namespace), each used as a value and as a type | the same as
`tsc`, 120 lines differed before |
| TypeScript's own tests, errors | 13,101 of 13,101, byte for byte |
| TypeScript's own tests, types | 12,463 of 12,463 |
| TypeScript's own tests, symbols | 12,463 of 12,463 |
| TypeScript's own tests, declaration files | 13,032 of 13,032 |
| Small programs that once differed, run through both tools in CI |
2,347, 1 of them new |

69 of the 229 modules are the new program in
`test/cli/check/differential-cases.json`. The test runs `tsc` and `bun
check` on each and compares the whole output.
…trict and to build without WebAssembly (oven-sh#44819)

Adds two build options. Both are on by default, so the builds we publish
do not change.

- `--codeGenerationFromStrings=off` makes
`--disallow-code-generation-from-strings=strict` a compile-time constant
in Rust and C++. No flag, environment variable or API of that binary
lowers the level. Each C++ check now throws and returns in one step, so
in such a build the code after it is unreachable. `process.execArgv` is
unchanged: it lists the flags that were given.
- `--webAssembly=off` builds a Bun with no `WebAssembly` global.
`BUN_JSC_useWasm` is still a valid name there and does not bring it
back.

Also removes `JSC__JSModuleLoader__evaluate`, which nothing calls.

Verified with a debug build of each. With both options off, the new
cases in `disallow-code-generation-from-strings.test.ts` and
`compile-rss.test.ts` pass. With both on, the existing cases pass. CI
builds with both on, so the new cases skip there.
### What does this PR do?

`cd test && bun check` (oven-sh#44361) reported 8,080 type errors in 1,107
files. Now it reports none. `tsc` 7.0.2 agrees on both numbers.

```
$ cd test && bun check
✓ No type errors in 2,615 files [418.81ms]
```

Apart from the few files listed under "The JavaScript that did change",
every edit to a test is erased by the transpiler: annotations, type
arguments, `!`, casts, `import type`, `// @ts-expect-error`. That is
checked mechanically, see below.

#### `test/tsconfig.json`

- **`baseUrl` is gone.** TypeScript 7 rejects it (TS5102) and stops
before checking anything. `paths` resolve relative to the
`tsconfig.json` without it, in `tsc` and in Bun's resolver. Of the 1,697
distinct (directory, bare specifier) pairs imported anywhere below
`test/`, `.js` files included, one resolved through `baseUrl`:
`"harness.js"` in `pnpm-migration.test.ts`. It is `"harness"` now, which
is the same file.
- **`lib.dom` is gone.** The config says `"lib": ["ESNext"]`, but one
unused svelte fixture had `/// <reference lib="dom" />`, which adds
`lib.dom` to the whole program. With `lib.dom` loaded, bun-types steps
aside for `ReadableStream`, `WebSocket`, `CompressionStream`, `fetch`
and others, so `type: "direct"` streams, `new
CompressionStream("zstd")`, `new WebSocket(url, { headers })` and so on
were all errors.
- **`composite: false`**, like `src/js/tsconfig.json`. Tests emit no
declarations, and they import files from all over the repository, each
of which was a TS6307.
- **bun-types comes in through `include`**, like in
`src/js/tsconfig.json`. A `types` entry is looked up from the directory
of the config that extends this one, so
`bundler/transpiler/tsconfig.json` could not find it, and neither could
an editor or `bun check <file>` for a file in that directory.
- **Excluded**: code vendored from Node.js and the React Compiler,
`esbuild-decorator-tests.ts` (standard decorators, this project sets
`experimentalDecorators`), the bun-types fixture (declares globals such
as `ProcessEnv.FOO`, and `bun-types.test.ts` checks it with its own
config), and six fixture apps that have their own `package.json` and
`tsconfig.json` and whose dependencies the test installs.
- `../src/runtime/bake/client/**.ts` is no longer included. Tests import
two of those files, which still brings them in. The rest needs `lib.dom`
and belongs to `src/runtime/bake/tsconfig.json`.

Two small ambient files are new. `test/internal-for-testing.d.ts`
declares the 9 `$` intrinsics that `src/js/internal-for-testing.ts`
calls (`src/js/builtins.d.ts` and `private.d.ts` declare them next to
changes to `Error`, `Function` and other globals that tests should not
see). `test/asset-imports.d.ts` declares `*.c`, `*.cc`, `*.h`, `*.pem`,
`*.br` and `*.gzip`, which tests import as a path or as text.

Tests import a few files from outside `test/`, so those are part of the
project: 5 files in `packages/bun-error`, `packages/bun-release` and
`scripts/build` got the same kind of erased fix.

#### bun-types

Correct tests that did not type check because the declarations were
wrong. Each has a case in `test/integration/bun-types/fixture`.

| | before |
| --- | --- |
| `test.todo("label")`, `describe.todo("label")`, and the same for
`skip` | TS2554, fixes oven-sh#25959. The runtime allows a missing callback for
both. `Parameters<typeof test.skip>` is unchanged, and
`describe.skip(SomeClass)` is still an error: on its own, a function is
the body |
| `test("label", { retry: 1 }, () => {})` | TS2769. It is the form in
`docs/test/index.mdx`. `Parameters<typeof test>` is unchanged |
| `test.each(["a", "b"] as const)`, `describe.each(...)` | "readonly ...
cannot be assigned to the mutable type 'unknown[]'" |
| ``expect(await sql`select 1`).toEqual([{ x: 1 }])`` | "not assignable
to parameter of type 'undefined'". For a call that is only generic in
its return type, `T` was inferred as `never` from `expect(actual?:
never)`, so that overload won. Same for `webview.evaluate()` |
| `Bun.write(path, Bun.TOML.stringify({ ... }))` | `string \| undefined`
for every input. It is `undefined` only for `undefined`, a function or a
symbol, and the return type says so now. This was about 350 of the
errors |
| `Bun.YAML.parse(buffer)` | `string` only. The runtime takes what
`Bun.TOML.parse` takes: typed arrays, `ArrayBuffer`, `DataView`, `Blob`
|
| `new ReadableStream({ type: "bytes" })` without `lib.dom` | TS2769,
and a comment said `Mode "bytes" is not supported`. The overload is the
last one, so a source typed `any` still gives `ReadableStream<any>` |
| `new Blob().slice()` without `lib.dom` | TS2339 |
| `new TextDecoderStream("utf-16le", { fatal: true })` without `lib.dom`
| TS2554 |
| `webview.goBack()`, `webview.goForward()` | declared as `back()` and
`forward()`, which do not exist at runtime |

#### The JavaScript that did change

For the type check:

- `cli/install/migration/pnpm-migration.test.ts`: `"harness.js"` is
`"harness"`, see above.
- `js/sql/wire-frames.ts` declared `export function pgParameterStatus`
twice, with the same body (TS2393). The second one is gone.
- `bundler/transpiler/macro-test.test.ts`: its three `assert { type:
"macro" }` imports are `with { type: "macro" }`. In TypeScript 7
`assert` is a syntax error (TS2880), which `// @ts-expect-error` cannot
suppress and which stops the check of the whole project. The transpiled
imports are the same. One test is new: it covers `assert` for a named,
aliased, default and namespace import and for `"bun"`, from a fixture
string.
- `js/bun/xml/generate_xml_test_suite.ts` emits the
`expect<unknown>(...)` that the generated suite now has.
- `harness.ts` passes `toMatchNodeModulesAt`, `toHaveBins`,
`toBeValidBin` and `toBeWorkspaceLink` to its own `expect.extend()`. It
declares them on `Matchers<T>` now, which applies to the whole project,
so a test that forgot to register them would type check and then fail
with "toHaveBins is not a function".
- 6 files in `integration/bun-types/fixture` have new cases. They are
only type checked.

Bugs in tests that a type error pointed at, where the alternative was a
cast that hides it. Each was run on `main` and on this branch:

| | on `main` | now |
| --- | --- | --- |
| `js/web/fetch/headers.undici.test.ts` | 14 lines are `expect(actual,
expected)`: no matcher, the second argument is the failure message |
`expect(actual).toBe(expected)`, all pass |
| `js/node/crypto/crypto.key-objects.test.ts` | an `expect()` without a
matcher, `publicKey.y` compared with itself (2x), 8 assertions after
`publicKeyFromJwk` / `privateKeyFromJwk` that check the other key, a
callback for `generateKeyPairSync()` that uses undeclared `resolve` /
`reject` | they assert what they mean to, the callback is gone, same
pass count |
| `js/bun/shell/test_builder.ts` | `import { ShellError } from "bun"` is
`undefined`, so `.error(aShellError)` throws `Right hand side of
instanceof is not an object`. `doChecks()` is not awaited in the catch
path | `$.ShellError`, awaited. The shell suite gives the same results |
| `js/bun/http/bun-serve-ssl.test.ts` | `keepAlive: false`, which
`fetch` does not read | `keepalive` |
| `js/bun/websocket/websocket-server-backpressure-buffer.test.ts` |
`maxBackpressure`, which `Bun.serve` does not read, so both tests ran
with the default limit | `backpressureLimit` |
| `bundler/bundler_splitting.test.ts` | 10 tests pass `env: "inline"`,
which is the environment of the subprocess (`0=i, 1=n, ...`) | `dotenv:
"inline"` |
| `bundler/esbuild/default.test.ts` | `automaticRuntime: false`, which
the helper does not read, so the test was a `todo` | `runtime:
"classic"`, passes, not a `todo` |
| `js/web/fetch/body.test.ts` | `const { readable } = file(url)` is
`undefined`, so the case was skipped as "fails, text is empty" |
`file(url).stream()`, passes, not skipped |
| `js/web/fetch/fetch.test.ts` | `first[0]` on an `ArrayBuffer`:
compares `undefined` with `undefined` | `Uint8Array` views, so it checks
that `arrayBuffer()` returns a copy |
| `js/bun/util/password.test.ts` | the async `hash` helper calls
`hashSync()` | `password.hash()` |
| `js/node/http/node-http.test.ts` | `socket =>
expect(socket).toBe(socket)` shadows the socket it means to compare with
| compares with the outer one |
| `cli/install/bun-install.test.ts` | with `--todo`, two `todo` tests
fail in their setup: `ctx is not defined`, and `join({ root }, ...)` |
they reach their assertions. Still `todo` |
| `js/bun/test/fake-timers/sinonjs/fake-timers.test.ts` | `let
timersModule, timersPromisesModule` shadows the two imports (inside
`describe.todo`) | the `let` is gone |
| `harness.ts` | `toHaveBins()` passes for an empty `.bin` directory,
`[].every(...)` is true | compares the number of bins. The 56 call sites
pass, also on Windows |
| `harness.ts`, `cli/install/bun-install-registry.test.ts` | `new
Error("Failed to create user:", response.statusText)` drops the status
text | it is in the message |
| `docker/index.ts` | `waitTcp()` calls `Bun.connect()` without
`socket`, which throws, so it times out on an open port | resolves in 2
ms, still times out on a closed port |
| `js/bun/archive-extract-leak-repro.ts` | `Bun.Archive.from()` does not
exist | `new Bun.Archive()`, the script runs |

#### With TypeScript 6.0.2

`cd test && bun run typecheck` goes from 8,065 errors to 2, both TS2786
in `jsx-production.tsx` and `jsx-dev.tsx`. TypeScript 6 adds an implicit
`react/jsx-runtime` import to every file of a `react-jsx` project, also
to `../src/js/internal-for-testing.ts`, where it resolves to the root's
`@types/react` 18.3 instead of `test/`'s 18.0. TypeScript 7 only adds it
to files that have JSX.

#### Not in this PR

- Nothing in CI type checks `test/`, so this can regress.
`.github/workflows/lint.yml` checks `scripts`, `src/js`, `src` and
`src/runtime/bake`.
- `bun check` at the repository root follows the references in the root
`tsconfig.json`, and `src/tsconfig.json`, `src/js/tsconfig.json` and
`src/runtime/bake/tsconfig.json` still have `baseUrl`.

### How did you verify your code works?

- `bun check`, `tsc` 7.0.2: 0 errors. `tsc` 6.0.2: the 2 above.
- **Same JavaScript.** A script transpiles each changed file at `main`
and on this branch (`bun build --no-bundle`, so with this
`tsconfig.json`'s `emitDecoratorMetadata`) and compares the output: of
662 files, 634 are byte-identical, 1 differs in the line breaks of an
object literal that prettier re-wrapped (identical with
`--minify-whitespace`), and 27 are the ones listed above.
- **Same test results.** Line numbers, columns and file sizes do change,
and some tests look at their own (`response.test.ts` snapshots `Response
(8.0 KB)` for its own source file). So I ran the 1,322 test files that
changed or that sit next to a changed helper or fixture, on `main` and
on this branch, with the same release build (linux x64, no docker):
49,644 tests pass on `main`. 1,302 files have the same exit code, the
same pass / fail / skip / todo counts and the same failing tests. The
other 20 differ in both directions (11 better, 9 worse). Run twice more
on each side, each of them gets the same result on both sides at least
once, and the one that looked worst (`serve-http2-lifecycle.test.ts`)
fails 3 of 5 runs on `main` and 1 of 5 here. The files touched after
that run were run again (191, then 161 files), with the same outcome. 70
of the 1,322 import `bun:internal-for-testing`, which a release build
only allows with `BUN_FEATURE_FLAG_INTERNAL_FOR_TESTING` and
`BUN_GARBAGE_COLLECTOR_LEVEL` set, so they failed to load on both sides
in that run. Run again on both sides with the two variables (3,682 tests
pass on `main`): 67 are the same, and the other 3 are the same when run
on their own. I did not run anything with a debug build: there is no
native change.
- **No hunk is unnecessary.** For each hunk: revert it, run `bun check`,
keep the revert if there is still no error. 504 hunks were reverted that
way, most of them workarounds that the bun-types fixes above made
unnecessary. 2,908 hunks are left.
- `bun test test/integration/bun-types/bun-types.test.ts`: 22 pass, with
and without `lib.dom`.

<details>
<summary>Other bugs in tests that the type errors pointed at. Not fixed
here</summary>

Each was confirmed by reading what the test calls or by running it. They
need a service, another OS, the network, or a decision about what the
test should do. Paths are relative to `test/`.

- `js/bun/http/bun-serve-ssl.test.ts:128`: The loop over `serverName`s
only sets the `Host` request header, which never selects the TLS SNI
name in Bun's `fetch`; SNI is always the URL hostname (`localhost`). The
`localhost2.com` entry of the `valid 2` fixture is never exercised, even
if `keepAlive` is corrected to `keepalive`; `tls: { serverName }` would
be needed.
- `js/bun/perf_hooks/histogram.test.ts:537`: The "toJSON method" test
wraps all its assertions in `if (typeof h.toJSON === "function")`. Bun's
histogram has no `toJSON`, so the test only logs a message and passes
with zero assertions.
- `js/node/http/node-http.test.ts:1340`: "should noop keepSocketAlive"
never calls `agent.keepSocketAlive()`: the call sits in the request
handler of a server that never listens, so the only assertion that runs
is `agent.keepAlive === true`. The dead handler also passes a
`ClientRequest` instead of a socket and calls `server.end()`, which does
not exist, and `keepSocketAlive` is no longer a no-op in Bun.
- `js/sql/sql.test.ts:1224`: "Transaction throws on uncaught named
savepoint" calls the misspelled `sql.savepoit("watpoint", ...)`. The
call throws `TypeError: sql.savepoit is not a function`, `.catch(() =>
"fail")` maps any error to `"fail"`, and the test passes without ever
creating a savepoint.
- `js/valkey/reliability/connection-failures.test.ts:307`: "should
handle rapid connection/disconnection" pushes `client.close()` results
into `promises` and awaits `Promise.all`, but `close()` returns
`undefined`, so nothing is awaited. The clients never connect either,
because `new RedisClient()` is lazy. The test has no `expect` and opens
no socket.
- `js/web/abort/abort.test.ts:9`: "AbortSignal > spawn test" never runs
the 23 tests in `abort.ts`. It copies them to
`<tmpdir>/bun.test.XXXX/abort.test.ts`, then runs `bun test
/abort.test.ts` with `cwd: os.tmpdir()`; that filter matches nothing and
the child exits 1, but the test only asserts that stderr lacks "✗" and
never checks the exit code. `abort.ts` is referenced nowhere else.
- `regression/issue/25589-frame-size-connect.test.ts:147`: The three
tests in `HTTP/2 FRAME_SIZE_ERROR with @connectrpc/connect-node` never
send a request: each one only does `assert.ok(transport)` on the object
returned by `createGrpcTransport()`. `makes basic gRPC request without
FRAME_SIZE_ERROR` makes no gRPC request, the 50-header interceptor in
`transport with large headers in interceptor` never runs, and the
`EchoService` definition is unused.
- `bundler/cli.test.ts:271`: `describe.concurrent("bun build", () =>
{...}, 10_000)` passes a timeout as third argument to `describe`.
`describe` parses it but never applies it, so the tests in the block run
with the default timeout instead of 10 s.
- `js/bun/http/async-iterator-stream.test.ts:245`: "async generator
aborted doesn't crash" has its only assertions
(`toBeInstanceOf(DOMException)`, `name === "AbortError"`) inside
`catch`; if `fetch` resolved instead of rejecting, the test would pass
with zero assertions. Today `fetch` does reject, so the assertions run.
- `js/bun/http/bun-serve-ssl.test.ts:144`: The last `fetch` of the
`valid`/`valid 2` tests (`Host: "badhost.com"`) assigns `const res` and
the test ends without any assertion on it, so the only thing checked is
that the fetch does not reject.
- `js/bun/image/image.test.ts:1425`: "BMP 32-bit BI_RGB is XRGB" passes
`{ backend: "bun" }` to the `Bun.Image` constructor, which has no such
option, so it is silently ignored. The backend is the process-global
`Bun.Image.backend`. On macOS and Windows that global is `"system"` at
this point, so the test decodes through ImageIO/WIC and not through
Bun's own BMP decoder, which it means to cover.
- `js/bun/perf_hooks/histogram.test.ts:474`: "percentiles map
properties" asserts that the values of `h.percentiles` are `bigint` and
equal to those of `h.percentilesBigInt`. This bakes in Bun's divergence
from Node, where `percentiles` holds numbers. In Node the assertion only
holds because both getters return the same Map object, which the
`percentilesBigInt` getter has just refilled.
- `js/bun/test/parallel/test-http-get-can-use-Agent.ts:7`:
`test-http-get-can-use-Agent.ts` and `test-https-get-can-use-Agent.ts`
make real requests to `google.com`, which the repo's test rules forbid
("Never contact the public internet"). The tests depend on external
network access.
- `js/bun/util/filesink.test.ts:74`: `Bun.file(path).stream(64)`,
commented "test it on a small chunk size", has no effect. The chunk size
is used only for in-memory blobs and is dropped for file-backed ones, so
the FIFO reader runs with the default chunking.
- `js/first_party/ws/ws.test.ts:172`: In the `send()` tests the `typeof
data === "string"` branch is dead code: the `ws` client always emits a
`Buffer`, and the echo server re-sends the `Buffer` it received, so
string messages come back as binary frames and take the `else` branch
(`isBinary` true). `expect(isBinary).toBeFalse()` never runs, so no test
in the file checks `isBinary === false` on a client socket (only server
sockets are checked, in `isBinary` and `handles data passed along with
the upgrade request`).
- `js/node/tls/test-system-ca-https.test.ts:40`: `test.skip(skipReason)`
is called from inside the running test to skip it. `bun:test` throws
`Cannot call test.skip() inside a test. Call it inside describe()
instead.`, so on a Linux machine with no system CA bundle the test fails
instead of being skipped (reached only with `BUN_TEST_ALLOW_NET=1`;
otherwise the whole test is `test.skip`).
- `js/web/fetch/response.test.ts:52`: The `print size` test snapshots
`Bun.inspect(new Response(Bun.file(import.meta.filename)))`, which
prints the size of the test file itself. The snapshot `Response (8.0
KB)` holds only while the file is 8000 to 8049 bytes, so any edit that
moves the size out of that window breaks the test, including adding a
test to the file.

</details>

<details>
<summary>Gaps in bun-types that are left. The tests use a cast there,
never <code>@ts-expect-error</code>, so fixing the types breaks
nothing</summary>

Each was checked against the runtime (release build) and the current
declarations.

Web globals without `lib.dom`:
- `Request#clone()`, `Response#clone()`, `formData()` and `bytes()` are
inherited from undici-types and return undici's `Request` / `Response` /
`FormData` / `Uint8Array<ArrayBufferLike>`. `Bun.serve({ fetch: () =>
res.clone() })`, `new Response(await req.formData())` and `const r:
Request = req.clone()` are errors.
- The global `FormData` has no `[Symbol.iterator]`, and `values()` /
`entries()` are typed as yielding `string` only.
- `ReadableStream#getReader()` returns the reader of `node:stream/web`,
which is not assignable to the global `ReadableStreamDefaultReader`. So
`stream.getReader().readMany()` does not type check.
- `new Request(url: URL, init?)` has no overload, and there is no
`RequestInfo`.
- `ResponseInit.headers` is undici's `HeadersInit`, not
`Bun.HeadersInit` (`RequestInit` overrides it). `Bun.HeadersInit` has no
iterable member, so `new Headers(map)` is an error.
- `port.onmessage = (e: MessageEvent) => {}` on a `MessagePort` or
`BroadcastChannel` is an error: the handler gets undici's
`MessageEvent`, and `Bun.MessageEventSource` is `undefined`.
`MessageEventInit` has no `ports`.
- `Blob#stream(chunkSize)`: declared without parameters, two docs guides
show `blob.stream(1024)`.
- `URLSearchParams#toJSON()` returns `string[]` for a repeated key,
typed `Record<string, string>`.
- `WebAssembly.Exception` and `WebAssembly.Tag` are missing.
- Client `WebSocket`: `binaryType` rejects `"blob"`, `send()` / `ping()`
/ `pong()` reject a `Blob`, `WebSocketEventMap` has no `ping` / `pong`.
- `Bun.WorkerOptions` has no `execArgv`.

`bun:test`:
- `jest.mock()` and `jest.now()` are missing from `namespace jest`.
`jest.now()` is in `docs/test/dates-times.mdx`.
- `toHaveReturnedWith()`, `toHaveLastReturnedWith()`,
`toHaveNthReturnedWith(n)`: `expected` is optional at runtime and in
Jest's types.
- `expect.extend({ toBeEven(received: number) {} })` is an error: the
first parameter can only be `unknown` or `any`. Jest's types use `any`.
- `expect({ value: 1, name: undefined }).toEqual({ value: 1 })`, the
example in the JSDoc of `toStrictEqual`, is an error.

`Bun.*`:
- `Bun.JSON5.parse` takes `string` only. Like `Bun.TOML.parse`, the
runtime takes typed arrays, `ArrayBuffer`, `DataView` and `Blob`.
- `Bun.YAML.stringify` returns `undefined` for `undefined`, a function
or a symbol. It is typed `string`, where `TOML.stringify` and
`JSON5.stringify` are `string | undefined`.
- `SyncSubprocess.exitCode` is `null` when a signal killed the child,
typed `number`.
- `Bun.spawn`: `cmd` and `stdio` reject `readonly` arrays, `stdin`
rejects `ArrayBuffer` / `SharedArrayBuffer`. The error lands on `cmd`
("does not exist in type 'string[]'").
- `Bun.udpSocket({ socket: { error(socket, error) {} } })`: the runtime
calls `error` with one argument, the error, and `this` is the socket.
The sockets have `[Symbol.dispose]`, undeclared.
- `Bun.serve({ unix, http2 })` and `{ unix, idleTimeout }` are errors:
those options are only on `HostnamePortServeOptions`. `server.fetch(url,
init)` takes one parameter. `WebSocketHandler.message` is required, the
runtime wants any one handler.
- `Bun.connect({ fd, socket })` has no overload, although
`Bun.FdSocketOptions` is declared.
- `Bun.TLSOptions` lacks `crl`, `sigalgs`, `ecdhCurve`,
`allowPartialTrustChain`, `sessionTimeout`. The `Error` of `handshake()`
and `getAuthorizationError()` always has a `code`, undeclared.
- `BuildConfig.metafile` is `boolean`. `string` and `{ json?, markdown?
}` are documented and work.
- `Bun.Transpiler`: no `minify` option, `scan()` / `scanImports()` take
no `loader`, `transform(code, loader)` rejects `json`, `toml`, `yaml`,
`text` and others.
- `BuildMessage#notes`, `line`, `column` and `ResolveMessage#line`,
`column` are missing.
- `Bun.file("s3://...", s3Options)` is an `S3File` at runtime, typed
`BunFile`. `Bun.write(dest, data, options)` does not take `S3Options`,
and `Bun.write(S3File, Response | Request)` has no overload.
- `BunFile#write()` rejects a `Blob`. Its options are typed `{
highWaterMark }`, the runtime reads `createPath` and `type`.
- `Bun.hash(data)` always returns a `bigint`, typed `number | bigint`.
- `Bun.peek(rejected)` returns the rejection reason, typed `Promise<T> |
T`. The example in `docs/runtime/utils.mdx` does not type check.
- `new Bun.CryptoHasher(algorithm, hmacKey)` rejects an `ArrayBuffer`
key.
- `RedisClient`: `psubscribe()`, `punsubscribe()`, `pubsub()` are
missing, `KeyLike` excludes `ArrayBuffer`.
- `sql.array()` rejects typed arrays, and Bun returns `int[]` columns as
`Int32Array`.
- `$.ShellPromise` has no `bytes()`.
- `Bun.WebView` overloads `addEventListener` for CDP events, not
`removeEventListener`.
- `JSCallback` (`bun:ffi`) has `[Symbol.dispose]`, undeclared.

</details>

<details>
<summary>Runtime behavior noticed on the way</summary>

Each was reproduced with the release build, and compared with Node 26.3
where that applies.

- `src/js/internal/sql/shared.ts:1342`: `sql.close({ timeout: 0 })`,
documented as closing all connections immediately, behaves like
`close()` with no timeout and waits for pending queries, so it never
resolves while a connection is stuck mid-handshake. `hasTimeout =
!!timeout` is false for the number `0`, so the `timeout === 0` branch is
only reached by the string `"0"`. `sql-close-pending-connection.test.ts`
passes `"0"` to get the forced close.
- `src/ast/scope.rs:494`: Bun accepts an ES module with two top-level
`function f` / `export function f` declarations and silently keeps the
last one. Node rejects the same file with `SyntaxError: Identifier 'f'
has already been declared`, because top-level function declarations are
lexical in a module. Bun also does not report the duplicate export name.
- `src/jsc/bindings/JSNodePerformanceHooksHistogram.cpp:232`:
`Histogram.percentiles` returns a Map with `bigint` values in Bun (`Map
{ 0: 5n, 100: 5n }`), where Node returns numbers (`Map { 0 => 5, 100 =>
5 }`). `getPercentiles` has the same body as `getPercentilesBigInt`.
- `src/jsc/bindings/JSNodePerformanceHooksHistogramPrototype.cpp:39`:
`perf_hooks.createHistogram()` objects have no `toJSON()` in Bun.
`JSON.stringify(histogram)` gives `{}`, where Node gives
`{"count":1,"min":5,"max":5,"mean":5,"exceeds":0,"stddev":0,"percentiles":{...}}`.
- `src/jsc/bindings/webcore/JSDOMFormData.cpp:314`: A plain `Blob`
appended to a `FormData` without a filename comes back from `get()` and
from `Response.formData()` as `instanceof File` with `name ===
undefined` and `constructor.name === "Blob"`, and is serialized with
`filename=""`. The XHR spec and Node give such an entry the name
`"blob"` (`filename="blob"`).
`test/js/web/html/FormData-multipart-serialization.test.ts` asserts the
current behaviour (`name: undefined`, `filename=""`).
- `src/runtime/ffi/FFI.h:222`: Calling a `cc()`-compiled function whose
argument is `"cstring"` with a JS string or a `CString` object
segfaults, while the same call through `dlopen` works and the shared
types allow `string`. This is the "FIXME: bus error" behind the skipped
block in `test/js/bun/ffi/cc.test.ts`.
- `src/js/node/inspector.ts:526`: `inspector.Session#post(method)`
without a callback returns the result object in Bun and throws
synchronously on an error such as an unknown method. Node returns
`undefined` and does not throw.
`test/js/node/inspector/inspector-profiler.test.ts` asserts the Bun-only
return value, e.g.
`expect(session.post("Profiler.enable")).toEqual({})`.
- `src/runtime/crypto/PBKDF2.rs:164`: `crypto.pbkdf2(password, salt,
iterations, keylen, "md55")` with no callback throws
`ERR_CRYPTO_INVALID_DIGEST` in Bun and `ERR_INVALID_ARG_TYPE` (`The
"callback" argument must be of type function`) in Node, because Bun
validates the digest name before the callback. The `digest` test in
`test/js/node/crypto/pbkdf2.test.ts` omits the callback and so pins the
Bun-only order.

</details>
…d once for all projects (oven-sh#44807)

A crash on plain JavaScript and three false errors are fixed. The
projects of one check load a declaration file once, which makes a build
with project references up to 1.5 times as fast. `bun check` on one
thread does 11 to 45% less work for all but the largest programs.

Fixes oven-sh#44763, fixes oven-sh#44766, fixes oven-sh#44781, fixes oven-sh#44808.

## A crash on an object literal that assigns to `this[k]`

`bun check` aborted with an index out of bounds on this file, with
`allowJs` alone. It takes nine members or more and an assignment to
`this` in one of the methods. `tsc` 7.0.2 reports TS7053 and TS2304
under `checkJs` and nothing without. Now `bun check` does too.

```js
const o = {
  a() {}, b() {}, c() {}, d() {}, e() {}, f() {}, g() {},
  h() { this[k] = 1; },
  i() {},
};
```

| What | canary of 620b50f | this branch |
|---|---|---|
| The file above | exit code 134 | the same output as `tsc` |
| 294 files: literals of 1, 7, 8, 9, 10, 17 and 40 members, 7 kinds of
statement in a method, 6 after the literal | exit code 134 | the same
output as `tsc`, 2,002 errors |

77 of the 294 files are a new program in
`test/cli/check/differential-cases.json`. The file above is a test of
its own in `check.test.ts`, with `allowJs` alone and with `checkJs`,
which runs in every build.

## Three false errors, and a missing one

`tsc` 7.0.2 reports nothing for the three programs. `bun check` reported
the error in the comment.

A destructured variable in a loop (oven-sh#44808). Every destructuring but a
rest had this, in every kind of loop, where the initializer is a call or
a `new` whose callee depends on the variable that the loop assigns:
`where(cursor).limit()`, `cursor.limit()`, `cursor.q().limit()`, `new
cursor.C()`.

```ts
type Row = { parent: string | null };
interface Query {
  limit(): Row[];
}
declare function where(id: string): Query;

let cursor: string | null = "root";
while (cursor !== null) {
  const [row] = where(cursor).limit(); // TS7022
  cursor = row?.parent ?? null;
}
export {};
```

The rest of an object whose keys are members of a string enum (oven-sh#44766).
`Required`, `Partial`, `Record`, `Pick`, `Omit`, `Readonly` and every
other mapped type had this. So had whatever is made from a property with
such a key: in `{ ...x }`, in a rest, in `{ ...(c ? x : undefined) }`,
in the type that is inferred for `T` from `{ [P in keyof T]: T[P] }`,
also where `x` is of a type, an interface or a class declared with
`[K.ID]`. There `keyof` had the string `"id"` in place of `K.ID`.

```ts
enum K {
  ID = "id",
}

type Item = { [K.ID]: string };

export function f({ [K.ID]: id, ...rest }: Required<Item>): Item {
  return { [K.ID]: id, ...rest }; // TS2783
}
```

A name imported from `export =` of a variable merged with a namespace,
as in `@types/chance` (oven-sh#44781).

`node_modules/lib/index.d.ts`

```ts
interface Instance {
  n: number;
}
interface Static {
  (): Instance;
  new (): Instance;
  Chance: Static;
}
declare var Chance: Static;
declare namespace Chance {
  export { Instance as Chance };
}
export = Chance;
```

`index.ts`

```ts
import { Chance } from "lib";

export const a = Chance(); // TS2693
export const b: Chance = a;
```

Such a name, imported with `import type` and used as a value, was not
reported. Now it is TS1361, as in `tsc`.

| Programs around each report, compared with `tsc` | canary of 620b50f
| this branch |
|---|---|---|
| oven-sh#44808: 20 kinds of declaration in 3 kinds of loop, 60 files | 36
files with a false TS7022 | the same output |
| oven-sh#44808: 12 shapes of pattern, 8 kinds of initializer, 4 kinds of loop,
3 pairs of types, with the type of every element compared, 1,152 files |
432 files differ | the same output, 3,217 errors |
| oven-sh#44766: 25 destructurings of mapped types over enum keys, with and
without an `as` clause, and `keyof` of each | 12 lines differ | the same
output, 41 errors |
| oven-sh#44766: 8 mapped types over enum members whose values spell numbers
(`A = "0"`), numeric enums and plain keys | 17 lines differ | the same
output, 67 errors |
| oven-sh#44766: 15 kinds of type with enum keys, 18 operations that make
properties from others, `keyof` of the result and a rest taken from it,
270 files | 245 files differ | the same output on one thread, 891
errors; on all threads `?: undefined` is printed elsewhere in 33 lines,
as `tsc` itself does between its two modes |
| oven-sh#44781: 7 kinds of value that the namespace exports under the name, 5
kinds of `export =` type, 35 files | the same output | the same output,
162 errors |
| oven-sh#44781: 9 kinds of export of the namespace, 4 kinds of property, 7
ways to import, 252 files | 60 lines differ | the same output, 643
errors |

A part of each is a new program in
`test/cli/check/differential-cases.json`.

## Projects of one check load a declaration file once (oven-sh#44763)

`bun check -p app` where `app` has `references`, `bun check -b`, and
`bun check a b` with directories of several projects check one program
for each project. Each of them loaded the libraries and the declaration
files in `node_modules` again, so loading grew with the number of
projects while the number of files did not. Now a declaration file is
loaded once for the whole check.

The workspace of the report: one app that imports N modules and lists
them in `references`, each module with ten files that import `effect`.
`bun check` in `apps/api`, best of 8 runs that alternate between the
builds.

| References | canary of 620b50f | this branch | this branch, the same
files without `references` |
|---|---|---|---|
| 5 | 84 ms | 54 ms | 30 ms |
| 20 | 157 ms | 90 ms | 27 ms |
| 40 | 285 ms | 133 ms | 35 ms |

Where loading is nearly all there is to do, as there, that is most of
the time. In a real monorepo most of the time goes into checking. These
are the largest builds with references among 47 repositories, one for
each repository: `bun check -b -p <config>`, best of 5 alternating runs.
The output of the two builds is the same for every one of them.

| Repository | Projects | Time, canary | Time, this branch |
Instructions, canary | Instructions, this branch | Peak memory, canary |
Peak memory, this branch |
|---|---|---|---|---|---|---|---|
| vercel-ai | 104 | 4.68 s | 4.05 s | 494.7 G | 447.4 G | 2708 MB | 3025
MB |
| effect | 40 | 1.77 s | 1.75 s | 149.7 G | 131.5 G | 2053 MB | 2167 MB
|
| astro | 68 | 1.48 s | 0.99 s | 82.7 G | 47.9 G | 1697 MB | 1704 MB |
| prisma | 25 | 1.57 s | 1.39 s | 71.3 G | 61.7 G | 1484 MB | 1574 MB |
| langchainjs | 25 | 1.52 s | 1.09 s | 61.0 G | 46.9 G | 1623 MB | 1647
MB |
| tldraw | 17 | 0.70 s | 0.61 s | 41.3 G | 33.5 G | 1215 MB | 1196 MB |
| typescript-eslint | 38 | 1.00 s | 0.71 s | 38.4 G | 20.6 G | 1007 MB |
1008 MB |
| apollo-client | 3 | 0.36 s | 0.30 s | 24.1 G | 24.1 G | 604 MB | 611
MB |
| hono | 7 | 0.33 s | 0.27 s | 15.1 G | 12.5 G | 889 MB | 911 MB |
| date-fns | 4 | 0.24 s | 0.23 s | 11.2 G | 10.1 G | 501 MB | 569 MB |
| rollup | 7 | 0.22 s | 0.20 s | 11.5 G | 10.2 G | 491 MB | 479 MB |
| query | 5 | 0.17 s | 0.15 s | 12.0 G | 9.7 G | 710 MB | 735 MB |
| nest | 9 | 0.24 s | 0.19 s | 9.6 G | 6.3 G | 675 MB | 668 MB |
| typescript | 2 | 0.24 s | 0.24 s | 8.9 G | 8.8 G | 324 MB | 343 MB |
| all 14 | 354 | 14.52 s | 12.17 s | 1031.6 G | 871.3 G | | |

What is loaded once is kept until the last project is loaded, so peak
memory goes up where it is reached before that. Nothing is kept for
compiler options that only one project has: the three projects of
apollo-client have three sets.

| What | Result |
|---|---|
| 317 configs with references in 47 repositories, the errors in order
and with duplicates, against `tsc -b` | 316 the same, as before |
| 47 repositories, one project each | the same output as before |
| About 35,000 files (TypeScript's tests, the libraries, `effect`,
vscode, next.js, storybook, excalidraw): what another project gets,
compared with what it would have loaded itself | the same for all but
one, 6,000 nested `else if`, which is parsed as far as the stack allows
|
| The same comparison with one of six steps of the translation left out
| 767 to 1,243 of 1,347 files are reported |

A debug build makes that comparison for every file that it keeps. Seven
projects with one package and seven sets of compiler options are a new
program in `test/cli/check/differential-cases.json`.

One difference from `tsc -b` stays. If a project reads an old
`dist/index.d.ts` from the disk before the project that emits it is
built, `tsc -b` gives that old file to every later project of the same
build too, although it has written the new one by then, and the next
`tsc -b` reports something else. `bun check -b` gives each project what
is there when it is built, as before. And `bun check late later`, with
two directories, is two builds: what a project emits in one of them is
not what the other finds on the disk. `check.test.ts` has a test for
both.

## One thread divides the files into as few tasks as it can

Two threads and more do what they did.

The files of a program are divided into tasks, and tasks that run side
by side each work out the types they share for themselves. With several
threads that keeps threads busy that have nothing else to do. With one
thread it is a fifth to a half of all the work.

So one thread now checks the files in program order, like `tsc
--singleThreaded`, in tasks of a bounded size. Most programs are one
task. A task keeps what it has worked out until it ends, so the bound is
what the memory grows by. A program of more than about 3,000 files is
divided as before: VS Code gains nothing from fewer tasks.

### `bun check --threads=1`, instructions and peak memory

Against the way the files were divided before. The output is the same in
every project.

| Project | Tasks before | Tasks now | Instructions | Peak memory before
| Peak memory now |
|---|---|---|---|---|---|
| fastify (its type tests) | 18 | 1 | -45% | 164 MB | 97 MB |
| nest | 199 | 2 | -45% | 305 MB | 294 MB |
| redux-toolkit | 156 | 1 | -42% | 399 MB | 332 MB |
| storybook | 194 | 2 | -21% | 647 MB | 980 MB |
| excalidraw | 167 | 1 | -19% | 210 MB | 245 MB |
| playwright | 182 | 1 | -17% | 340 MB | 375 MB |
| zod | 166 | 1 | -14% | 400 MB | 565 MB |
| next.js | 182 | 3 | -13% | 455 MB | 571 MB |
| nuxt | 186 | 1 | -12% | 388 MB | 473 MB |
| xstate | 152 | 1 | -11% | 243 MB | 298 MB |
| VS Code | 251 | 251 | 0% | 1,653 MB | 1,649 MB |

### One thread and several can report something different

In a program with cycles through many modules (inferred return types,
recursive aliases, variances), what is reported depends on which file
enters a cycle first. `tsc` has the same property. On the 60 modules of
the test:

| Command | Errors | Compared with `tsc --singleThreaded` |
|---|---|---|
| `tsc --singleThreaded` | 518 | |
| `tsc --checkers 2` | 519 | 1 line missing, 2 more |
| `tsc` | 521 | 4 lines missing, 7 more |
| `bun check`, any number of threads, before | 554 | 46 lines missing,
82 more |
| `bun check --threads=1`, now | 518 | the same output |
| `bun check`, 2 threads and more, now | 554 | as before |

No real project I have shows a difference between one thread and
several.

### How it was verified

| What | Result |
|---|---|
| The 11 projects above, one thread, against the division of several
threads | the same output in all |
| 47 open source projects, one thread against all threads | the same
output in all |
| 2,508 small programs that once differed, on one thread, against `tsc`
| the same 2,440 agree as on all threads |
| TypeScript's own tests, with the division of several threads | 100% of
errors, types, symbols and declaration files |
| `test/cli/check`, 7 files and the 8 of `conformance`, on a release
build | all pass |
| The differential cases on the canary of 620b50f | fail on the new
program with `exit code 134, signal SIGABRT` |

In CI, every program of `differential-cases.json` is now checked twice,
on the default number of threads and on one, and a crash counts as a
difference: before, one that printed nothing looked like `tsc` reporting
nothing. The test that all thread counts report the same on the 60
modules is now two: 2, 3, 8 and 16 threads report the same, and one
thread reports what `tsc --singleThreaded` does. TypeScript's own tests,
each of which runs on one thread, keep the division of several threads.
Two false errors and a missed one are fixed. `bun check` reported the
first two where `tsc` 7.0.2 reports nothing, and nothing where `tsc`
reports the third.

Fixes oven-sh#44758, fixes oven-sh#44839.

## A renamed binding with the name of a type parameter (oven-sh#44839)

```ts
export type Fn = <X>({ a: X }: { a: X }) => void;
```

```
c2.ts(1,27): error TS2842: 'X' is an unused renaming of 'a'. Did you intend to use it as a type annotation?
```

`X` in the annotation counts as a use of the binding `X`, because the
two have one name in one signature. Function types, constructor types,
call, construct and method signatures, overloads, abstract methods and
`declare`d functions and methods all had this.

Where the name is not used at all, the error is still reported, as in
`tsc`:

```ts
export type Fn = ({ a: string }: { a: string }) => void;
```

```
c4.ts(1,24): error TS2842: 'string' is an unused renaming of 'a'. Did you intend to use it as a type annotation?
```

## immer's `Draft`, type-fest's `SetRequired` and a method that takes
`Draft<this>` (oven-sh#44758)

```ts
type WritableDraft<T> = { -readonly [P in keyof T]: Draft<T[P]> }
type Draft<T> = T extends Function ? T : T extends object ? WritableDraft<T> : T
type Simplify<T> = { [P in keyof T]: T[P] }
type SetRequired<T, P extends keyof T> = Simplify<Omit<T, P> & Required<Pick<T, P>>>

interface Group {
  readonly items?: ReadonlyArray<Item>
}

type Kind = 'a' | 'b'
declare class Item<K extends Kind = Kind> {
  update(state: Draft<this>): void
  readonly group?: Group
  readonly kind: K
}

type ItemA = Item<'a'>

declare function isItemA(item: WritableDraft<Item>): item is WritableDraft<ItemA>
declare function isItemA(item: Item): item is ItemA

declare function useItemA(item: SetRequired<ItemA, 'group'>): void

export function f(item: SetRequired<Item, 'group'>): void {
  if (isItemA(item)) useItemA(item)
}
```

```
repro.ts(25,22): error TS2615: Type of property 'update' circularly references itself in mapped type 'WritableDraft<Simplify<Omit<Item<Kind>, "group"> & Required<Pick<Item<Kind>, "group">>> & ItemA>'.
```

The report names seven things of which each one, taken away, made the
error go away. Of their 128 combinations, with `update` as a method and
as a property, 15 had the false error.

## An invalid spread that shows only while a type argument is inferred

```ts
class K { k = 1 }
declare function id<T>(value: T): T;

export function f<P extends K | string | undefined>(k: P) {
  return id(k instanceof K ? { s: 1, ...k } : undefined);
}
```

```
a.ts(5,38): error TS2698: Spread types may only be created from object types.
```

`tsc` reports this and `bun check` did not. While `T` is inferred, `k`
is a `P & K`, which could be a `string & K`. Once `T` is known, `k` is a
`K`, and the spread is fine. An error that is found on the way is
reported all the same.

It takes a type parameter or a `NoInfer` whose constraint is a union
with a primitive, a narrowing by `instanceof` or `typeof`, and an
argument of something generic: a call, `new`, a tagged template, the
body of a callback, or the attributes of a generic JSX component.

## How it was verified

Every row is the whole output of the real `bun check` against `tsc`
7.0.2, on all threads and with `--threads=1`, `2` and `8`.

| Programs | canary of e555359, which is main | this branch |
|---|---|---|
| oven-sh#44839: the five programs of the report | 3 false errors | the same
output |
| oven-sh#44839: 10 kinds of declaration without a body, 5 things that share
the name, 12 ways to use it, 580 files | 140 false errors | the same
output, 636 errors |
| oven-sh#44839: the same files with `noUnusedLocals` and `noUnusedParameters`
| 140 false errors | the same output, 1,068 errors |
| oven-sh#44758: the program of the report | 1 false error | the same output |
| oven-sh#44758: the seven ingredients in all combinations, as a method and as
a property, with the types of the narrowed value, of `update` and of
what it has from `Function`, 256 files | 15 false errors | the same
output, 1,878 errors |
| oven-sh#44758: the same combinations where the one statement reads the
properties another way: a weak type, a misspelled member, an excess
property, `in`, 1,536 files | 60 false errors | the same output, 3,297
errors |
| The spread: the program above | 1 error missing | the same output |
| The spread: 9 declared types, 7 narrowings, 23 contexts with and
without inference, JSX, and a file that asks for the type from outside
first, 130 files | 358 errors missing | the same output, 2,407 errors |

Five new entries in `test/cli/check/differential-cases.json` keep a part
of each matrix in CI, where both tools run on them again.

| Everything else | Result |
|---|---|
| TypeScript's own suite, 5 kinds of baseline | 100% of each, as before
|
| 2,512 small programs that once differed, against `tsc`, on all threads
and on one | the four new ones and an older one agree now, no other
changed |
| 47 real projects | the same output as main |
| 317 builds with project references | the same output as main |
| Instructions for 9 configs, among them vscode, next.js and storybook |
+0.1% against main, and main against itself gives -0.2% |
oven-sh#44820)

### What does this PR do?

With `bun build --splitting`, a chunk that two entry points share can
run its modules in the wrong order for one of them. No error, no
warning.

```js
// index.js
import("./a.js").then(m => console.log("ok", m.a));
// worker.js
import { q } from "./q.js"; import { a } from "./a.js"; console.log("worker", q, a);
// a.js
import "./p.js"; import { q } from "./q.js"; export const a = "a" + q;
// p.js
globalThis.P = { v: "P" };
// q.js
export const q = "q" + (globalThis.P?.v ?? "-noP");
```

| `--splitting --target=bun ./index.js ./worker.js` | `out/index.js` |
`out/worker.js` |
|---|---|---|
| source, and without `--splitting` | `ok aqP` | `worker q-noP aq-noP` |
| 1.4.2 | `ok aqP` | `worker qP aqP` |
| `main` (620b50f) | `ok aq-noP` | `worker q-noP aq-noP` |
| this PR | `ok aqP` | `worker q-noP aq-noP` |

A chunk prints its files in one order. `index.js` needs `p`, `q` and
`worker.js` needs `q`, `p`, so no order of one chunk is right for both.

### The change

One rule for which files share a chunk:

| | files share a chunk when |
|---|---|
| before | the same entry points load them |
| now | the same entry points load them, and run them back to back in
the same order |

Each chunk already imports the other chunks in its own evaluation order,
so the engine runs the files in the order of each program. Here `p.js`
and `q.js` get a chunk each. There are no new wrappers, and
`runtime.js`, the printer, tree shaking and the layout of a chunk do not
change.

- `splitChunksByEvaluationOrder.rs` (new) gives each file a number that
`compute_chunks` adds to the chunk key.
- Only files that run something when they load count
(`loading_file_has_no_side_effects`), and files that import one of
those. Files that only declare never split a chunk.
- Only the entry points that can be the first to load the files count. A
lazy page that imports what the main entry point has already run does
not. That is `load_class`, which moves out of `merge_small_chunks` into
`EntryLoadGraph`, unchanged.
- The rule applies after `--min-chunk-size` has folded chunks.
- Two places in `findAllImportedPartsInJSOrder.rs` took "same entry
bits" to mean "same chunk". They also look at the chunk now.
- The file of an entry point that another entry point imports is in a
shared chunk (`a.js` above, which `worker.js` imports). The chunk of the
entry point then has no file to rank its imports from, and lists them by
chunk index. When it loads a split-off chunk, they are ranked from the
file of the entry point.

Each of these does nothing when no chunk is split, so such a build
prints what it prints on `main`.

### Behavior changes

- A build whose entry points run shared modules with side effects in
different orders has more chunks: one more for the example above.
- Two `import()`s that are in flight at once, whose targets run shared
modules in different orders: the target that evaluates first decides the
order, as from source. On `main` the order was fixed at build time.

### Not fixed

- Only plain files are split off. A group bundles as on `main` when one
of its counted files is in an import cycle, reaches a top-level await,
or imports a wrapped file (a CommonJS module), or when a split
`require()` loads the group. Chunks import each other for the bindings
that they use, not for every `import` of their files, and all the
imports of a chunk run ahead of its code. So in a cycle the chunks do
not run in the order of the files, a chunk does not wait for an async
chunk that it only imports for its side effects, and the `require_x()`
that prints where an `import` is cannot run between two chunks. And the
imports of a chunk are not ranked by what a split `require()` runs at
load.
- A shared chunk runs before the code of the entry point that imports
it, as on `main`.
- Two shared chunks with different entry points whose files interleave,
as on `main`.

An earlier version of this PR turned the files into `__esm` wrappers.
That is gone, with everything that it needed.

### How did you verify your code works?

20 new tests in `bundler_splitting.test.ts`
(`splitting/SharedFilesIn*`). On a release build of `main`, 11 fail: the
example above in six configurations, two `import()` targets, files that
stay together, an entry point whose file is in another chunk, files that
a fold puts in one chunk, and a file that folds into a group that is
split. The other 9 say that nothing changes: one order, declarations
only, files that are already loaded, an import cycle, entry points that
import each other, the imports of an entry point's chunk, a top-level
await, an import of a CommonJS module, a `require()` at load. The last
seven each failed on an earlier head of this approach.

On a release build of the head, `bundler_splitting`, `esbuild/splitting`
and `bundler_compile_splitting`: 275 pass, 0 fail. All of
`test/bundler`: no test fails that does not fail on a build of `main`
here.

31 small graphs (51 entry points) from earlier review and fuzzing, each
run from source under Node 22, from a bundle of `main` and from a bundle
of the head: 10 entry points are wrong on `main` and right here, none
the other way around.

A differential fuzzer (not in the repo) builds random module graphs with
`main` and with this branch, runs every entry point from both bundles,
and compares with the source. On the head, 2,250 graphs and 9,409
comparisons: 30 are wrong on `main` and right here, none the other way
around. So were the two rounds of 2,250 graphs before, on the two heads
before. The files of these graphs are small, so `--min-chunk-size` folds
few of them. Without `--splitting` the two never differ.

Three rounds of 2,250 graphs on earlier heads found ten graphs that were
right on `main` and wrong there, from five causes. Review found two
more. Each cause has a test above, and all ten graphs now print what
`main` prints or what the source prints.

300 more graphs with `--splitting`, on the head: 278 bundles have the
bytes of `main`. Of the entry points of the other 22, 17 are wrong on
`main` and right here, none the other way around.

Not run: build time, a debug build, Linux, Windows.
… breaks no load-time read (oven-sh#44876)

### What does this PR do?

Fixes a `--splitting` bundle that throws at load from every entry point,
where the same build on 1.4.2 loads. A regression from oven-sh#42695, which no
release has.

```js
// index.js
import("./m5.js").then(m => console.log("ok", m.w5));
// worker.js
import "./flag.js"; import "./m6.js"; console.log("worker");
// flag.js
globalThis.IS_WORKER = true;
// m2.js
export { v5 as x } from "./m5.js"; export function v2() { return 2 }
// m5.js
import { v2 } from "./m2.js"; import { v6 } from "./m6.js";
export function v5() { return v2 }
export const w5 = globalThis.IS_WORKER ? null : v6.toUpperCase();
// m6.js
import { v2 } from "./m2.js"; export const w6 = typeof v2; export let v6 = "six";
```

`bun build --splitting --outdir=out index.js worker.js`

| | `out/index.js` | `out/worker.js` |
|---|---|---|
| unbundled, 1.4.2 | `ok SIX` | `worker` |
| `main` | `TypeError: undefined is not an object (evaluating
'v6.toUpperCase')` | the same |
| this PR | `ok SIX` | `worker` |

### The cause

`m2.js`, `m5.js` and `m6.js` import each other and share a chunk. A
chunk prints its files in one order, that of its owner: the entry point
that loads first among the chunk's entry points. Here that is
`worker.js`. It enters the cycle at `m6.js`, so in its order `m5.js`
comes first, and `m5.js` reads `v6` when it runs.

### The change

When the owner's order puts a statement that evaluates code at load
ahead of a binding that it reads, the chunk follows the next entry point
by rank whose order does not. Here that is the `import("./m5.js")`, with
`m2.js`, `m6.js`, `m5.js`. With no such entry point, the chunk stays as
it is on `main`.

- A read counts when the statement names the binding itself, or through
the functions that it names (`part.dependencies`). `const f = () => x`
is a function to search through, not a read of `x`
(`Expr::can_be_moved`).
- Only a chunk that two or more entry points share, and in which the
walk comes back to a file that it has not left, is looked at. Every
other build takes the same path as before.
- The walk that passes the check lays the chunk out, so the order that
prints is the one that was checked. Every other chunk of the build keeps
the layout that it has on `main`.
- All of it is in `findAllImportedPartsInJSOrder.rs`. `run_walks` and
`create_walks` are moves, so that more walks can run.

### Not fixed

- A cycle that no one order serves stays as on `main`: `a.js` reads
`b.js` under one entry point, and `b.js` reads `a.js` under another.
- The read is judged by syntax, per statement. A closure inside an
object or a call (`{ get: () => store }`) counts as a read of what it
names, and so does a read that a condition skips at run time.
- The entry point that gives the chunk up no longer gets its own order
of side effects in that chunk. The one that takes it does. So a file
that needs what another file of the cycle sets on a shared object, not
through a binding, can run too early for the first entry point. That
takes an order of its own that breaks a read by the rule above.
- In the example, `flag.js` still runs after the shared chunk, so `w5`
is `"SIX"` under `worker.js`, where the source has `null`. 1.4.2 does
the same (oven-sh#44028, oven-sh#44495).

oven-sh#44823 handles these with `__esm` wrappers, and changes much more.

### How did you verify your code works?

10 new tests. 9 fail on a build of `main`: the example, the entry points
in the other order, `--target=browser`, minified, `[hash]` in the entry
names, without `flag.js`, a read through a function, a function that is
only stored, and `--compile` with a `Worker`. The other one passes
there: an owner whose order breaks no read keeps its chunk, which the
first commit of this branch got wrong.

`bun bd test` on `bundler_splitting`, `esbuild/splitting`,
`bundler_html` and `css/*`: 442 pass, 0 fail. On the first commit, those
with `bundler_edgecase`, `bundler_cjs2esm` and `bundler_barrel`: 805
pass, 0 fail. No existing test or snapshot changes.

**Builds that must not change.** Two entry points that share `hono`,
`svelte` or `axios` (49, 105 and 87 modules): every output file is
identical to `main`. 900 random graphs without `--splitting`: all
identical.

**Random module graphs with many cycles**, each built by `main` and by
this branch and compared with the unbundled program. Every module logs
when it runs and what it reads at load. The script is not in the repo.

| | ES modules | wider syntax | up to 73 files | `"sideEffects": false`
package | HTML pages | package with top-level await |
|---|---|---|---|---|---|---|
| graphs | 1,000 | 600 | 150 | 800 | 400 | 500 |
| graphs whose output differs | 54 | 66 | 43 | 39 | 33 | 16 |
| values that the source reads and the bundle does not, `main` | 106 |
441 | 630 | 102 | 191 | 75 |
| the same, this branch | 75 | 352 | 503 | 49 | 146 | 48 |
| entry point runs that lose more values than on `main` | 0 | 0 | 0 | 0
| 0 | 0 |
| pairs of effects in the wrong order, `main` | 520 | 2,859 | 10,025 |
654 | 2,232 | 354 |
| the same, this branch | 554 | 2,930 | 10,180 | 648 | 2,342 | 348 |

The values that are still lost are not traced one by one. These graphs
are dense with cycles, and the first two limits under "Not fixed" both
leave the owner as it is. The order of effects moves a little either
way, for the third.

What these graphs do not cover: none has a statement like `const f = ()
=> x`, so only the tests reach that rule. Every module logs, so
`--min-chunk-size` folds next to nothing: 1,550 graphs with it give the
same counts.

Not run: Windows and Linux locally, a timing comparison.
…w into it (oven-sh#44862)

### Problem
- When the consumer of `new Response(asyncIterable)` leaves, the
iterator can stay open for good. `Bun.serve`, the client hangs up:
`return() calls: 0; listeners left on the emitter: 5`. 1.4.2 returns all
five.
- The cancel hook picks `reason ? iterator.throw?.(reason) :
iterator.return?.()`
(`src/jsc/bindings/webcore/streams/BunAsyncIterableSource.cpp:462`). The
error tail calls `iterator.throw?.(error)` (`:184`). Since oven-sh#41456,
oven-sh#41757, oven-sh#41894 and oven-sh#42130 (none released) consumers send a reason.
- An iterator with no `throw()` gets no call. A generator that catches
the throw never runs `finally`.

### Fix
- Every cancel calls `return()`, the rule of oven-sh#13687 and
`docs/guides/http/sse.mdx:19`.
- The error tail follows `for await`: a failed write calls `return()`, a
failed iterator gets no call. A write that fails at once takes this path
too.
- Correct because a body cannot catch `return()`. node and
`ReadableStream.from()` never call `throw()`.
- Verified: 120 new cells in 5 test files, 115 fail on main.
Self-reviewed: 14 concerns raised, 13 addressed.

### Background
- The body is a direct `ReadableStream` pumped from `iterator.next()`.
The cancel hook runs when the consumer leaves, the error tail when the
pump fails.
- Considered `return()` only where `throw()` is missing: a generator
that catches stays open. A reason-less close at `streams.rs:2015` misses
`FileSink::on_close`.

### Downsides
- Reverses oven-sh#41757 (unreleased): `cancel(reason)` no longer throws into a
generator. Released change: `catch` no longer sees a failed write, and
an iterator with only `throw()` gets no call.
- Not closed: a generator that never reaches a `yield` again.
- Cost: a refused chunk takes one more reaction (14 to 15 cells). Per
chunk: none. Release text: -1,280 B.

<details><summary>Notes</summary>

**Who sends a reason (all end in the same cancel hook)**

| Producer | Since | Reason |
| --- | --- | --- |
| `HTTPServerWritable::abort` (`streams.rs:2015`): client hang-up, idle
timeout, `server.stop(true)` | oven-sh#41894 | connection-closed `AbortError` |
| `FileSink::on_close`: a failed write on `Bun.spawn` stdin or
`Bun.write` to a pipe or file | oven-sh#41456 | the write error |
| HTMLRewriter input cancel | oven-sh#42130 | the handler error or an
`AbortError` |
| `reader.cancel(reason)`, `pipeTo` error or abort, `tee()`/`clone()`,
`Readable.fromWeb().destroy(err)`, `fetch()` abort | oven-sh#41757 | the
caller's reason |

The producers are not changed. Direct and default streams still get the
reason in `cancel(reason)`.

**Rulings this follows**

- oven-sh#13687 (in every release since 1.1.27) wrote the abort arm: "undefined
=== Abort. We don't want to throw here because it will almost inevitably
become an uncatchable exception."
- `docs/guides/http/sse.mdx:19`: "If the client disconnects, the
generator's `finally` block runs so you can clean up."
- oven-sh#41894 lists "Async generator and `Response` bodies" under "Not
changed".
- oven-sh#33020 and oven-sh#33167 set the rule for pipes: a sink that closes under an
async iterable returns the generator. oven-sh#36087 made the pump wait on a
FileSink write, which put the error tail in front of that close.

**What it reverses**

- oven-sh#41757 stated "`cancel(reason)` throws the reason into the generator
(as `Bun.serve` already does on abort)". At that commit `Bun.serve`
called `cancel(undefined)`, which returned the generator. Its test
`cancel(reason) on an async generator body throws the reason into the
generator` now expects `finally` and no `catch`.
- oven-sh#41894's test `async generator body: client abort between yields
throws the connection-closed error into the generator once, then
finally` now expects `["finally"]`, which is what 1.4.2 gives.
- The error tail has called `iterator.throw?.(error)` since 1.0.27. No
existing test observed it.
- Not reversed: what `return()` itself throws still replaces the body's
error (oven-sh#39913). A rejected `return()` promise does not.

**Results** (debug ASAN build unless a build is named)

| Check | main bd599f5 | this PR | 1.4.2 |
| --- | --- | --- | --- |
| The 8 ledger scripts at exit 0 | 0 of 8 | 8 of 8 | 7 of 8 |
| Twelve ways a JS consumer leaves, iterator not returned | 7 of 12 | 0
of 12 | 12 of 12 |
| A body that ends its own controller, then yields: shapes left open (of
4) | 2 | 0 | 0 |
| `Bun.write` to `/dev/full`, 64 KiB chunks | pulls for ever | rejects
`ENOSPC` after 1 pull, body closed | pulls for ever |

node v26.3.0 returns the iterator in 12 of 12 ways and never calls
`throw()`.

New tests on release builds of main and of this PR:

| File | main | this PR |
| --- | --- | --- |
| `test/js/web/fetch/body-async-iterator.test.ts` | 10 pass, 40 fail |
50 pass |
| `test/js/bun/http/async-iterator-stream.test.ts` | 97 pass, 42 fail |
139 pass |
| `test/js/workerd/html-rewriter.test.js` (new rows) | 10 fail | 10 pass
|
| `test/js/bun/spawn/spawn-stdin-readable-stream.test.ts` (new rows) |
10 fail | 10 pass |
| `test/js/bun/io/bun-write.test.js` (new rows) | 14 fail | 14 pass |
| `test/js/bun/http/serve-direct-readable-stream.test.ts` (rewritten
pin) | 1 fail | 1 pass |

The 5 new cells that pass on main are the `reader.cancel()` controls
with no reason. Each cell runs five body shapes from
`test/js/web/streams/async-iterable-body-shapes.ts`: an iterator with
`next()` and `return()`, one with `throw()` too, a generator with
try/catch/finally, a generator whose loop catches, and a node `Readable`
with `autoDestroy: false`.

**The two commits**

- Commit 1 changes only the cancel hook. Built alone, the 8 ledger
scripts exit 0. It leaves the end-then-yield route open for 2 of 4
shapes, and `/dev/full` with 64 KiB chunks still pulls for ever.
- Commit 2 changes the error tail and adds the rejected-write branch.
Both routes above need it.

**Still open, for a maintainer**

- A generator acts on `return()` at a `yield`. One that waits in an
`await` is returned when that `await` settles (the same on 1.4.2). The
docs paragraph says so and shows `req.signal`.
- A generator that writes through the yielded controller in a loop and
never yields again is not stopped. 1.4.2 stopped it because `write()`
threw once the client left. Since oven-sh#41894 `write()` returns 0. The
question is which of the two a write through the yielded controller must
do after the consumer left.
- A `return()` that throws synchronously while `Bun.serve` aborts the
body is an uncaught exception, as on 1.4.2.
- Open PR oven-sh#44181 adds three tests that expect a refused chunk's error in
a generator's `catch` (`thrown: outOfMemory`). They need a `finally`
flag so that neither PR turns main red.
- Not run: Windows and macOS. The stdin rows are skipped on Windows
because `fs.closeSync(0)` does nothing there. HTTPS, HTTP/2 and HTTP/3
share the abort path by reading, and only HTTP/1.1 was run.

**Measurements** (release builds of main bd599f5 and of this PR)

- Release text (`size`): 88,771,341 to 88,770,061 B (-1,280 B). Reaction
handlers per global: 98 to 98.
- Cells per chunk (`heapStats` slope, GC off, 2,000 to 6,000 chunks, 3
runs): 3.000 to 3.000 on `Bun.serve` and `.bytes()`.
- Per client abort of a generator body (N=200): Promise 10 to 9,
reactions 6 to 5.
- `body.cancel(err)`: an iterator with `throw()` Promise 4 to 3. An
iterator with `return()` only Promise 2 to 3, because `return()` is now
called.
- Error tail per failing body under `.text()` (N=1000): a generator that
throws 13 to 9 cells and 4 to 3 microtask turns. A refused chunk with a
generator 14 to 15 cells and 6 to 7 turns. A refused chunk with `{next,
return}` 9 to 13 cells, because `return()` is now called.
- End of a body whose `return()` gives a plain object and not a promise:
10 to 12 cells. The result is now awaited, so that a thenable or a lazy
`Bun.sql` promise runs.
- Static (`llvm-objdump`): the four iteration-protocol exception checks
in the pump go from 3 instructions to 2. A `write()` that returns a
promise gets one more status compare.
- Not measured: dynamic instruction counts. `perf` hardware counters
fail in the build VM and valgrind could not be installed.

**Suites run** on the debug ASAN build: the six files above,
`streams.test.js`, `body.test.ts`, `body-stream.test.ts`,
`fetch-args.test.ts`, `serve-stream-body-error.test.ts`, and the two
main files with `BUN_JSC_validateExceptionChecks=1`. In a CPU-capped
container the subprocess tests of `serve-direct-readable-stream`,
`spawn-stdin-readable-stream`, `bun-write`, `streams` and
`serve-stream-body-error` time out at the default concurrency. All pass
with `--max-concurrency 4 --timeout 60000`. Two older chunk-count tests
in `async-iterator-stream.test.ts` (`async generator function`,
`[Symbol.asyncIterator]`) fail in 2 of 6 debug runs with the new tests
filtered out.

**Self-review, the one concern not taken:** deleting the `lastResort`
overload of `atStreamsBoundary`. The error tail still uses it, because
what `return()` throws still replaces the error.

</details>

<!-- robobun:evidence:begin -->

---

**no test proof** · iteration 0 · platform-specific test(s) that do not
run on this machine, deferring to CI, which covers all platforms:
test/js/workerd/html-rewriter.test.js,
test/js/bun/spawn/spawn-stdin-readable-stream.test.ts,
test/js/bun/io/bun-write.test.js,
test/js/bun/http/serve-direct-readable-stream.test.ts

<!-- robobun:evidence:end -->
…ven-sh#44892)

### Problem
- `fetch()` keeps a string body when it ends before it queues a request:
48 rejected calls with a 4 MiB body grow RSS by 192 MiB. `Bun.spawn` and
the shell do the same with a `Response` that they refuse.
- Cause: the string arm of `blob::Any`
(`src/runtime/webcore/Blob.rs:5998`) was a raw pointer that owns one
reference, and a drop did not release it. `fetch_impl` drops it at 18
exits, `Stdio::extract_blob` at 2.

### Fix
- The arm holds `bun_core::WTFString`: one reference on a
`WTF::StringImpl`, released on drop. Drop glue releases it at every
exit. No exit is edited.
- The reference moves at three sites, with no ref and deref pair.
- Verified: 26 new cases in `test/js/web/fetch/fetch-args.test.ts` and
`test/js/bun/spawn/spawn-empty-arrayBufferOrBlob.test.ts` fail on main.
19 more suites ran under ASAN: no report.
- Self-reviewed: 5 concerns raised, 5 addressed.

### Background
- `blob::Any` is the body of `fetch`, `Bun.serve`, `Bun.spawn` and the
shell: a `Blob`, bytes, or a JS string held by reference.
- oven-sh#40411 retypes the same arm, and its tip would release the string too.
It is stacked on two open PRs and has no test for this. This PR moves
the reference where oven-sh#40411 clones it, and adds 9 conflict hunks to its
rebase.
- Considered a guard in `fetch_impl` or `Drop for HTTPRequestBody`: both
leave `Bun.spawn` and the shell.

### Downsides
- `.text` grows 8,448 B: LLVM inlines the smaller `Any::detach` at more
call sites. Measured common paths run 7 to 24 fewer instructions.
- A wrong hand-off is now a double release, not a leak. 50 refused
fetches destroy 50 strings (main: 0).

<details><summary>Notes</summary>

**For a maintainer to decide**

1. The order with oven-sh#40411. Its commit ec572ec has the same arm type
and a byte-identical `ExternalShared::into_raw`, so its tip would
release a dropped string too. It is stacked on oven-sh#40221 and oven-sh#40213, was
last updated on 2026-09-08, and has no test for this leak. A merge of
its head with main conflicts in 39 files. With this PR the count stays
39, and two files gain hunks: `Blob.rs` 51 to 55 and `Body.rs` 26 to 31
(`git merge-tree`).
2. Move or clone at a hand-off. oven-sh#40411 clones the handle and drops the
old one at each hand-off, which is one `ref` and one `deref`. This PR
moves the reference, so the hand-offs add no atomic write.
3. The 8,448 bytes. `Any::detach` is 28 bytes smaller, and LLVM now
inlines it at more call sites. `#[inline(never)]` on it would give the
bytes back and put the call back on those paths. This PR keeps LLVM's
choice.

**Where the string stayed**

- `fetch_impl` has 29 exits between the point where it reads the body
and `FetchTasklet::queue`. 4 of them call `detach()`. 25 do not. A
string body can reach 18 of the 25 (3 of them only on Windows). The
other 7 need a `Blob`, `FormData` or stream body, which main releases
already.
- The 18 include the `?` on the headers (an invalid header name, a
getter that throws), proxy with unix, `file:` with a host, a `blob:` URL
that is not registered, an unsupported protocol, GET with a body, the S3
option and signing errors, and the resolved 200 for `file:` and `blob:`
URLs.
- `Stdio::extract_blob` takes the `Any` by value and returns an error at
`stdio.rs:619` and `stdio.rs:632`. `Bun.spawn`, `Bun.spawnSync` and the
shell (an external command with `>` or `2>` into a `Response`) reach it.
The shell does not accept a `Request`.
- The `fetch` half starts with 1.4.0: the Zig `fetchImpl` released the
body in one `defer` (`fetch.zig:256` at `bun-v1.3.14`). The spawn and
shell half is older: 1.3.14 keeps the string too.
- A string with a character outside ASCII, bytes and a `Blob` body do
not use this arm on these paths and were always released.

**The three sites that move the reference**

- `Value::take_wtf_string` (`Body.rs`): from `body::Value` into the arm,
for `use_as_any_blob`, `try_use_as_any_blob` and
`use_as_any_blob_allow_non_utf8_string`. `Value` keeps its raw arm and
its `impl Drop`, so a by-value move out of it does not compile (E0509).
The helper forgets the arm and adopts the pointer. That is the one new
`unsafe` block.
- `Any::take_string` (`Blob.rs`): from the arm into a
`bun_core::String`, for `to_string`, `to_json` and
`to_array_buffer_view`. It is a by-value `match` on `mem::replace`.
- `Value::from(any)` (`Body.rs`): from the arm back into `body::Value`,
through `into_raw`. No caller passes a string there today.

**Not in this PR**

- `body::Value` keeps its raw string arm. oven-sh#40411 retypes it.
- The explicit `detach()` calls after `use_as_any_blob*` stay. They are
no longer needed for the string.
- The fd of a `Bun.file()` body that `fetch` sends with sendfile is
closed only by `HTTPRequestBody::detach`. No exit reaches that state
today. A separate fd leak there (`Bun.file(path).slice(0, 0)` as the
body) has another cause and is tracked on its own.
- oven-sh#41812 makes `Bun.spawn` refuse a `Request` or `Response` for stdout
and stderr before it takes the body. It does not touch `fetch`.

**Which thread drops the string**

Every holder drops its `Any` on the JS thread: the `fetch_impl` local,
`FetchOptions`, `FetchTasklet.request_body` (the tasklet hops to the JS
thread for its teardown), `RequestContext.blob`, `Stdio::Blob` and the
pipe writer behind it, the shell command, `HTMLRewriter`, WebAssembly
streaming and `node/types.rs`. `Bun.write` and `fs.writeFile` copy the
bytes into a `Blob` first.

**Tests**

- Each case runs one child process: 8 warm-up calls, then 48 calls with
a fresh 4 MiB ASCII string, then `Bun.gc(true)` and an RSS read. A
release build of main grows 192 to 194 MiB in all 26 cases. With the fix
it grows 0 to 2 MiB.
- A debug build with the fix grows 8 to 45 MiB. A loop that only
flattens the string, with no `fetch` at all, measures the same there. So
the bound is 48 MiB for release and 96 MiB for debug.
- `fetch` cases (19): unsupported protocol, GET with a body, invalid
header name, a headers getter that throws, proxy with unix, `file:` with
a host, `blob:` not registered, `blob:` registered, `file:`, S3 signing
error, S3 option of the wrong type, `Bun.fetch`, `FetchSession.fetch`,
`node-fetch`, `undici.fetch`, `undici.request`, a `String` object, a
`Request` that carries the body, and an init object as the first
argument.
- Spawn cases (7): `Bun.spawn` stdout, stderr and `stdio[3]`,
`Bun.spawnSync` stdout, a `Request` for stdout, and the shell with `>`
and `2>`.
- The cases run one child at a time. On a debug build, 19 children at
once do not finish within the default timeout.
- The tests are not in `fetch-leak.test.ts`, `spawn.test.ts` or
`shell/leak.test.ts`. On a debug build of main those three files take 8
to 12 minutes here and have tests that time out, so a before and after
run of the whole file proves nothing.

**Measurements** (release builds of main 620b50f and of this diff on
it, linux x64. perf, valgrind and bloaty are not in the container: the
counts are from gdb single-step, GNU objdump, nm and size)

- Leak: two scripts of 8 cells each (refused `fetch` calls and refused
`Bun.spawn` calls, with bodies of 256 KiB or more) go from 8 of 8
growing cells to 0 of 8. A probe of 41 rows (more `fetch` forms and the
other consumers of a string body): 8 growing rows to 0. A shell redirect
into a `Response`: 338 and 356 KB per call to 18 to 76 KB, which is the
level of a row that only builds and drops a `Response` (13 to 80).
- One release per body (gdb breakpoint on
`WTF::StringImpl::~StringImpl`, bodies of 262,144 characters): 50
refused fetches destroy 0 strings on main and 50 here (control with no
fetch: 50). 50 refused `Bun.spawn` calls with stdout and with
`stdio[3]`: 0 and 0 on main, 49 and 49 here (control that drops the
`Response`: 49).
- Executed instructions, callees included (gdb `stepi` from entry to
return, hits 2 and 3 identical): `new Response(string).text()` 179 to
157. `.json()` 32,459 to 32,435. `.bytes()` 1,817 to 1,794.
`.arrayBuffer()` 1,996 to 1,973. `FetchTasklet` teardown for GET, a
string body and a bytes body: 629, 625, 655 to 618, 614, 644.
`RequestContext::finalize_without_deinit` for `Bun.serve` that returns
`new Response(string)`: 266 to 256. The number of lock-prefixed
instructions is equal in every row.
- Executed instructions inside one function (callees stepped over, 3 of
3 identical): `Any::to_string` 58 to 49. `Any::to_json` 63 to 52.
`use_as_any_blob_allow_non_utf8_string` 40 to 43: the hand-off helper
checks the arm once more.
- `fetch_impl` on a fetch that reaches the network: 986 to 986
instructions for a POST with a 1 KiB string body, 755 to 755 for a GET.
The function is 23,718 bytes in both builds.
- Lock-prefixed instructions, static: the three `use_as_any_blob*`
functions 0 to 0. `Any::to_string` 1 to 1. `Any::to_json` 2 to 2.
`Any::detach` 1 to 0. Drop glue of `blob::Any` 2 to 3. The one `deref`
moved from `detach` into the drop glue.
- `size_of` in a release profile: `blob::Any` 112 to 112 bytes,
`HTTPRequestBody` 112 to 112, `Stdio` 112 to 112, `body::Value` 168 to
168, `FetchOptions` 664 to 664, `FetchTasklet` 832 to 832. Debug: 384,
384, 384, 384, 936 and 1,128, unchanged.
- Release binary: `.text` 65,659,477 to 65,667,925 bytes (+8,448). File
89,142,856 to 89,151,048 bytes (+8,192). 51 symbols change size, net
+9,858 bytes. `Any::detach` 196 to 168 bytes. `Stdio::extract_blob` 916
to 1,293 bytes and `Cmd::transition_to_exec` +1,392 bytes: they gain the
release on their exits. The 8 `RequestContext::finalize_without_deinit`
instances gain 195 bytes each from the inlined `detach`.
- `unsafe` lines in `src/`: +1, -0. New `bun_core` surface:
`ExternalShared::into_raw` (3 lines).

**Suites run on the debug build with ASAN and `Malloc=1`** (so that ASAN
sees the frees of WTF strings)

`fetch-args`, `spawn-empty-arrayBufferOrBlob`, `body`, `body-clone`,
`blob`, `response`, `fetch`, `fetch-leak`, `wasm-streaming`,
`html-rewriter`, `html-rewriter-leak`, `bun-write`, `serve`,
`bun-serve-static`, `bun-server`, `spawn`,
`spawn-stdin-readable-stream`, `spawn-stdin-readable-stream-edge-cases`,
`bunshell`, `shell/leak` and `shell/yield`. No ASAN report. The failures
are timeouts of the debug build, and tests that need a network or a user
that is not root. Also run: `cargo check -p bun_runtime` for
`x86_64-pc-windows-msvc` and `aarch64-apple-darwin`, `cargo fmt`, and
`bun check` in `test/` (no error in the two test files).

**Self-review**

5 concerns, all addressed: three sentences of this description (how the
reference left the arm on main, the shell and `Request`, and which
strings use the arm), the two counts (18 of 25 exits, and three move
sites, not two), `Any::take_string` as a by-value move, the measured
numbers in place of a placeholder, and comment blocks cut to one line.
One part was not taken as asked: the comment on `Value::take_wtf_string`
gives the reason for its `unsafe` (E0509) and does not name oven-sh#40411,
because a comment that names an open PR goes stale.

</details>

Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
…line took cubic time (oven-sh#44890)

Two things: `bun check --threads=1` dropped errors, and an array without
a type that is filled line by line took cubic time.

## `--threads=1` dropped errors

It could print "No type errors" over real ones. This is a regression
from oven-sh#44807 and it is in the canary of e555359. The default number of
threads, and `--threads=2` and more, were not affected.

A file whose top-level code is too long for control flow analysis gets
TS2563. That starts at about 2,000 statements, so it is usually a
generated or vendored file. With `--threads=1`, most errors of every
file after it were dropped, and right code got false errors.

With the `tsconfig.json` of `bun init` it is silent. `allowJs` is on
there and `checkJs` is not, so a long `.js` file does not show its own
TS2563 either.

`a-gen.js` is `const data = [];`, then `data[0] = 0;` 10,000 times, then
`export default data;`.

```ts
// z-app.ts
declare let s: string;
export const a: number = s;
export function f(x: string): number { return x; }
export const b = s.nope;
```

| `bun check` | canary of e555359 | this branch |
|---|---|---|
| `--threads=1` | No type errors, exit 0 | 3 errors, exit 1 |
| `--threads=2` | 3 errors, exit 1 | 3 errors, exit 1 |
| `--threads=8` | 3 errors, exit 1 | 3 errors, exit 1 |

Now control flow analysis is switched off for the file that is too long,
and for no other.

That also holds where files refer to one another. If the long file,
behind its limit, reads a variable or calls a function of another file,
that file keeps its errors. And if that other file is too long as well,
neither switches the analysis on again for the other.

`tsc` 7.0.2 never switches control flow analysis on again. On the
project above `tsc --singleThreaded` reports nothing, and `tsc` reports
the 3 errors only because another one of its checkers has `z-app.ts`.
`bun check` does not copy that. The docs list it among the differences
from `tsc`.

## An array without a type that is filled line by line

```ts
const rows = [];
rows.push({ id: 0 });
rows.push({ id: 1 });
// and 998 more
```

Twice the lines took eight times as long. This is old. Seed data,
fixtures and generated tables are written like that, and nothing in the
output said which file was slow.

CPU time of `bun check`, and of `tsc` 7.0.2, for one such file and one
small file:

| Each line | Lines | canary of e555359 | this branch | `tsc` |
|---|---|---|---|---|
| `rows.push({ id: 0 })` | 1,000 | 15.92 s | 0.18 s | 0.26 s |
| `rows.push({ id: 0 })` | 1,900 | over 60 s | 0.71 s | 0.89 s |
| the same in a `.js` file that is not checked | 1,000 | 15.85 s | 0.27
s | 0.27 s |
| the same in a `.js` file that is not checked | 1,900 | over 60 s |
0.71 s | 0.86 s |
| `rows[0] = { id: 0 }` | 1,000 | 15.85 s | 0.20 s | 0.38 s |
| `rows[0] = { id: 0 }` | 1,900 | over 60 s | 0.75 s | 1.39 s |
| objects of 4 shapes | 1,000 | 20.52 s | 0.19 s | 0.27 s |
| objects of 4 shapes | 1,900 | over 60 s | 0.76 s | 0.80 s |
| `if (c) rows.push({ id: 0 })` | 500 | 5.58 s | 0.15 s | 0.33 s |
| `if (c) rows.push({ id: 0 })` | 1,000 | 12.75 s | 0.21 s | 0.63 s |

The types are the same as before.

## How it was verified

Every project below on 1, 2, 4 and 8 threads. In each of them every
error that was planted in a file that is not too long has to be
reported, and a file of right code has to stay silent.

| Projects | an error is missing on main | on this branch | in `tsc` |
in `tsc --singleThreaded` |
|---|---|---|---|---|
| 60: 5 long shapes, as a checked file, as JavaScript that is not
checked, with `@ts-ignore` and with `@ts-nocheck`, in front of, among
and behind 13 files with one error each | in 18 | in 0 | in 18 | in 18 |
| 36: the long file and another one refer to one another in 6 ways, the
long file first and last, among 0, 30 and 90 other files | in 18 | in 0
| in 14 | in 18 |

| 12 programs with two files that are too long, in the 6 orders of the
files | main | this branch |
|---|---|---|
| Programs with more than one output | 12 | 0 |

For the arrays: 9 ways to add an element, 10 things between the
additions (conditions, assertions, `!`, loops, closures), at the top
level and in a function, with the types of the array and of an element
after 3 and after 12 additions. 180 programs, 811 errors. With
`--threads=1` the output is that of `tsc`, before and after. On all
threads 8 lines print `?: undefined` elsewhere, before and after, as
`tsc` itself does between its two modes.

`check.test.ts` has five new tests. All of them fail on the canary and
pass here. The one for the arrays fails there by its timeout.

| Everything else | Result |
|---|---|
| TypeScript's own suite, 5 kinds of baseline | 100% of each, as before
|
| 2,512 small programs that once differed, against `tsc`, on all threads
and on one | no change |
| 47 real projects, on all threads and on one | the same output as main
|
| 317 builds with project references | the same output as main |
| The 47 projects: the errors on one thread against those on all threads
| the same in all 47 |
…bic time (oven-sh#44895)

A long chain of interfaces or classes, each of which extends the last,
took cubic time.

```ts
interface I0 { k0: number }
interface I1 extends I0 { k1: number }
interface I2 extends I1 { k2: number }
// and 1,997 more
```

Twice the interfaces took eight times as long. This is old. Generated
typings are written like that.

CPU time of `bun check`, and of `tsc` 7.0.2:

| Each interface adds | Interfaces | main | this branch | `tsc` |
|---|---|---|---|---|
| a property | 500 | 0.26 s | 0.04 s | 0.09 s |
| a property | 1,000 | 2.00 s | 0.10 s | 0.19 s |
| a property | 2,000 | 15.28 s | 0.56 s | 0.68 s |
| a method | 500 | 0.28 s | 0.05 s | 0.08 s |
| a method | 1,000 | 1.86 s | 0.10 s | 0.18 s |
| a method | 2,000 | 15.56 s | 0.53 s | 0.68 s |

## Members that mention `this` or a type parameter

The same for a chain with any other kind of member, in classes too.
`tsc` is cubic there as well, so it is measured with 500 only.

CPU time of the check on one thread, and of `tsc` with
`--singleThreaded`:

| Each one adds | main, 1,000 | this branch, 1,000 | this branch, 2,000
| `tsc`, 500 |
|---|---|---|---|---|
| interface, `k: this` | 4.04 s | 0.55 s | 2.18 s | 8.70 s |
| interface, a method that returns `this` | 4.03 s | 0.68 s | 3.34 s |
8.75 s |
| generic interface, `k: T` | over 25 s | 0.45 s | 2.02 s | 10.04 s |
| class, a method without a return type | 4.37 s | 0.63 s | 3.02 s |
8.95 s |
| class, a field with an initializer | 3.59 s | 0.31 s | 1.81 s | 9.83 s
|
| class, a parameter property | 4.02 s | 0.41 s | 1.63 s | 8.92 s |
| class, a pair of accessors | 3.66 s | 0.34 s | 1.85 s | 8.26 s |

On main each of them takes over 25 s with 2,000.

The errors and the types are the same as before.

## How it was found

The arrays of oven-sh#44890 were one shape of generated code that `bun check`
was slow on and `tsc` was not. So I ran 61 such shapes at 500, 1,000 and
2,000 lines, both tools on one thread, and compared the time and the
number of errors. Seed data, fixtures, tables, long unions with a
`switch`, chains of `else if`, spreads, builders, overloads, enums,
JavaScript that assigns to `exports`, `this` and prototypes, API
clients, mapped and conditional types, tuples, `satisfies`, merged
declarations, callbacks with contextual types.

This chain was the only one where `bun check` took more than twice as
long as `tsc` and more than half a second. In most of the others it
takes about half the time of `tsc`.

## How it was verified

What changes is when an inherited member counts as the same member. So:
5 kinds of owner, 12 kinds of member with and without `this`, plain,
generic, inside a generic function and merged, each in the 12 situations
where that decides something. Two interfaces with a common base that are
extended together, overriding a member with the same and with another
type, assigning both ways, unions and intersections of two heirs,
`keyof`, indexed access, spread, a chain of six. 192 programs, 2,527
errors, with the type of the member probed in each. The output is that
of `tsc`, before and after, on all threads and on one.

132 more programs try to make the base type of an heir matter to a
member from further up. Heirs that rename and wrap their type parameters
over four levels, with 10 kinds of member, outer type parameters,
mixins, classes inside generic classes and functions, bases that are
mapped types, intersections, arrays and tuples, defaults, statics,
overriding half way, a common base. Every inherited member is probed
through the last heir. 891 errors, the output of `tsc` before and after,
on all threads and on one.

`check.test.ts` has two tests, one with 2,000 interfaces and one with
1,000 of three of the other kinds. Both run into their timeout on main.

| Everything else | Result |
|---|---|
| TypeScript's own suite, 5 kinds of baseline | 100% of each, as before
|
| 2,512 small programs that once differed, against `tsc`, on all threads
and on one | no change |
| 47 real projects, on all threads and on one | the same output as main
|
| 317 builds with project references | the same output as main |
| Instructions for 9 configs, among them vscode, next.js and storybook |
+0.2% against main, and main against itself gives +0.5% |
…ps, WebSocket, SQL) (oven-sh#44618)

### What does this PR do?

Consolidates the open TLS pull requests into one. Each was reproduced on
`main` and, for `node:*` behavior, on Node v26.3.0 first. About a third
are ported as written, the rest are rewritten smaller or merged into one
fix where several PRs patched the same cause. One commit per fix, so it
can be read commit by commit.

Fixes oven-sh#43520, fixes oven-sh#31396, fixes oven-sh#43635, fixes oven-sh#37193, fixes oven-sh#43846,
fixes oven-sh#17932, fixes oven-sh#41061, fixes oven-sh#36887, fixes oven-sh#31810, fixes oven-sh#35240,
fixes oven-sh#32234, fixes oven-sh#44365, fixes oven-sh#43807, fixes oven-sh#42280, fixes oven-sh#44517.
Addresses oven-sh#41856 (SNI and `servername`; not `checkServerIdentity` for
SQL), oven-sh#24845 (the spin is gone, shown with fault injection on Linux; not
run on macOS), oven-sh#19754 (node-fetch forwards the agent's TLS options; the
Kubernetes client itself was not run).

#### The ones that matter most

| | On `main` | PRs |
|---|---|---|
| Client certificate disclosure | `https.request()` with a client
certificate sends it to a server it then refuses (wrong name,
`checkServerIdentity`, `destroy()` in `'secureConnect'`, `terminate()`
in `handshake`). A server can force it with a junk record behind its
Finished | oven-sh#43946 |
| False `authorized` | Over a Duplex, `secureConnect` with `authorized
=== true` for a peer that failed the key proof; `secureConnect` for a
plaintext peer with `rejectUnauthorized: false` | oven-sh#44422, oven-sh#32929 |
| Cleartext https | `https.createServer()` without a usable key/cert
answers plain HTTP | oven-sh#41672, oven-sh#33539 |
| Revoked client certificates | An https mTLS server never sees `crl`,
so a revoked client is `authorized` | oven-sh#41641 |
| Pooled sockets | Requests with different client certificates or CAs
share an `https.Agent` socket and session | oven-sh#42498 |
| Silent plaintext | `tls: [...]` given to `Bun.listen` / `Bun.connect`
is plain TCP | oven-sh#41490 |
| Server weakened by a client knob | `NODE_TLS_REJECT_UNAUTHORIZED=0`
turns off a server's client-certificate enforcement | oven-sh#35245 |
| Pins never checked | `WebSocket` never calls `tls.checkServerIdentity`
and ignores `tls.serverName` | oven-sh#41648 |
| `verify-full` dropped | `PGSSLMODE=verify-*` is lost next to a `TLS_*`
URL variable; `tls: true` sends no SNI | oven-sh#44498 |
| Crashes | use-after-free from `destroy()` in `ALPNCallback` over a
Duplex; `abort()` on a late `setSession()`; SIGABRT in `fetch` with an
https proxy from the environment and a `Bun.file()` body | oven-sh#44462,
oven-sh#41671, oven-sh#44458 |
| Stream corruption | A TLS `write()` can lose 16 KiB it reported as
written while another socket on the loop is stalled | oven-sh#44529 |
| Hangs and spins | 100% CPU on a failing `send()`; a fatal `SSL_write`
leaves the socket open forever; `idleTimeout` never sheds a TLS client
that ignores `close_notify` | oven-sh#34510, oven-sh#38176, oven-sh#42336 |
| Wrong certificate (regression since 1.3.14) | Connections accepted
before `stop()` / `close()` get the default certificate and skip their
entry's `requestCert` / `ca` | oven-sh#42355 |
| Quadratic Duplex / proxy tunnel | Reading one chunk over a Duplex,
CPU: 8 MB 0.88 s → 0.14 s, 16 MB 3.18 s → 0.23 s, 32 MB 11.75 s → 0.39
s; `fetch` upload through CONNECT: 1.7 s → 0.18 s (debug build) | oven-sh#44464
|

#### By area

- **fd engine, write path** (`openssl.c`, `socket.c`): oven-sh#42352, oven-sh#34510 +
oven-sh#38176 + oven-sh#42336 as one change, oven-sh#44529, oven-sh#44458, oven-sh#44192. A rejected
`send()` ends the write side only and closes at the next writable event
unless the peer's bytes are still queued (a 413 sent before a reset is
still read). No new per-socket state. Also, on kqueue, **a FIN no longer
ends a socket that waits in the low-priority queue** (`loop.c`): with
more than 5 TLS handshakes at once, a client that ended right after its
handshake could be reset and its server socket report `socket hang up`,
because the eof that the sentinel read knote reports was acted on ahead
of the unread Finished. That is on `main` too (the macOS entry for
`node-tls-server.test.ts` in `test/flaky-tests.txt`: 7 of 48 recent
builds of other branches), and this branch made it likelier (6 of 8
builds), since Finished now leaves in one segment with the close_notify.
- **Error reporting, both engines**: oven-sh#44422, oven-sh#32929, oven-sh#44516, oven-sh#37094,
oven-sh#41272 + oven-sh#42324 + oven-sh#44223 as one change, oven-sh#44021, oven-sh#37472, oven-sh#43946, oven-sh#33630.
One channel: a fatal error on an established session is reported, then
**the engine closes the connection itself**, whatever the owner does
with the report. `test/js/bun/net/tls-fatal-error-closes.test.ts`
asserts closed-and-nothing-delivered for every owner (node:tls,
`Bun.connect`, `Bun.listen`, `fetch` direct and through CONNECT,
`Bun.serve`, `WebSocket` direct and through a proxy, Postgres, MySQL,
Valkey, Duplex).
- **Duplex engine** (`SSLWrapper`, `UpgradedDuplex`): oven-sh#44462, oven-sh#43529,
oven-sh#42332, oven-sh#44464. oven-sh#43877 + oven-sh#44394 were in and are **out again**, see
"Worth a look" 5.
- **node:tls wrap lifecycle** (`net.ts`, `tls.ts`): oven-sh#38007, oven-sh#38058,
oven-sh#38028 + oven-sh#38122 + oven-sh#38076 as one change (six copies of the attach code
become two helpers), oven-sh#38311, oven-sh#39008, oven-sh#38154, oven-sh#42340 + oven-sh#42343 + oven-sh#42339 +
oven-sh#42453 as one change, oven-sh#43791, oven-sh#42425, oven-sh#44085, oven-sh#42683, oven-sh#39088, oven-sh#39040,
oven-sh#40375, and what was still real of oven-sh#36534.
- **SNI, ALPN, server contexts**: oven-sh#43080, oven-sh#42050, oven-sh#37195 + oven-sh#43849 as one
change (**one** SNI matcher for TCP and HTTP/3), oven-sh#42355, oven-sh#42285, oven-sh#33253,
part of oven-sh#37896, part of oven-sh#37013. A `tls.Server` has one `SSL_CTX`.
- **Verification and options**: oven-sh#44738, oven-sh#41490, oven-sh#37005 + the cwd pin of
oven-sh#40984, oven-sh#31811, oven-sh#43982, oven-sh#33483 + oven-sh#35245, oven-sh#41810, oven-sh#32235, oven-sh#44441, oven-sh#38092.
- **node:tls API and CA store**: oven-sh#41671, oven-sh#38145, oven-sh#32824, oven-sh#43594, oven-sh#39997,
oven-sh#41696, oven-sh#33534, oven-sh#34748, oven-sh#42991, oven-sh#42996, oven-sh#42970.
- **node:https, Agent, `ws`, node-fetch**: oven-sh#41672 (https half), oven-sh#41641,
oven-sh#38261, oven-sh#42498, oven-sh#44346, oven-sh#35609, oven-sh#31397, oven-sh#42325.
- **WebSocket client**: oven-sh#41648, oven-sh#37487 + oven-sh#43048 as one change.
- **SQL, Redis**: oven-sh#33666, oven-sh#41711, oven-sh#44498, part of oven-sh#42054.
- **Tests only**: oven-sh#41426, oven-sh#40040, oven-sh#44395, oven-sh#44016, oven-sh#37860, oven-sh#40591,
oven-sh#44440, oven-sh#41424.

Found on the way and fixed here: an upload that a TLS 1.2 server
interrupts with a renegotiation never completes on `main` (0 of 32 runs
over `https.request`, `fetch`, `node:tls` and `Bun.connect`: the
renegotiation ClientHello lands inside an application record that is
still unsent, or the socket gets no `drain` again) and completes here,
with two tests from robobun; the fix for oven-sh#40653 (final flight and first
write in one segment) stopped working whenever another TLS socket on the
loop was stalled, on `main` too; the `tls.Server` prototype pinned the
last server constructed and every `SSL_CTX` it owned;
`Object.create(process.env).NODE_TLS_REJECT_UNAUTHORIZED = "0"` turned
verification off process-wide once a `SHARE_ENV` worker existed; two
debug panics when wrapping a shut-down or still-connecting socket; a
`fetch` POST through a proxy sent its headers twice when the origin
renegotiated; `BlockList` ignored IPv6 zone ids; a test now ties
`root_certs.der` to `certdata.txt`.

#### Behavior changes

- **A server's `ca` without `requestCert` no longer asks for a client
certificate** (`Bun.serve`, `Bun.listen`, HTTP/3, node:tls). It matches
the docs and Node. On `main` such a server refused clients with no
certificate but served any unrelated self-signed one, so it was never
authentication. **Set `requestCert: true` to require a certificate.** A
matrix test pins that `requestCert: true` still refuses no certificate
and an untrusted one on 8 kinds of server, TLS 1.2 and 1.3, with
`NODE_TLS_REJECT_UNAUTHORIZED` unset and `0`.
- `NODE_TLS_REJECT_UNAUTHORIZED=0` no longer relaxes a server.
- `Bun.connect` / `Bun.listen` hear of a fatal TLS error after the
handshake through `error(socket, err)`. With no `error` handler the
socket just closes.
- HTTP/3 server names match like TCP: `*.` covers exactly one label,
case is ignored, a trailing dot is ignored, the last registration of a
name wins.
- `requestCert` on node:https is `=== true`, as in Node.
- An array where a generated options dictionary is expected throws
(`tls: []`, `jest.useFakeTimers([])`).
- `key` / `cert` arrays serve every identity. A client that can use both
gets ECDSA, where `main` served whichever pair came last.
- `ecdhCurve` is forwarded by node:https, `ws` and node-fetch now, so a
group BoringSSL lacks (`X448`) throws there as it already does in
`tls.createServer`.
- A wrapped socket's error is re-emitted on the TLS socket as in Node,
so `raw.destroy(err)` with a listener on `raw` only is uncaught, as in
Node.
- `sql.options.tls` is always an object, never `true`. `RedisClient`
sends SNI.
- `tls: { secureContext }` alone asks for TLS on `Bun.listen` /
`Bun.connect` (it was plain TCP), and a value that is not a
`SecureContext` throws. The context is served as it is: the
`requestCert` / `rejectUnauthorized` it was created with hold whatever
the options next to it say, and `requestCert` in the options over a
context that does not ask throws at `listen()`.
- `tls.DEFAULT_CIPHERS` reaches every client once assigned (`fetch`,
`WebSocket`, `Bun.connect`, `RedisClient`, `Bun.SQL`, `S3Client`, proxy
tunnels) and servers again. A list that selects no cipher throws
`ERR_SSL_NO_CIPHER_MATCH` at the assignment. `fetch.preconnect()` dials
nothing after an assignment.
- The warning for an unreadable `NODE_EXTRA_CA_CERTS` is Node's one
line, without the `warn:` prefix.
- `BUN_CONFIG_WS_CLOSE_TIMEOUT` (default 30 s): how long a `WebSocket`
client waits for the server to close the connection after the closing
handshake.

#### Worth a look in review

1. **oven-sh#44529**: the kernel-refused remainder of a TLS write moves from
the loop's one slot onto the connection (in the existing rare struct),
so the write BIO never refuses a sealed record. Nothing is allocated on
an unstalled path (200 writes: 0 appends, same `send()` count as
`main`), memory with 16 stalled writers is lower than on `main` (276 KB
vs 340 KB, which `main` holds inside BoringSSL's buffers), `us_socket_t`
stays 80 bytes. It needs a bound on how long a deferred close waits, or
a peer that stops reading pins the fd past `destroy()`:
`US_SSL_CLOSE_AFTER_SPILL_TIMEOUT` is a fixed 10 s, not re-armed on
progress. Separate commits, but the fix that keeps the client
certificate off the wire beside a stalled socket builds on them.
2. **The default name check of node:tls also runs inside the
handshake**, so a wrong-name server gets no client certificate on TLS
1.2 either. JS still runs it after every successful handshake, so a
difference between the two matchers can only refuse. Error objects are
byte-identical.
3. **oven-sh#44441** widens trust by design: a self-issued leaf whose
`keyUsage` lacks `keyCertSign` (`dotnet dev-certs`) is its own anchor
when the store holds a byte-identical copy. No BoringSSL change. Expired
pin, same subject with another key, wrong EKU and a pinned intermediate
are tested to fail.
4. **oven-sh#32235** only adds Ed25519 and ECDSA P-521 to the verify list. A
captured ClientHello shows `main`'s list with the two inserted;
`rsa_pkcs1_sha1` stays.

5. **A stream that a TLS socket wraps, when that TLS socket closes.** An
earlier state of this branch lost data here while CI was green (found by
oven-sh#44709's report): with the peer closing first, 4 of 8 MiB arrived with
TLS in TLS, 4 of 32 MiB on the http2 `emit("connection")` path, and a
`write()` with no `'error'` listener ended the process. Three
Node-parity changes only hold together: destroying the wrapped stream at
the close (oven-sh#38028 + oven-sh#38122 + oven-sh#38076, oven-sh#38154) is safe only if every write
has really completed (oven-sh#43877), which in turn needs Node's handling of
the peer's close_notify, which needs half-open sockets that the GC can
collect. So:
- oven-sh#43877 + oven-sh#44394 are reverted and reopened. A write over a stream
completes once the stream has taken the ciphertext, as on `main`.
- Until the verdict on the peer lets the session through, the
application cannot have written over it. There the wrapped stream is
destroyed as in Node, with the sessions below it. That keeps the release
of the connection after a failed handshake, a rejected certificate and
an early `destroy()`. The same for an http2 socket the application never
got, and for `resetAndDestroy()`.
- After that it is `main`'s teardown: a `net.Socket` only gets the
engine's `end()`, closes at its peer's FIN, keeps its own timeout and
reports its own errors. Any other stream is destroyed with the TLS
socket.

The regular suites cannot see any of this (999 files were green on every
broken variant), so it was steered by eleven seeded differential fuzzers
run on this build, `main`, Node v26.3.0 and the earlier state: close,
`end()`, `destroy()`, `destroySoon()`, resets, hung and half-open peers,
paused writers, timeouts, two and three sessions deep, over TCP and over
Duplexes, before, at and after the handshake, and http2 requests. See
"How did you verify".

#### Known limits

- `fetch` with a `checkServerIdentity` function still sends the client
certificate (not the request) to a server the function refuses. On TLS
1.2 any verdict a JS callback gives is too late, as in Node.
- `addContext()` / `SNICallback` still do not apply to a server-side
socket on the stream engine (`emit("connection", duplex)`, TLS in TLS,
unflushed writes, named pipes), as on `main`.
- A CA bundled in a pfx extends an explicit `ca` only, for `ws` /
node-fetch / `WebSocket`: the native `ca` can only replace the default
store, and that store keeps `SSL_CERT_FILE` / `SSL_CERT_DIR`.
- P-521 leaves work on TLS 1.3 only. TLS 1.2 needs secp521r1 in every
ClientHello (`it.todo`).
- Once `tls.DEFAULT_CIPHERS` is assigned, `fetch(url, { protocol:
"http3" })` is `HTTP3Unsupported`, as with an explicit `ciphers`.
- `addCACert()` by hand does not extend the chains of a context with
several identities.
- A throwing `ALPNCallback` sends `no_application_protocol` on both
engines. Node sends nothing and its client sees `ECONNRESET`.
- TLS in TLS, peer FIN while the outer handshake runs: the inner socket
gets one `write EPIPE`, where Node gives `ECONNRESET` (`main` gives it
no error at all).
- `@SECLEVEL` in `ciphers` is dropped by the `ws` / node-fetch shims,
which used to ignore `ciphers`. node:tls keeps throwing
`ERR_SSL_INVALID_COMMAND`.
- Beside a stalled TLS socket only the first record (16 KiB) of the
first write leaves with the handshake flight. The rest goes record by
record, which is what bounds the memory of stalled writers.
- After a fatal error on an established session the socket emits
`'error'` and then `'close'`. Node emits `'error'` and leaves the socket
open.
- A paused reader whose own write the kernel rejects loses what it had
not read yet, with an `EPIPE`, as on Node. `main` reports no error there
and delivers it.
- On `main` too: a `Bun.listen` socket without `allowHalfOpen` that has
unsent ciphertext when the client's `shutdown()` arrives loses that
ciphertext (32 KiB), and over plain TCP `end()` with the peer still
sending is a close over unread input, so a reset.
- Differences from both `main` and Node that the differential runs below
found and that stay, all with a peer that aborts: `ECONNRESET` instead
of a clean `'end'` after the socket's own `'finish'` when the peer
destroyed with unread data; under TLS 1.2, a zero-length `write()`
followed by `destroy()` in `'secureConnection'` leaves the client
without `'secureConnect'` (a plain `destroy()` there matches Node); a
TLS 1.2 client that destroys in `'secureConnect'` gets no `'session'`; a
`ClientRequest` whose handshake fails with an alert emits `'error'` and
`'close'` but no `'finish'` (`writableFinished` is true).
- Once `tls.DEFAULT_CIPHERS` is assigned, `fetch.preconnect()` opens
nothing: `fetch()` then uses a context of its own, and a socket warmed
under the default one would never be picked up.
- A TLS `send()` that the kernel refuses outside a `write()` call (the
drain of unsent ciphertext) is reported with the close, as `read EPIPE`
/ `read ECONNRESET`. Node says `write EPIPE`. `main` does not report it
at all.
- Once the application has a session over a `net.Socket` (TLS in TLS,
http2 `emit("connection")`), a peer that never sends its FIN holds that
socket after the TLS socket closed, as on `main`. Node destroys it. Two
tests of oven-sh#38154 are `todo` for this. Closing it any earlier (at its
`'finish'`, say) makes the kernel drop what it has not sent yet as soon
as the peer's close_notify arrives.
- Plaintext that was queued on a socket before it was wrapped (STARTTLS
with a backlog) is dropped when the TLS socket is destroyed, or its
handshake fails, before the session is accepted. Node drops it too,
except on `destroySoon()`. `main` sends it.
- Over a stream that is no `net.Socket`, `end()` can still cut what that
stream has buffered, and there is no backpressure, both as on `main`
(oven-sh#43877).
- `tls.secureContext` (the undocumented door node:tls uses) is not read
by a Windows named pipe listener, which builds its context from the
options. On `upgradeTLS({ isServer: true })` the options next to it are
the policy, as with Node's `SetVerifyMode`.
- `selectServerName()` rebuilds the name tree per ClientHello for
injected sockets of a server with `addContext()` entries: 0.4 µs for 1
entry, 3.7 µs for 10, 41 µs for 100, against 631–1111 µs for a
handshake.

#### Not included

Left open, because they need a decision or are not TLS: oven-sh#43877 + oven-sh#44394
(see "Worth a look" 5; oven-sh#43874 stays open with them), oven-sh#38548, oven-sh#38591
(both shrink who is trusted), oven-sh#41589 (`verify-full` vs
`NODE_TLS_REJECT_UNAUTHORIZED=0`), oven-sh#37197, oven-sh#41706, oven-sh#43216, oven-sh#33487,
oven-sh#33545, oven-sh#36707, oven-sh#32435, oven-sh#37255, oven-sh#28691, oven-sh#40275, oven-sh#30314 (features),
oven-sh#38120 (needs the BoringSSL fork, as did oven-sh#33517, which the stale bot has
closed since), oven-sh#38529 (needs a Windows measurement), oven-sh#34342, oven-sh#38232,
oven-sh#43089, oven-sh#44454, oven-sh#40451, oven-sh#42710, oven-sh#44527, oven-sh#38088, oven-sh#38093, oven-sh#41898. oven-sh#37896,
oven-sh#42054 and oven-sh#37013 stay open for the halves not taken.
`http.createServer({ key, cert })` keeps serving TLS on purpose.

One open question: `tls: {}` (an object that names no TLS option) is
plain TCP on `Bun.listen` / `Bun.connect`, here and on `main`. It is the
same trap as `tls: []`, but changing it changes a Bun default, so it is
left alone.

### How did you verify your code works?

- Every new test fails on `main` for the stated reason and passes here,
except guards that pin existing behavior, each shown to fail when its
clause is removed. `node:*` tests also pass on Node v26.3.0; the few
that cannot say which Node version has the behavior.
- 212 test files that touch TLS, sockets, http, http2, fetch, WebSocket,
SQL, Valkey and workers: 6154 pass, 2 fail. Both are seen on `main` too:
`serve.test.ts` "root range port" (the box runs as root), and
`worker_threads.test.ts` "terminate(): nothing of the worker's runs
after the request", which is flaky there and passed in the run below.
- 58 of those files the way the ASAN lane runs them (LeakSanitizer +
`BUN_JSC_validateExceptionChecks`): 58 files, 48 of them with leak
checking, 4132 pass, 3 fail. All three also fail on `main`:
`serve.test.ts` "root range port", `node-net.test.ts` "should not leak
when connect({path}) fails synchronously on a reused handle" (times out
under this environment), `worker_threads.test.ts` "process.exit() with a
shell cp in flight" (a `ShellCpTask` leak).
- 647 vendored `test-tls-*`, `test-https-*`, `test-net-*`,
`test-http2-*`: the only two failures also fail on `main`.
- The SNI matcher was diffed against both old matchers: 3 seeds × 1.23 M
lookups × 3 registration flavours, every difference in one of the
intended classes, TCP and HTTP/3 identical on every lookup.
- The headline rows were also driven by hand with scripts against this
build, `main` and Node v26.3.0: cleartext https, `crl`, `tls: []` / `{
secureContext }`, the `ca` / `requestCert` matrix, the client
certificate on a wrong-name server, late `setSession()`, `destroy()` in
`ALPNCallback`, `[rsa, ec]` identities with an intermediate from `ca`,
`WebSocket` `checkServerIdentity`, a corrupted record, the Duplex read
above, `tls.DEFAULT_CIPHERS`.
- The `setSession()` guard was checked against the real `abort()` at 43
handshake states.
- `bun run rust:check-all`: 12 of 12 targets. `tsc`, oxlint, source
lints, prettier, rustfmt, mordant clean.
- usockets' `_Nonnull` is compiled out of debug builds, so 105 of those
files were also run on a local release ASAN build with the CI runner's
environment (92 with leak checking): 4595 pass, 1 fail,
`child_process.test.ts` "spawn reports EPERM after dropping privileges",
which cannot pass as root and fails on `main` too.
- The close of a TLS socket over another stream ("Worth a look" 5):
eleven seeded differential fuzzers, 8,424 scenarios compared, each run
on a release ASAN build of this branch, on `main`, on Node v26.3.0 and
on the earlier state of the branch. Against `main`:
- Data that `main` delivers in full is cut in 5 scenarios, and about 150
that `main` cuts arrive in full. Of the 5, in 2 `main` never notices the
peer's close and keeps the socket for good, 2 call `end()` on the middle
one of three sessions over an in-memory Duplex, and 1 does the same on
Node.
- No dead timeout, no silent reset and no uncaught error that `main`
does not have (4 uncaught errors fewer).
- A socket stays open where `main` closes it in 109, and closes where
`main` keeps it in 295. 92 of the 109 do the same on Node or on the
earlier state (a `destroy()` that an in-memory Duplex does not show its
peer, half-open peers). 14 wait for a peer that paused reading and so
does not read the FIN (oven-sh#42332's backpressure, as in Node); the socket's
own timeout fires there. 3 are left: one on a 5 ms timer, two with three
sessions over an in-memory Duplex.
- The earlier state of the branch cut data in 173 of the 400 scenarios
of one of them, where `main` cuts none and this cuts none.
- 23 new tests pin what they found. Each earlier attempt at this fix
fails the ones that describe it, the earlier state of the branch fails
7, and all pass on Node.
- After that change: 999 test files on the release ASAN build (20,246
pass; the 11 files that fail need a database, Docker, DNS or a non-root
user, or share a temp directory with a parallel run and pass alone), 61
on the debug build.
- TLS over a file descriptor (`openssl.c`, the path of `fetch`,
`Bun.serve`, `tls.connect`, `Bun.connect`) got the same treatment after
the rebase: seeded differential fuzzers on CI's release build of this
branch, on `main` and, for `node:*`, on Node v26.3.0. Every runtime also
against itself for the noise floor, injected faults and known bugs of
`main` as positive controls, and a difference counts only if it shows in
5 of 5 fresh processes.
- `node:tls` over TCP: 11,500 scenarios (one connection with Node as the
oracle line by line; 2 to 60 connections beside stalled neighbours; raw
peers that break the handshake). HTTPS: about 136,000 runs over
`Bun.serve` + `fetch`, `node:https`, `node:http2` and `wss://`, also
with the two ends in different runtimes. `Bun.connect` / `Bun.listen` /
`upgradeTLS`: 11,500 scenarios and 720 slow connections, with writers
driven by what `write()` returns, beside up to 6 stalled, dripping,
closing or resetting neighbours, and plain TCP as a second oracle. No
crash, hang, duplication, reordering or silent truncation, and no change
in time or in connection reuse.
- They found six things that `main` does better, none of which any test
showed. All are fixed, each with a test that fails on the build before:
what the peer sent lost behind a rejected `send()` (23 scenarios, and an
early HTTPS response lost with only `EPIPE`), the same silently for a
paused reader, `server.close()` never calling back on a half-open server
after a ClientHello and a reset (17), `closeAllConnections()` taking 12
s with a stalled client, `end()` losing up to 1.3 of 4 MiB that
`write()` had reported while the peer still uploads, and `end()` a
little after a stall never closing beside other stalled TLS sockets. The
last two fixes also deliver the 1 to 2 MiB that `main` loses there, and
close the socket that `main` keeps for good without such neighbours.
- All of them again after every fix, on CI's release build of it. That
caught one regression of a fix itself (a reader stopped for backpressure
lost 86,385 bytes, 1 of 6,000 scenarios), fixed too. On the last build:
scenarios that lose data where `main` does not 23 → 2, and Node loses it
in both, with the same `EPIPE`; `server.close()` that never calls back
17 → 0; connections held 4 → 0; requests that end in an error only where
`main` has a response 6 → 0. With a Node server in another process, a
request ends in an error only in 8 and 10 of 1,500 scenarios here, 5 and
3 on `main`, 10 with Node as the client.
- `Bun.connect` / `Bun.listen` on the last build against `main`, in
scenarios: hangs 0 against 1,031, sockets and fds never released 0
against 965, corrupted data 0 against 345, `abort()` 0 against 26
(`setSession()` after the handshake), writers that never close 0 against
101 of 720 connections. No kind of failure shows here and not on `main`.
About a third of the slow connections close later than on `main`, in 1
to 16 s instead of at once, waiting for unsent ciphertext or for the
peer's close_notify, and 79 more of them deliver all that `write()`
reported. RSS and time with 16 to 256 stalled writers are the same.
- What they found that `main` does worse: a `WebSocket` that calls
`close()` with sends pending loses messages in 81 of 999 scenarios (0
here), 37 server sockets left open, 10 `server.close()` that never call
back, 20 write callbacks that never run.
- The kqueue fix cannot be run on Linux. The `connectionListener` count
test now says what became of a missing connection, which is how the
cause was found (`'tlsClientError'` "socket hang up", then `read
ECONNRESET` at the client of the same port, after its
`'secureConnect'`). On macOS x64 it failed every attempt of the three
builds before the fix and passed at the first attempt of the build with
it.
- Windows and macOS were only run by CI. Four new tests asserted what
only the Linux kernel does (a FIN read ahead of a reset, unread bytes
surviving a reset, loopback buffer sizes, `fstat()` on a socket) and now
say so per platform.

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: robobun <117481402+robobun@users.noreply.github.com>
…sh#45008)

### Problem
- `new WebSocket()`, `new BroadcastChannel()` and `new Worker()` read
`new.target.prototype` after `X::create()`. When that read throws,
`create()` has already dialed the socket, subscribed the channel or
started the thread. The caller has no object to close.
- `setSubclassStructureIfNeeded`
(`src/jsc/bindings/JSDOMWrapperCache.h:181`) needs a finished wrapper,
so all 27 call sites run `create()` first.
- `new ReadableStreamDefaultReader(body)` has the same order: a failed
read leaves the body used.

### Fix
- `structureForNewTarget<X>()` does the read and returns the Structure
(JSC's hidden class, with the prototype). Each constructor calls it
after argument conversion and before `X::create()`.
- Correct: the read now precedes `create()`, as in Web IDL and Node
v26.3.0.
- Verified: `websocket-new-target.test.ts`,
`broadcast-channel-new-target.test.ts`, `web-globals.test.js` (15 tests
fail on main). More suites in Notes.
- Self-reviewed: 10 concerns raised, 8 addressed. Rejected: no size cost
(it shortens the paths), a read per `Bun.Cookie` arm (no spec order).

### Background
- `new.target` is the constructor that `new` or `Reflect.construct`
names. The object gets `new.target.prototype`. An accessor or a Proxy
can make that read throw.
- Considered a fix at the 4 WebSocket and BroadcastChannel sites: 23
more keep `create()` first. Considered a split of `create()`: the helper
stays.

### Downsides
- A failed read now wins over a constructor-step error (`Invalid URL`).
8 of 26 probe rows change. Node agrees on its 6.
- Release `.text` grows by 4,608 bytes (inlined wrapper creation).
- 4 event constructors run up to 13 more instructions (+0.44%), a
construct whose steps throw up to 29. 17 constructors run fewer.

<details><summary>Notes</summary>

#### Repro

```js
const server = Bun.serve({
  port: 0,
  fetch(req, server) { if (server.upgrade(req)) return; return new Response("no"); },
  websocket: { open() { console.log("the server saw a WebSocket open"); }, message() {} },
});
const newTarget = function () {}.bind(null);
Object.defineProperty(newTarget, "prototype", { get() { throw new Error("prototype getter"); } });
try { Reflect.construct(WebSocket, [`ws://localhost:${server.port}/`], newTarget); } catch (e) { console.log("WebSocket constructor threw:", e.message); }
try { Reflect.construct(BroadcastChannel, ["chan"], newTarget); } catch (e) { console.log("BroadcastChannel constructor threw:", e.message); }
server.stop(true);
```

Bun 1.4.3 and main print the two `constructor threw` lines, then `the
server saw a WebSocket open`, and the process never exits. With this PR
the script prints the two lines and exits.

#### What a failed read leaves behind

One script, 20 constructs that throw: 12 `WebSocket` (4 argument shapes,
3 kinds of newTarget), 6 `BroadcastChannel`, 1 `Worker` with a transfer
list, 1 `ReadableStreamDefaultReader` of `response.body`. A control
socket is dialed last. Release builds.

| | main 6c1eb06 | this PR | Node v26.3.0 |
| --- | --- | --- | --- |
| TCP connections that the server counts (1 is the control socket) | 13
| 1 | 1 |
| transfer list buffer after `new Worker()` | detached | 8 bytes | no
global `Worker` |
| `response.bodyUsed` after `new
ReadableStreamDefaultReader(response.body)` | true | false | false |
| the process exits by itself | no | yes | yes |

The three kinds of newTarget: an accessor `prototype` on a bound
function, a Proxy with a `get` trap that throws, and a revoked Proxy
(`getFunctionRealm` throws).

#### Where each constructor does the read now

- 25 sites: after argument conversion and before `X::create()`. This is
the Web IDL position: overload resolution, then "internally create a new
object implementing the interface" (the read), then the constructor
steps. In `new Worker()` the read comes before the env tree, the
transfer and the thread.
- `Bun.Cookie`: before its arguments. It has three `create` arms and no
specification. The classes from `generate-classes.ts` read first too.
- `PerformanceMark`: after `create()`. Node validates `startTime` and
clones `detail` before `super()`, and `create()` has no side effect. A
test pins this.
- `ReadableStreamDefaultReader` has its own helper
(`streams/StreamConstructor.h`). Its read moves above
`materializeIfNeeded()`.

The constructor now makes the wrapper with the Structure. It no longer
makes the wrapper with the base Structure and calls `setStructure`
afterwards.

#### The 8 probe rows that change

The probe has one row per constructor and overload (26 rows). Each row
constructs with a newTarget whose `prototype` getter throws, and with
arguments that convert but that the constructor steps reject.

| Constructor | main | this PR | Node v26.3.0 |
| --- | --- | --- | --- |
| `URL("x")` | TypeError (steps) | error of the getter | error of the
getter |
| `URLPattern("((", base)` | TypeError (steps) | error of the getter |
error of the getter |
| `URLPattern({ pathname: "((" })` | TypeError (steps) | error of the
getter | error of the getter |
| `Headers({ "bad name": "1" })` | TypeError (steps) | error of the
getter | error of the getter |
| `Bun.Cookie("a", "b", { sameSite: "bogus" })` | TypeError (steps) |
error of the getter | not in Node |
| `WebSocket("http://[bad")` | SyntaxError (steps) | error of the getter
| error of the getter |
| `WebSocket("http://[bad", "p")` | SyntaxError (steps) | error of the
getter | error of the getter |
| `WebSocket("http://[bad", {})` | SyntaxError (steps) | error of the
getter | not run |

Rows that do not change: every row reads `prototype` one time, before
and after. An argument that does not convert still wins over the getter
(Node reads the prototype first for most classes, that difference is not
part of this PR). `PerformanceMark` with a negative `startTime` still
reports the `startTime` error, as in Node. `Bun.Cookie` with no
arguments now reports the error of the getter.

#### Measurements

linux-x64, release builds (`bun run build:release`) of main 6c1eb06
and of this change on top of it, same toolchain. Tools: `llvm-size`,
`llvm-nm`, `llvm-objdump`, `gdb`. Not installed: `perf`, `valgrind`,
`strace`, `bloaty`.

| What | main | this PR |
| --- | --- | --- |
| `.text` bytes of `bun-profile` (`llvm-size -A`) | 65,818,853 |
65,823,461 |
| stripped `bun` file bytes | 89,318,984 | 89,323,080 |
| `setSubclassStructure` / `subclassStructure` bytes | 552 | 352 |

The growth is in the 23 `construct` functions: each now holds the
wrapper creation that `toJSNewlyCreated()` held out of line. That
function stays for the wrappers that are made outside a constructor.

Instructions of the main thread from the entry of the `construct` host
function to its return (`gdb` `stepi`, 8 constructs after 8 warm-up
constructs, the value that repeats). Calls into the vDSO clock are not
stepped.

| Construct | main | this PR |
| --- | --- | --- |
| `new URL(s)` | 876 | 875 |
| `new URLSearchParams(s)` | 2,052 | 2,032 |
| `new Headers()` | 323 | 306 |
| `new Headers(init)` | 1,770 | 1,756 |
| `new Headers(headers)` | 222 | 196 |
| `new FormData()` | 231 | 209 |
| `new TextEncoder()` | 97 | 92 |
| `new AbortController()` | 460 | 431 |
| `new EventTarget()` | 411 | 383 |
| `new Event(type)` | 496 | 413 |
| `new CustomEvent(type, init)` | 1,848 | 1,850 |
| `new MessageEvent(type, init)` | 3,498 | 3,504 |
| `new ErrorEvent(type, init)` | 2,956 | 2,969 |
| `new CloseEvent(type, init)` | 2,343 | 2,351 |
| `new DOMException(message, name)` | 702 | 677 |
| `new MessageChannel()` | 960 | 932 |
| `new PerformanceMark(name)` | 547 | 521 |
| `new PerformanceObserver(fn)` | 655 | 632 |
| `new Bun.Cookie(name, value)` | 544 | 534 |
| `new Bun.CookieMap(s)` | 813 | 789 |
| `new BroadcastChannel(name)` | 1,300 | 1,274 |
| `new URL("x")`, the steps throw | 3,399 | 3,417 |
| `new Headers({ "bad name": "1" })`, the steps throw | 3,888 | 3,917 |
| `class U extends URL`, `new U(s)` | 1,040 | 1,014 |
| `class H extends Headers`, `new H()` | 488 | 444 |
| `class T extends EventTarget`, `new T()` | 578 | 520 |
| `class E extends Event`, `new E(type)` | 661 | 552 |
| `class B extends BroadcastChannel`, `new B(name)` | 1,465 | 1,413 |
| `Reflect.construct(URL, [s], boundFunction)` | 1,347 | 1,321 |
| `controller.signal` getter | 18 | 18 |
| `channel.port1` getter | 22 | 22 |
| `toJS(FetchHeaders&)`, new wrapper | 71 | 52 |
| `performance.mark(name)` | 723 | 688 |

The four WebSocket rows have no value that repeats, because the dial and
the error path allocate. The 8 hits of one build differ by up to 3,500
instructions. The two builds differ by less than 250 at the median.

| Construct | main (min, median) | this PR (min, median) |
| --- | --- | --- |
| `new WebSocket(url)` | 15,657, 15,904 | 15,676, 15,900 |
| `new WebSocket(url, options)` | 21,345, 21,534 | 21,340, 21,774 |
| `class W extends WebSocket`, `new W(url)` | 15,870, 15,949 | 15,901,
15,995 |
| `new WebSocket("http://[bad")`, the steps throw | 3,913, 4,163 |
3,937, 4,136 |

Not measured: `new URLPattern()` (its steps outlast the step harness)
and `new Worker()` (a thread start).

Entries into the allocator per construct (breakpoints on the `mi_*`
entry points): the same on both builds for each row above (1 for `URL`,
10 for `URLSearchParams`, 7 for `BroadcastChannel`, 15 for
`WebSocket(url)`).

Wall-clock does not resolve this change in my container. In an
interleaved run of 20 rounds, operations that this change does not touch
(`new Map()`, `URL.canParse()`, `TextDecoder.decode()`) move by up to
4.8% between the two binaries. One binary against itself moves by up to
3.3% in that run, and by up to 17% in a run of 9 rounds. `new URL()`
moved by +1.1% in the run of 20 rounds.

#### Self-review

The 10 concerns, and what I did:

1. The large test files do not pass reliably under a debug build in my
container (`websocket.test.js` needs internet,
`websocket-client.test.ts` has a 1,000 ms limit per test,
`broadcast-channel.test.ts` has a worker test near its 5 s limit). The
WebSocket test and the BroadcastChannel test have their own files.
2. The Worker test called `terminate()` during startup. It now waits for
`close`.
3. The first version called `getDOMStructure()` out of line on the fast
path. `structureForNewTarget()` reads the slot inline.
4. `PerformanceMark`: Node runs the steps first. Kept that order, with a
test.
5. A comment in `JSDOMURL.cpp` was separated from its statement. Fixed.
6. Exception checks: 196 construct paths run clean under
`BUN_JSC_validateExceptionChecks=1`.
7. Values held across the read (the `JSValue` locals and init
dictionaries): each is on the stack or in a `Strong`, checked per site.
8. The WebSocket test relies on accept order. On a fixed build the count
is exact. Only a build with the bug could miss a socket.
9. Rejected: avoid the 4,608 bytes. The inlined wrapper creation is what
shortens 17 constructors.
10. Rejected: one read per `Bun.Cookie` arm, to keep its arguments
first. Three copies of the read for a class with no specified order.

#### Suites run with a debug build on linux-x64

All pass: `websocket-new-target.test.ts`, `websocket-client.test.ts`,
`websocket-close-connecting.test.ts`,
`websocket-custom-headers.test.ts`,
`websocket-subprotocol-strict.test.ts`,
`websocket-handshake-event.test.ts`, `websocket-unix.test.ts`,
`websocket-proxy.test.ts`, `error-event.test.ts`,
`test/js/first_party/ws/ws.test.ts`, the 4 files of
`test/js/web/broadcastchannel/`, `web-globals.test.js`, `url.test.ts`,
`url-wpt-constructor.test.ts`, `urlpattern.test.ts`, `FormData.test.ts`,
`URLSearchParams.test.ts`, `headers.test.ts`, `headers.undici.test.ts`,
`headers-case.test.ts`, `fetch_headers.test.js`, `abort.test.ts`,
`text-encoder.test.js`, `performance.test.js`,
`performance-entries.test.ts`, `message-channel.test.ts`,
`message-event.test.ts`, `performance-observer-leak.test.ts`,
`request-subclass.test.ts`, `cookie-map.test.ts`,
`event-emitter.test.ts`, `perf_hooks.test.ts`, `worker_threads.test.ts`,
`worker-transfer-list.test.ts`.

Files with failures that do not come from this change:

- `websocket.test.js`, `worker.test.ts`, `streams.test.js`,
`structured-clone.test.ts`: the same tests fail on main in my container
(3 need internet, the others end at their time limit under a debug
build). The set changes from run to run.
- `vm.test.ts`: 309 pass in one run. In two later runs, 1 and 2 tests
(`SourceTextModules with the same identifier ...`) ended at 5.1 to 5.5 s
against a limit of 5 s. They take 3.6 to 4.8 s in the run that passes.

Not run: Windows, macOS.

#### Other open PRs on the same lines

- oven-sh#44942 reorders `new Worker()` so that the transfer list is consumed
after every option validates. It calls `setSubclassStructureIfNeeded` in
`JSWorker.cpp`. After this PR that line becomes
`structureForNewTarget<Worker>()` before `Worker::create()`.
- oven-sh#44122 and oven-sh#44123 change the arguments of `WebSocket::create()` in
`JSWebSocket.cpp`. They keep the tail of the three constructors, which
this PR changes.
- oven-sh#32898 touches the `DOMException` constructor.

</details>
### What does this PR do?

Adds two commands:

- **`bun lint`**: a linter that reads ESLint and oxlint configs. 1,021
rules built in, type-aware rules included.
- **`bun format`**: a formatter that reads Prettier and oxfmt configs
and prints what they print.

Both are native, use every core, and need no packages and no config
file. Both can be configured in `bunfig.toml`.

## Compatibility

### The tools' own test suites

| Tool | Suite | Pass | Fail | Not run |
| --- | --- | ---: | ---: | ---: |
| ESLint 10.12, typescript-eslint 8.71, eslint-plugin-react 7.37.5,
-import 2.32 | their own rule tests, 505 rules | 69,640 | **0** | 303 |
| the same rules | more cases, judged by the real ESLint, 632 rules |
76,552 | 4 | 258 |
| oxlint 1.87 | its own rule tests, 504 rules | 27,702 | **0** | 63 |
| Prettier 3.9.9 | `tests/format` | 9,728 | 5 | 88 |
| oxfmt 0.72 | fixtures | 1,337 | 161 | 0 |

A lint case passes when message, location, fix and suggestions are what
the real tool reports. A format case passes when the bytes are the same.
"Total" includes the cases that are not run (15, 6, 96, 316, 101, 88,
0); failing: 0, 0, 0, 0, 0, 5, 161. Of eslint-plugin-import's 316, the
plugin itself throws under ESLint 10 in 135, 90 need babel-eslint and 71
the webpack resolver. The suites are checked in under
`test/cli/lint/conformance` and `test/cli/format/conformance` and run in
CI.

<details>
<summary>Prettier by language</summary>

| Language | Pass | Total |
| --- | ---: | ---: |
| JavaScript | 1,357 | 1,357 |
| JSX | 186 | 186 |
| TypeScript | 756 | 756 |
| Flow | 1,500 | 1,505 |
| JSON | 785 | 785 |
| CSS | 162 | 162 |
| Less | 238 | 238 |
| SCSS | 136 | 136 |
| GraphQL | 104 | 104 |
| YAML | 1,623 | 1,623 |
| Markdown | 1,572 | 1,572 |
| MDX | 32 | 32 |
| HTML | 272 | 272 |
| Vue | 189 | 189 |
| Angular | 109 | 109 |
| Handlebars | 128 | 128 |
| MJML, LWC, `misc` | 579 | 579 |
| Not run (stage-2 proposals that only Babel parses, Prettier plugins) |
0 | 88 |

</details>

### Real repositories

330 open-source repositories at a pinned commit, 135 of them with their
dependencies installed, in a sandbox. Each project's own lint and format
commands are run by the project's own tool and by `bun`. Counted by run.
Every result is of the release build of 6208ecd9 or newer; none is
carried over from an older build. Of its 975 results 12 moved against
the pass on 8c1bd4bc, about 400 performance steps earlier: 8 because
ESLint's 74 deprecated formatting rules have left `bun lint` (5 ESLint
runs that were the same now lack the reports of `quotes`, `max-len`,
`key-spacing` .., and nothing else), 2 because the `jsdoc` plugin has, 1
towards oxlint (napi-rs: which files are ignored), and 1 is a project's
own oxfmt that crashes now and then. The commits since then change what
becomes of oxlint's `jsdoc` and `vue` plugins, which are not built in,
and how `import/*` resolves beside an oxlint config. So 644 results ran
again on 7df1455c (one oxlint run, ComfyUI_frontend's, on ade5c82a),
among them all 35 oxlint runs that name one of the two plugins: 27 only
list it: a note, and oxlint's exit code; 8 turn rules of theirs on by
name: exit code 2, as for any rule that is not built in. oxlint 1.87
reports nothing of these rules in any of the 35.

| Compared with | What has to match | Same | Of | Rate |
| --- | --- | ---: | ---: | ---: |
| ESLint | exit code | 96 | 102 | 94 % |
| ESLint | `--fix`, bytes | 93 | 102 | 91 % |
| ESLint | every message, against the project's own ESLint and plugins |
73 | 102 | 72 % |
| ESLint | every message, against that or the releases that the built-in
rules follow | 80 | 102 | 78 % |
| oxlint 1.87 + tsgolint | exit code | 203 | 215 | 94 % |
| oxlint 1.87 + tsgolint | `--fix`, bytes | 208 | 215 | 97 % |
| oxlint 1.87 + tsgolint | every message | 191 | 215 | 89 % |
| oxfmt 0.72 | bytes | 227 | 240 | 95 % |
| Prettier | bytes, against theirs or 3.9.9 | 57 | 74 | 77 % |
| Prettier | exit code of `--check` | 66 | 74 | 89 % |

"Every message" compares lists, not sets: rule, severity, both ends,
text, fix objects, suggestions, suppressed messages, the files that were
linted, and the exit code.

| Beyond sameness | | Of |
| --- | ---: | ---: |
| Runs of `bun` that ended by themselves with exit code 0, 1 or 2: none
killed, hung or crashed | 951 | 951 |
| A second run in the same copy says the same | 631 | 631 |
| The project's lint command at a terminal (a pty, no `-f`): the exit
code of `-f json`, no crash | 150 | 150 |
| Projects whose config `bun lint` cannot load | 0 | |

**The 6 ESLint exit codes that differ.** Open: ComfyUI_frontend (12
`import-x/no-unresolved`: the plugin's resolver works there only with
the `NODE_PATH` that pnpm's shim exports), discord.js
`packages/discord.js` (4 `unicorn/consistent-function-scoping`). A
version that the project has and `bun` does not follow: TanStack
router-core (ESLint 9.22's `no-var`), eslint/eslint (eslint-plugin-n's
table), shadcn-ui/ui (react-hooks 7.0.1), sinon (ESLint 10.3's
`new-cap`).

Of the 88 runs that are not the same, 27 are open, 30 are a version's
drift and 31 are known differences. Some that are the same in every
message, fix and exit code: nodejs/node's whole lint command (1,383
messages in 7,924 files), eslint-plugin-unicorn (628), react-hook-form
(603), rollup (290), react-router (246).

<details>
<summary>Every run that is not the same, with its class</summary>

What "the same" means:

- **lint**: as said above; for oxlint also the length of the span.
- **`--fix`** and **format**: the bytes of every file afterwards, and
which files were read.
- **judges**: the project's own installed version first. `bun lint` and
`bun format` follow one release of each tool (ESLint 10.12.0,
typescript-eslint 8.71.1, eslint-plugin-react-hooks 7.1.1, oxlint 1.87.0
with oxlint-tsgolint 7.0.2003, oxfmt 0.72.0, Prettier 3.9.9), so each
run is also judged by that release with the project's configuration; for
oxlint and oxfmt that release is the judge.
- A tool of the project that cannot run here (it needs a build, or a
package that is not published) takes its run out of the count.

Each column "judged on" names the commit of this branch whose release
build gave the verdict. Nothing is older than 6208ecd93. The runs at a
terminal are of 6208ecd93.

| By run | the same | of | | judged on 6208ecd93 | judged on 7df1455c4 |
judged on ade5c82ae |
|---|---|---|---|---|---|---|
| ESLint: lint, identical to the project's own ESLint and plugins | 73 |
102 | 72 % | 97 | 5 | 0 |
| ESLint: lint, identical to that, or to the releases that bun follows
(ESLint 10.12; typescript-eslint 8.71.1, react-hooks 7.1.1) | 80 | 102 |
78 % | 97 | 5 | 0 |
| ESLint: --fix, the same bytes | 93 | 102 | 91 % | 97 | 5 | 0 |
| ESLint: the same exit code | 96 | 102 | 94 % | 97 | 5 | 0 |
| oxlint: lint, identical to 1.87 (rule, severity, place, length, text,
files) | 191 | 215 | 89 % | 3 | 211 | 1 |
| oxlint: --fix, the same bytes as 1.87 | 208 | 215 | 97 % | 3 | 211 | 1
|
| oxlint: the same exit code as 1.87 | 203 | 215 | 94 % | 3 | 211 | 1 |
| oxfmt: the same bytes as 0.72 | 227 | 240 | 95 % | 41 | 199 | 0 |
| oxfmt: the same exit code of --check | 229 | 240 | 95 % | 41 | 199 | 0
|
| Prettier: identical to theirs or to 3.9.9 | 57 | 74 | 77 % | 74 | 0 |
0 |
| Prettier: the same exit code of --check | 66 | 74 | 89 % | 74 | 0 | 0
|

Not the same: 88 runs: open 27, drift of a version 30, known differences
31. A run is listed once, under the first row of the table above that it
fails.

#### Open (27): a difference that is bun's to close

| Run | Judge | Exit theirs / bun | What |
|---|---|---|---|
| Comfy-Org/ComfyUI_frontend | ESLint 10.10.0 | 0 / 1 |
import-x/no-unresolved 12 only bun: the plugin's resolver works only
with the NODE_PATH that pnpm's shim exports; their exit code flaps too
(better-tailwindcss) |
| NarHakobyan/awesome-nest-boilerplate | ESLint 9.38.0 | 1 / 1 | spec
files: `vi.fn` / `jest.fn` is "a type that cannot be resolved" for bun:
no-unsafe-* 16 only bun; sonarjs' typed rules 11 only theirs (a plugin
in JavaScript gets no TypeScript program); deprecated formatting rules
(`quote-props` 61 ..) 65 only theirs |
| discordjs/discord.js:packages/discord.js | ESLint 9.39.5 | 0 / 1 |
unicorn/consistent-function-scoping 4 only bun (errors); no-eval's text
is ESLint 9's |
| graphql/graphql-js | ESLint 9.39.3 | 0 / 0 | 1 suppressed message:
import/unambiguous is placed on the whole file by bun (1:1), from the
first token by them (4:1) |
| mermaid-js/mermaid | ESLint 9.35.0 | 1 / 1 | typed rules in a .js file
(svgDraw.js): no-floating-promises 9, restrict-plus-operands 1 only bun,
with the plugin version bun follows |
| pixijs/pixijs | ESLint 9.39.2 | 0 / 0 | 18 fix objects of
jsdoc/require-param are missing in -f json; --fix writes the same bytes.
Deprecated formatting rules (`max-len` 147 ..) 149 only theirs; all else
the same (380) |
| withastro/astro | ESLint 10.12.0 | 1 / 1 | eslint-plugin-regexp: 3
suppressed reports only theirs, so 3 "unused directive" warnings only
bun; 1 file "not found by the project service" only theirs |
| Pawn-Appetit/pawn-appetit | oxlint 1.87 | 0 / 0 | React Compiler rule:
preserve-manual-memoization 1 only bun |
| arcjet/arcjet-js | oxlint 1.87 | 1 / 1 | "Unused oxlint-disable
directive" 58 only theirs |
| digirati-co-uk/iiif-manifest-editor | oxlint 1.87 | 1 / 1 |
react-hooks(exhaustive-deps) 2: another text |
| glide-browser/glide | oxlint 1.87 | 1 / 1 | "A 'declare' modifier
cannot be used in an already ambient context": 2 diagnostics against
their 16, and without the code TS(1038) |
| karakeep-app/karakeep:apps/web | oxlint 1.87 | 0 / 0 | React Compiler
rule: static-components 1 only theirs |
| monkeytypegame/monkeytype:frontend | oxlint 1.87 | 1 / 1 |
typescript(TS2307) 1 only theirs |
| openclaw/openclaw | oxlint 1.87 | 1 / 1 |
eslint(no-useless-assignment) 5 only bun |
| petyosi/react-virtuoso:packages/masonry | oxlint 1.87 | 1 / 1 | the
checker's own diagnostics (TS7031, TS7006, TS2307) 69 only theirs |
| petyosi/react-virtuoso:apps/virtuoso.dev | oxlint 1.87 | 1 / 1 | the
checker's own diagnostics (TS2307, TS7031, TS7006) 21 only theirs |
| reserve-protocol/register | oxlint 1.87 | 0 / 0 | React Compiler rule:
preserve-manual-memoization 1 only theirs |
| ietf-tools/purple | oxfmt 0.72 | 0 / 1 | 4 files only bun would
change: 1 .json, 2 .html, 1 .yaml |
| olton/metroui | oxfmt 0.72 | 2 / 2 | 1 .css, 1 .md differ; 4 .less
that 0.72 refuses; both exit 2 |
| zendeskgarden/react-components | oxfmt 0.72 | 1 / 1 | 1 .ts file only
bun would change |
| chakra-ui/chakra-ui | Prettier 3.8.3 | 1 / 1 | 8 .md only bun would
change (code blocks: their import-sorting plugin; Markdown) |
| renovatebot/renovate | Prettier 3.9.9 | 0 / 1 | 65 .md files that only
bun would change, against the same release 3.9.9 |
| tailwindlabs/tailwindcss | Prettier 3.9.6 | 0 / 1 | embedded languages
in templates (html``, css``): 32 .ts files only bun would change |
| timlrx/tailwind-nextjs-starter-blog | Prettier 3.8.1 | 1 / 1 | 1 .mdx
only theirs, 1 .cjs only bun; prettier-plugin-tailwindcss: 1 .css, 1
.tsx |
| vercel/next.js | Prettier 3.6.2 | 2 / 2 | the same bytes in 26,334
files; one file with a Babel proposal
(examples/with-custom-babel-config/pages/index.js) that their 3.6.2
formats and bun refuses; 3.9.9 exits 2 too, on another file |
| vuejs/eslint-plugin-vue | Prettier 3.9.10 | 1 / 1 | 1 .md differs |
| withastro/astro | Prettier 3.9.9 | 0 / 1 | 1 .md only bun would change
|

#### Drift of a version (30): the project has an older release than the
one bun follows, and that release explains the difference

| Run | Judge | Exit theirs / bun | What |
|---|---|---|---|
| TanStack/router:packages/router-core | ESLint 9.22.0 | 0 / 1 | two
drifts at once: ESLint 9.22's no-var reports `declare global { var }`,
10.12's does not (their disable comment is unused for bun); against
10.12 their older typescript-eslint is left
(no-unnecessary-type-assertion 2) |
| chartjs/Chart.js #1 | ESLint 8.37.0 | 0 / 0 | ESLint 8.37: places of
complexity, no-unused-vars |
| chartjs/Chart.js #2 | ESLint 8.37.0 | 0 / 0 | ESLint 8.37 /
typescript-eslint 5 |
| eslint/eslint | ESLint 10.12.0 | 0 / 1 | eslint-plugin-n's table of
Node.js versions: bun follows the newest release of the plugin |
| mastodon/mastodon | ESLint 9.39.5 | 0 / 0 | ESLint 9 looks the
configuration up from the cwd, 10 from each file: streaming/ (9 files)
is linted by 10 and by bun: 1 suppressed message |
| pinojs/pino | ESLint 9.39.5 | 0 / 0 | ESLint 9: the text of no-eval |
| sadmann7/skateshop | ESLint 8.57.0 | 1 / 1 | .eslintrc with
typescript-eslint 6 |
| webpack/webpack | ESLint 9.39.5 | 1 / 1 | ESLint 9: the text of
no-eval (2 messages); everything else of 5,521 files the same |
| TanStack/router:packages/react-router --fix | ESLint 9.22.0 | | lint
is identical with the plugin versions bun follows; the fix is that
version's |
| apollographql/apollo-client #1 --fix | ESLint 9.31.0 | | lint is
identical with the plugin versions bun follows; the fix is that
version's |
| sveltejs/svelte --fix | ESLint 10.0.0 | | ESLint 10.0:
no-useless-assignment; lint is identical to 10.12 |
| shadcn-ui/ui:apps/v4 exit code | ESLint 9.26.0 | 0 / 1 |
eslint-plugin-react-hooks 7.0.1 against 7.1.1: set-state-in-effect 38,
refs 16; identical with 7.1.1 in the place of theirs |
| sinonjs/sinon exit code | ESLint 10.3.0 | 0 / 1 | ESLint 10.3: new-cap
1; identical to 10.12 |
| diced/zipline, sanity-io/next-sanity | oxlint 1.78.0 | 0 / 2 | 1.87
refuses their configuration ("Rule 'react-compiler' not found in plugin
'react'"); bun lints and says that this rule did not run: exit 2 unless
--allow-unsupported is given |
| ixartz/Next-js-Boilerplate | oxlint 1.78.0 | 1 / 2 | as zipline, and
20 `jsdoc` rules |
| makeplane/plane:apps/web, :apps/admin, :apps/space, :apps/live,
:packages/editor, :packages/utils | oxlint 1.51.0 | 0 / 0 | 1.87 refuses
their configuration ("Rule 'prevent-abbreviations' not found in plugin
'unicorn'", 'prop-types' in 'react'); against their 1.51: other names of
plugins, other rules in the categories; the same exit code |
| olton/metroui | oxlint 1.42.0 | 0 / 0 | 1.87 refuses their
configuration ("Rule 'no-unused-vars' not found in plugin 'import'");
the same exit code as their 1.42 |
| preactjs/preact | oxlint 1.8.0 | 0 / 0 | 1.87 refuses their
configuration (four rules it does not have: camelcase, jsx-no-bind ..);
the same exit code as their 1.8 |
| vercel/ai | oxlint 1.56.0 | 0 / 1 | 1.87 refuses their configuration
("Rule 'no-unresolved' not found in plugin 'import'"); against their
1.56 the categories hold other rules |
| apollographql/apollo-client | Prettier 3.1.1 | 2 / 1 | Prettier 3.1.1:
12 files only bun would change; 3.9.9 fails with their plugin (exit 2);
6 .mdx only theirs; bun reads 438 of their 724 files (.mdx; files behind
their overrides): that part is open |
| dubinc/dub | Prettier 3.2.5 | 1 / 1 | Prettier 3.2.5 with plugins of
that age: 3.9.9 prints broken code with them, so neither judge is clean;
against 3.9.9: 4 differ, 10 only theirs |
| nextauthjs/next-auth | Prettier 3.3.3 | 2 / 1 | Prettier 3.3.3: 20
files only bun (and 3.9) would change; 3.9.9 fails with their plugin on
one file (exit 2); no byte differs from 3.9.9 elsewhere |
| shadcn-ui/taxonomy, steven-tey/precedent | Prettier 2.8.8 | 1 / 1 |
Prettier 2.8.8 with plugins of that age |

#### Known differences (31): the other tool fails here, or bun does
something else on purpose

| Run | Judge | Exit theirs / bun | What |
|---|---|---|---|
| angular/angular-cli | ESLint 10.12.0 | 0 / 0 | as jest: `max-len` 82,
`padding-line-between-statements` 1; all else the same (413) |
| jestjs/jest | ESLint 10.10.0 | 0 / 0 | ESLint's deprecated formatting
rules do not run in bun: `quotes` 2 only theirs; all else the same (653)
|
| jquery/jquery | ESLint 9.39.4 | 0 / 0 | as jest: `key-spacing` 9; all
else the same (19) |
| microsoft/playwright | ESLint 9.39.4 | 0 / 0 | as jest: `quotes` 276,
`key-spacing` 158, `comma-spacing` 117, `object-curly-spacing` 112; all
else the same (436) |
| nextauthjs/next-auth | ESLint 9.9.1 | 1 / 1 | 3 files "not found by
the project service" are fatal for them, linted by bun; in 2 .svelte
files svelte-eslint-parser throws in both: the TypeError's text is the
engine's |
| nuxt/nuxt | ESLint 10.12.0 | 1 / 1 | 37 files "not found by the
project service" are fatal for them, linted by bun; no-deprecated 3 / 1
|
| vueuse/vueuse | ESLint 10.9.1 | 0 / 0 | as jest: `spaced-comment` 2;
all else the same (68) |
| Sandakan/Nora | oxlint 1.87 | 0 / 0 | import(no-named-as-default) 1
only bun. The `import` plugin is named only inside an override; there
1.87 does not look at other modules, so its rules that ask another
module are silent; bun builds the module graph whenever such a rule is
on. The report is true of the code |
| s00d/nuxt-i18n-micro | oxlint 1.87 | 0 / 1 | not installed here:
tsconfig.json extends a file that `nuxt prepare` writes; 1.87 then
resolves no import, bun goes on without it: import(no-duplicates) 1 only
bun |
| Comfy-Org/ComfyUI_frontend exit code | oxlint 1.87 | 1 / 2 | as vitest
|
| eikowagenknecht/lootscraper exit code, jumpserver/luna #1,
jumpserver/luna #2, lihbr/lihbr-apex, nuxt-modules/prismic,
vitest-dev/vitest | oxlint 1.87 | 0 / 2 | oxlint's `jsdoc` and `vue`
plugins are not built in: rules of theirs that are on by name did not
run, and bun says so: exit 2 unless --allow-unsupported is given; every
message the same |
| chhoumann/PodNotes | oxfmt 0.72 | 2 / 0 | not installed here: 0.72
needs svelte/compiler and exits 2; bun leaves the 26 .svelte files as
they are, and says so |
| digirati-co-uk/iiif-manifest-editor | oxfmt 0.72 | 2 / 1 | 0.72 FAILS
on one file ("Invalid document: Expected end tag of kind LineSuffix"),
exit 2; bun formats it; all other bytes the same |
| generalaction/emdash:apps/emdash-desktop, mengxi-ream/read-frog |
oxfmt 0.72 | 2 / 1 | not installed here: sortTailwindcss: 0.72 exits 2
("Can't resolve 'tw-animate-css'"), bun warns and goes on; the bytes are
the same |
| heyhuynhgiabuu/proxypal | oxfmt 0.72 | 2 / 1 | not installed here:
0.72 cannot load their Tailwind 3 configuration and exits 2; bun warns;
the bytes are the same |
| ontola/atomic-server:browser/data-browser | oxfmt 0.72 | 2 / 1 | as
iiif-manifest-editor (2 files) |
| radix-ui/primitives | oxfmt 0.72 | 2 / 1 | 0.72's CSS parser refuses
one .module.css (exit 2); bun formats it; all other bytes the same |
| reshaped-ui/reshaped | oxfmt 0.72 | 2 / 1 | 0.72's CSS parser refuses
`@each` in 7 .module.css files (exit 2); bun formats them |
| salsita/node-pg-migrate | oxfmt 0.72 | 2 / 1 | 0.72 refuses `export`
in a .cjs file (exit 2); bun formats it |
| storybookjs/storybook | oxfmt 0.72 | 2 / 0 | 0.72 refuses `import` in
a .cjs file (exit 2); bun formats it |
| PrismJS/prism | Prettier 3.5.3 | 1 / 1 | three Prettier plugins that
lay JavaScript out otherwise (brace-style, space-before-function-paren,
merge): bun runs no Prettier plugin: 244 .js; 12 .md |
| alii/website | Prettier 3.9.6 | 1 / 0 | prettier-plugin-tailwindcss:
bun runs no Prettier plugin: 2 files only theirs would change |
| mickasmt/next-saas-stripe-starter | Prettier 3.9.6 | 1 / 1 | as
alii/website: 1 .tsx differs |
| shadcn-ui/ui:apps/v4 | Prettier 3.6.2 | 2 / 1 | their Prettier exits 2
here (prettier-plugin-tailwindcss needs a build); 19 files only bun
would change |
| trpc/trpc | Prettier 3.9.6 | 2 / 0 | their Prettier exits 2 here
("Unable to load your Tailwind CSS v3 config": www is not built); no
byte differs |

#### By repository

| | target | 9 October, 19:38 UTC (d9fee8c85) | now |
|---|---|---|---|
| oxlint + oxfmt in one repository: the same exit codes in both | 85 % |
132 of 143 (92 %) | 128 of 143 (90 %) |
| oxfmt: repositories with the same bytes as 0.72 | 95 % | 203 of 217
(94 %) | 204 of 217 (94 %) |
| oxlint: repositories identical to 1.87: rule, severity, place, length,
text, files, exit code, --fix | 75 % | 139 of 161 (86 %) | 143 of 161
(89 %) |
| of those: every label (span, text) the same too | | 103 of 139 (74 %)
| 142 of 144 (99 %) |
| of those: the help the same too | | 89 of 139 (64 %) | 143 of 144 (99
%) |
| Prettier: repositories identical | 80 % | 56 of 71 (79 %) | 54 of 71
(76 %) |
| ESLint: runs with the same exit code | 85 % | 89 of 102 (87 %) | 96 of
102 (94 %) |
| ESLint: repositories identical (with --fix) | 60 % | 60 of 86 (70 %) |
61 of 86 (71 %) |
| ESLint: repositories identical to the project's own version or to
10.12.0 with its configuration and plugins | | 62 of 86 (72 %) | 63 of
86 (73 %) |

#### What these numbers cannot say

- These projects are clean: their own CI keeps them so. Most runs
compare silence with silence. All 33 ESLint runs with the TypeScript
import resolver together hold 85 messages of `import/*` (counted on
0c0224b00); all 24 runs with eslint-plugin-react hold 42 messages of
`react/*` (counted on 611d8cbad). The rules' own test suites and the
differential fuzzers speak for the rest.
- No wall time in this table: the runs stood side by side on a crowded
machine.
- The terminal's own format is compared by exit code, signal and "has it
printed a summary", not by text.

</details>

## Usage

### `bun lint`

```sh
bun lint                      # eslint:recommended + typescript-eslint/recommended if there is no config
bun lint --fix src test
bun lint --type-aware         # rules that need types, on bun check's checker: no tsc, no tsgolint
bun lint --type-aware --type-check   # type errors too, in the same pass
bun lint -f json -o report.json .
bun lint --rules              # what is built in
```

- **Config**: `eslint.config.{js,mjs,cjs,ts}`, `.eslintrc.*`,
`.oxlintrc.json`, `oxlint.config.ts`. It takes the flags and output
formats of `eslint` and of `oxlint`, and behaves like the tool whose
config it finds.
- **Rules built in** (62 need types):

  | Plugin | Rules | Built in under an ESLint config |
  | --- | ---: | ---: |
  | ESLint core | 218 | 218 |
  | unicorn | 138 | |
  | typescript-eslint | 137 | 136 |
  | react | 106 | 102 |
  | vitest | 73 | |
  | `bun:test/` (new) | 65 | 65 |
  | jest | 60 | |
  | import | 46 | 46 |
  | jsx-a11y | 36 | |
  | `bun/` (new) | 29 | 29 |
  | oxc | 27 | 1 |
  | React Compiler | 26 | 26 |
  | nextjs | 21 | |
  | promise | 16 | |
  | n | 15 | 4 |
  | react-perf | 4 | |
  | react-hooks | 3 | 3 |
  | prettier | 1 | 1 |
  | **All** | **1,021** | **631** |

- **`bun:test/*`**: the rules of eslint-plugin-jest and -vitest that
make sense for `bun test`, one implementation for all three, chosen by
what the file imports. They look at files that import from `"bun:test"`
and know Bun's own API (orders of arguments, modifiers, matchers).
Without a config six are on: `no-focused-tests`, `valid-expect`,
`valid-expect-in-promise`, `valid-describe-callback`, `no-export` as
errors, `no-disabled-tests` as a warning. A fix that can break a passing
test is a suggestion.
- **Left out on purpose**: the 75 rules about layout that ESLint and
eslint-plugin-react have deprecated in favour of a formatter (`indent`,
`quotes`, `semi` ..), and oxlint's `jsdoc` and `vue` plugins. See "Known
differences".

- **Which of them answer depends on the config.** With an oxlint config,
all of them. With an ESLint config, those of the last column, each exact
by the original's own test suite: ESLint core, typescript-eslint, all
102 rules of eslint-plugin-react that are not about layout, all 46 of
eslint-plugin-import, react-hooks with the React Compiler's rules,
`prettier/prettier`, 4 rules of eslint-plugin-n and `bun/`. The other
plugins are ports of oxlint's rules, which differ from the original
plugins, so there the project's own plugin runs. A prefix means the
package only if the plugin object comes from that package:
`eslint-plugin-import-lite` under `import`, or `@eslint-react` under
`react`, answer for themselves.
- **Opting out**: `--no-native-plugin-rules` hands every plugin back to
its package; `--native-plugin-rules=import,react` names those that stay
built in.
- **Globals from the project's types**, behind a flag: `--infer-globals`
takes the globals of a file from the tsconfig or jsconfig that includes
it (`lib`, `types`, every `declare global`). `--infer-globals=fast`
reads only the compiler options and loads the types of a program only
when `no-undef` is about to report a name. Both are off by default.
- **ESLint plugins run as they are**, in JavaScript, on several VMs at
once: `context`, `sourceCode`, tokens, scopes, code paths, selectors,
fixes, processors.

### `bun format`

```sh
bun format                    # writes, prints what changed
bun format --check
bun format --stdin-filepath a.ts < a.ts
```

- **Languages**: JS, JSX, TS, Flow, JSON, CSS, SCSS, Less, GraphQL,
YAML, Markdown, MDX, HTML, Vue, Angular, Handlebars, and what is
embedded in them. A language that only a plugin of Prettier reads
(Svelte, Astro, PHP ..) goes to the project's own Prettier, if it and
the plugin are installed. With `svelte` in an oxfmt config, `.svelte`
files are left as they are, exit 2.
- **Config**: `.prettierrc*`, `prettier.config.*`, `"prettier"` in
package.json, `overrides`, `.editorconfig`, `.prettierignore`;
`.oxfmtrc.json`, `oxfmt.config.ts`. Flags of `prettier` and of `oxfmt`.
- **Built in**: import sorting (`@trivago/prettier-plugin-sort-imports`,
`@ianvs/prettier-plugin-sort-imports`,
`prettier-plugin-organize-imports`, oxfmt's `sortImports`), oxfmt's
`sortTailwindcss`, `sortPackageJson`, JSDoc. Other plugins of Prettier
are not: files of a project that names one
(`prettier-plugin-tailwindcss` ..) are left as they are, exit 2, or
formatted without it under `--allow-unsupported`.
- **It checks its output before it writes**: the result is parsed again
and compared with the input's syntax tree, comments and literals. A file
whose meaning would change is left alone and reported.

### `bunfig.toml`

```toml
[lint]
typeAware = true
maxWarnings = 0
ignorePatterns = ["vendor/**"]
nativePluginRules = ["import"]   # true (default), false, or the plugins whose built-in rules may answer

[lint.rules]
eqeqeq = ["error", "smart"]
"no-console" = "warn"

[format]
semi = false
singleQuote = true
```

- A key is the flag of the same name in camelCase, and `[format]` takes
Prettier's option names. `[lint.rules]`, `[lint.globals]`,
`[lint.parserOptions]` and `[lint.categories]` are tables.
- **Order**: command line, then `bunfig.toml`, then the tools' config
files, then the defaults. `configPrecedence = "prefer-file"` makes
`[format]` a fallback.
- An unknown key, a wrong type or a bad value is an error with file,
line and column, "Did you mean", and exit code 2. Flags that say what
one run does (`--fix`, `--init`, `--stdin`) are refused as keys.

### If the project already uses the name

A `lint` or `format` script in package.json, `./lint.ts`,
`./lint/index.*` (what can be run, so not `lint.json`),
`node_modules/.bin/lint` or an executable `lint` beside package.json
keep what `bun lint` meant before this PR. The same goes for `format`.

## Benchmarks

`microsoft/vscode` `src/` (10,149 files, 143 MB), Linux x64, 8 pinned
cores.

### Against oxlint 1.87 and oxfmt 0.72

15 alternating rounds on a shared machine, a release build of the commit
at which the tree was closed; the rows of `format` are of 8c1bd4bc. Both
linters report the same to within 0.03 % (see "How did you verify").

**Wall time, best round**

| Workload | `bun` | oxlint / oxfmt | Speedup |
| --- | ---: | ---: | ---: |
| lint, `correctness` | 0.21 s | 0.43 s | 2.0x |
| lint, recommended | 0.25 s | 0.51 s | 2.0x |
| lint, all 709 rules that both have | 0.73 s | 1.82 s | 2.5x |
| format `--check` | 0.42 s | 0.79 s | 1.9x |

**CPU time (user), median**

| Workload | `bun` | oxlint / oxfmt | Speedup |
| --- | ---: | ---: | ---: |
| lint, `correctness` | 1.52 s | 2.65 s | 1.7x |
| lint, recommended | 1.70 s | 2.96 s | 1.7x |
| lint, all 709 rules that both have | 4.86 s | 13.59 s | 2.8x |
| format `--check` | 3.02 s | 5.79 s | 1.9x |

**Instructions (user), least of 15**

| Workload | `bun` | oxlint / oxfmt | Fewer |
| --- | ---: | ---: | ---: |
| lint, `correctness` | 7.7 G | 15.8 G | 2.1x |
| lint, recommended | 8.4 G | 17.7 G | 2.1x |
| lint, all 709 rules that both have | 19.4 G | 66.7 G | 3.4x |
| format `--check` | 19.7 G | 39.9 G | 2.0x |

**Peak memory, median**

| Workload | `bun` | oxlint / oxfmt | Less memory |
| --- | ---: | ---: | ---: |
| lint, `correctness` | 119 MB | 271 MB | 2.3x |
| lint, recommended | 124 MB | 371 MB | 3.0x |
| lint, all 709 rules that both have | 517 MB | 1264 MB | 2.4x |
| format `--check` | 181 MB | 557 MB | 3.1x |

By instructions on six other repositories (next.js, node, twenty,
PostHog, ant-design, workers-sdk), with the default rules and with two
React configs: 1.6x to 2.9x fewer than oxlint. All 742 shared rules with
`import/*`, on vscode: 56 G against 303 G, 5.4x.

### Against ESLint and Prettier

Measured on earlier builds; being measured again.

**vscode's own ESLint config (8 plugins, 13,069 files), wall time.**
9,823 messages identical, fixes included; 3 differ.

| `bun lint` | ESLint 10 | Speedup |
| ---: | ---: | ---: |
| 15.8 s | 404 s | 26x |

**`prettier/prettier` as a lint rule, 8,042 real files, wall time.**
8,038 files get the same reports.

| `bun lint` (native rule) | ESLint + eslint-plugin-prettier + Prettier
| Speedup |
| ---: | ---: | ---: |
| 2.9 s | 67 s | 23x |

**Formatters, `--check` on vscode, CPU time**

| `bun format` | oxfmt 0.72 | Biome 2.5 | Prettier 3.9.9 |
| ---: | ---: | ---: | ---: |
| 2.9 s | 5.5 s | 20 s | 295 s |

With heavy third-party JavaScript plugins the gain over ESLint is
smaller: median 1.7x in wall time over 74 real runs of 5 s or more.

### Binary size

| Target | `main` | This PR | Added |
| --- | ---: | ---: | ---: |
| linux-x64 | 85.18 MB | 100.09 MB | +14.91 MB |
| linux-aarch64 | 84.68 MB | 99.80 MB | +15.13 MB |
| darwin-aarch64 | 67.85 MB | 82.50 MB | +14.66 MB |
| darwin-x64 | 74.81 MB | 90.49 MB | +15.68 MB |
| windows-x64 | 92.27 MB | 107.52 MB | +15.25 MB |

## Native eslint-plugin-react on real repositories

27 runs in 24 repositories that have the plugin in their resolved config
(versions 7.32.2 to 7.37.5), one build, three ways: built in, the
package in Bun's plugin host (`--native-plugin-rules` without `react`),
and the repository's own ESLint.

| Check, over the 24 runs whose config loaded on that build | Result |
| --- | --- |
| Built in against the package: messages, order, fix objects,
suggestions | the same in 24 |
| `--fix`, bytes | the same in 24 |
| `react/` messages, suppressed ones included | 42, the same in all
three |
| Runs that need no JavaScript engine any more | 2 |
| Runs that load fewer modules | 15 of the first 20 (react-hook-form:
750 to 24) |

These repositories are clean, so this shows silence where ESLint is
silent more than it shows the same reports.

| Repository | Built in | The package | Their ESLint |
| --- | ---: | ---: | ---: |
| shadcn-ui/ui | 2.1 s | 5.8 s | 90.4 s |
| chakra-ui | 0.59 s | 1.43 s | 22.6 s |
| mastodon | 6.6 s | 7.4 s | 100.0 s |
| playwright | 4.9 s | 4.5 s | 53.4 s |

## Known differences

**Configuration and command line**

1. An `eslint.config.*` and an `.oxlintrc.json` in one directory: `bun
lint` stops (exit 2) and asks for `--flavor=eslint` or
`--flavor=oxlint`.
2. A rule in `.oxlintrc.json` that `bun lint` does not have: oxlint
refuses the file. `bun lint` lints with everything else, names the rule
at the end and exits with 2; `--allow-unsupported` makes that a warning.
3. Beside an oxlint config, an option name that a rule does not know is
refused in oxlint's words; a wrong type or value that oxlint refuses is
accepted. Nothing that oxlint 1.87 accepts is refused (29,443 option
sets).
4. Under an ESLint config, files in other languages or for other parsers
(JSON, Markdown, CSS, Vue, Svelte, Astro, MDX) are linted by the
`eslint` package of the project, in JavaScript. Without it they are
named at the end and the exit code is 2.
5. The built-in rules follow ESLint 10.12, typescript-eslint 8.71,
eslint-plugin-react-hooks 7.1, -import 2.32, -n 18.4 and -react 7.37,
whichever versions are installed. A `note:` says so where the installed
one is older by a minor version or more.
6. Two rules that report at one place can come in another order than in
ESLint, which has first what was listened on the outer node. Where both
fix the same range the first wins, so `--fix` can write other bytes. The
rules of eslint-plugin-react say on which node they listen: with its 30
fixable rules at once on the 11,367 texts of its own tests, `--fix`
writes the plugin's bytes in 11,360, and in those looked at of the other
7 the plugin's text does not parse; the order of messages still differs
in 67 texts with 102 rules on. The core rules do not say it yet: 255 of
745 ties in ESLint's own sources come in another order. The 26 rules of
the React Compiler report while ESLint creates them, before it visits a
node, so at one place they come before every other rule and among
themselves in the order of the config: the same here.
7. There is no cap on what a file, a rule or a function may have: no
count of reports, nodes, steps or levels. What stands before the stack
is a check of the real stack, so how deep code, a config or a comment
can nest depends on the thread's stack and not on a number. The parser
and the binder nest as deep as the stack of a thread of Bun's pool
allows (4 MB; 18 MB on Windows); the threads of `bun lint`, `bun format`
and `bun check` have eight times that, for the checker, the rules and
the printers that walk the same tree. Beyond the stack the answer is an
error ("The code is nested too deeply.", Prettier's "Invalid string
length", `react/todo` "Support functions of this size", ESLint's
"Maximum call stack size exceeded"), where oxlint 1.87 ends with a
segmentation fault and ESLint with a `RangeError`. Hostile input costs
what it costs in the tool that is stood in for: 33 shapes in 47 rules
are the square of their size, as in ESLint, and patterns beyond
minimatch's and brace-expansion's reach are as slow as there. What is
left are bounds of the machine: `stylish` stops padding its rows where
ESLint ends with `RangeError: Invalid string length`. `--threads` above
1,024 counts as 1,024. A file of more than 2^29 - 24 bytes, which
JavaScript could not hold as a string, is not read (`EFBIG`); of what is
no regular file (`/dev/null`, a pipe) at most 64 MB are.
8. A plugin or rule in JavaScript that leaves a rejection unhandled:
ESLint stops with exit 2, `bun lint` prints the error and goes on.
9. `-f junit`, `checkstyle`, `gitlab` and `sarif` print oxlint's layout
under an ESLint config too, under the name `bun lint`. ESLint 10 ships
none of them.
10. `bun format`: `printWidth` and `tabWidth` have no upper bound and
take what Prettier takes, from every source (flags, JSON, YAML, TOML,
JSON5, programs, overrides, `.editorconfig`): of 536 cases none prints
other bytes. What is no integer is refused in Prettier's words, with its
exit code. Left: `--print-width -1` as two arguments is an unknown flag
here, a warning in Prettier; `0x10`, `1_000` and `05` in a
`.prettierrc.json` are read, where `JSON.parse` refuses the file. Beside
an oxfmt config nothing that oxfmt 0.72 takes is refused or printed
otherwise (222 cases); `tabWidth: 0` is formatted, where oxfmt panics.
The flags `--check`, `--list-different` and `--write` are refused
together where the tool that is stood in for refuses them, with its
sentence and exit code and nothing written (893 cases; `[error]` stands
where oxfmt has `Error:`).
11. What ESLint 10.12 gets wrong, `bun lint` gets wrong with it. One
case is known: `--fix` with `no-var` and `one-var` together turns `if
(x) { var a = 1; var b = 2; } return b;` into `let a = 1, b = 2;`, where
`b` is then not defined. ESLint has fixed it (eslint/eslint#21389), in
no release yet. The fix objects of all 106 fixable core rules are those
of 10.12.0 (10,330 cases), and the recorded suites compare them, so this
follows with the release that has it.
12. The same for oxlint 1.87. Of 4,624 self-checking programs
(JavaScriptCore's tests), run before and after `--fix` with one rule at
a time, 479 end otherwise after a fix of oxlint's own, and `bun lint`
writes the same bytes: `no-var` 259, `unicorn/catch-error-name` 105,
`sort-keys` 19, `no-new-wrappers` 15, `unicorn/prefer-spread` 12,
`prefer-arrow-callback` 11, 19 more rules 1 to 7 each. These tests look
at what ordinary code does not (the text of a function, the order of
keys), so this is an upper bound.
13. Beside an oxlint config, `no-var` leaves a `var` that is declared
twice, each time in a `try` block of its own; oxlint writes `const` for
both.
14. A fix that would leave a file with a syntax error is not applied,
and a warning says so. ESLint, oxlint and the plugins apply it: of the
1,908 texts of eslint-plugin-import's tests, 50 end as text that does
not parse with the plugin and are left alone, or stopped a pass earlier,
here.
16. YAML is read by Bun's own parser. Prettier's suite (7,598 checks)
and 1,219 files in four sets of options print the same bytes. Of 96,458
damaged texts, 250 are refused that Prettier formats, 150 formatted that
it refuses, 9 print other bytes, and a syntax error is worded as
`Bun.YAML.parse` words it.
18. `<!--` and `-->` comments: several comment rules at once `--fix`
other bytes than ESLint 10.12, whose `-- >` is no comment.
19. The config file that is found says whose run it is, wherever it lies
and whatever flags there are. A flag of the other tool has its plain
meaning (`-D <rule>` is `--rule`) or is refused in one line with exit 2.
20. The deprecated rules about layout are not built in. A config that
turns them on gets one `note:` on stderr that names them; the exit code
is that of the rules that ran. `bun format` is what does their work.
21. oxlint's `jsdoc` and `vue`: a rule of theirs that a config turns on
by name is named and ends the run with 2, like any rule that is not
built in. A plugin that is only listed, so that a category would turn
its rules on, is a `note:` on stderr, in every format and with
`--quiet`.
22. A search that leaves the project (`bun lint ..`, `'/**/x.js'`) runs
no `eslint.config.*` that ESLint 10.12 would not run, and ends as ESLint
does. The working directory's own config is always run. Where the
working directory has no config, one counts only in and below it, or
where a plain path names it: what a pattern merely reaches gets the
defaults, and nothing of it is loaded. The same for `bun format`.
23. Beside an oxlint config, `no-useless-assignment`,
`constructor-super` and the other rules on code paths follow ESLint's
code paths. oxlint has a control flow graph of its own, which answers
otherwise in a few corners (`a = (a = f()) ? a.b : 1`, `class A { x =
id++ }`, `try` with `finally`).
24. oxlint stops at an error of its semantic checks, such as a name
declared twice, and runs no rule on that file. `bun lint` goes on.
25. A NUL in the text of an HTML element is the end of the file for
Prettier 3.9.9 and oxfmt 0.72, which drop what follows and exit with 0.
`bun format` refuses the file and leaves it.

**eslint-plugin-react and eslint-plugin-import**

1. `settings.react.version: "detect"` asks the nearest installed react
for each file; the plugin lets the first file decide for the process.
2. Where the plugin throws and ESLint's run ends (settings of the wrong
kind, `jsx-handler-names` with `ignoreComponentNames` on `<A.B>`),
nothing is reported for that rule and the run goes on.
3. `jsx-sort-props` compares names in English order. The plugin uses the
machine's default locale, or the rule's `locale`, so with `ignoreCase`
under a locale with a collation of its own (cs, sk, hu, tr, sv, da, nb,
lt), or with such a `locale`, reports and the order that `--fix` writes
can differ.
4. A file with Flow types goes to the project's own parser, and there
the package's rules answer.
5. `import/resolver: typescript` is answered by Bun's resolver following
eslint-import-resolver-typescript 4.4.5. Its options `extensions`,
`conditionNames`, `mainFields` and `alwaysTryTypes: false` are not read;
`./a.js` where only `a.jsx` exists is found.
6. With any other resolver, another parser in `import/parsers`, or a
regular expression object in `import/ignore`, the installed package's
rules run instead.
8. `import/no-unused-modules` and `import/no-cycle` answer each file as
if it were the first one linted; the package's answers can depend on the
order of the files.

## TODO

- [ ] On Windows a top-level `preload` (and `define`, `loader` ..) of
`bunfig.toml` still applies to the process that evaluates
`eslint.config.*`, `prettier.config.*` and the like. Elsewhere that
process gets `--config=/dev/null`. It needs a way to tell `bun` to read
no bunfig.toml.

**Compatibility**

- [ ] Defaults, locations and texts of ESLint 9 where ESLint 9 is
installed (45 of the 102 real ESLint runs).
- [ ] oxfmt's own departures from Prettier (161 fixtures).
- [ ] 5 Flow cases of Prettier's suite; a comment before the first `.`
of a member chain that breaks.
- [ ] JavaScript plugins get no type information.
- [ ] Types are TypeScript 7's, also where typescript-eslint runs on
TypeScript 5 or 6.
- [ ] `--infer-globals=fast` as the default: on projects that never load
the types it costs +0.1 % to +8.1 % instructions. Prepared as a change
of its own.
- [ ] `import/resolver: typescript` on one resolver instead of
TypeScript's resolution with corrections.

**Speed and robustness**

- [ ] A catastrophic regular expression from a config or an `/* eslint
rule: [..] */` comment costs about 1.8 s per match (Yarr's own
backtracking limit). Nothing limits a file or a run.
- [ ] `x = (x = (x = ( ..` 1,500 deep around 20,000 lines takes the
parser 36 s (lines x depth). ESLint refuses the file; oxlint 1.87 does
not end in 200 s.
- [ ] The type checker's file reader has no bound, as `bun check` on
`main` has none: a file of gigabytes, or a link to a device, as
`tsconfig.json` (with `--infer-globals`) or as the `package.json` of an
imported package (beside an oxlint config with `import` rules that look
at other modules) is read until there is no memory. The reader of `bun
lint` and `bun format` has the bound; it belongs in that one too, as a
change of its own.
- [ ] Memory and time of `bun format` on one very large YAML file have
not been measured again since YAML moved to Bun's parser (858 real
files: 3 % more instructions).
- [ ] Tens of thousands of different keys in one object of a config file
cost the square: 80,000 keys, about 1 MB, take 8 to 48 s (Prettier 67 s
on its own; `tsc` 7.0 14 s where `bun check` takes 25 s).
- [ ] `jsx-sort-props` with a collator for the machine's locale, or the
rule's `locale`.
- [ ] What JSON has no word for, as an option of a rule in
`eslint.config.*`: `Infinity` (as in `{ max: Infinity }`), `-Infinity`,
`NaN`, functions, symbols, bigints, `Map`, `Set`, `RegExp`, class
instances and a trailing `undefined` are taken or refused as by ESLint
10.12, in its words (103 of 128 cases). Left: other words for a `Date`,
a boxed number and a circular object; a key of a config object that is
`undefined` is read as absent, where ESLint refuses the config. Beside
an `oxlint.config.ts`, `Infinity` is no limit, where oxlint 1.87 refuses
the file.
- [ ] A binary file under an extension that is read leniently is
rewritten, as Prettier rewrites it. A look for NUL bytes would leave it
alone.
- [ ] A pass of `--fix` that would leave a syntax error drops all of the
fixes of that pass, not only those that break the file.
- [ ] `bun check`, on `main` (4c54c9bc6) as here: a call of a generic
function with 6,000 type parameters that are each constrained by the
next ends with a segmentation fault (`bun lint --type-aware`: from
52,000). `tsc` 6.0 says TS2589 from 110 on, a limit of its own that this
path never reaches, so from there the answers differ. The same for
function types some thousand deep in each other, and for `export *`
through 6,678 modules.
- [ ] `bun check`: generic arrow functions 5,200 deep in each other:
four functions of the checker go up through the enclosing arrows and
never ask for the stack. It holds on the 32 MB of the threads and fell
on 4 MB.
- [ ] `bun check -b` on a chain of projects, each of which refers to the
next, that are all in error (`composite` with `noEmit`): every project
repeats the errors of all below it, and 35 KB are held for each: 400
projects deep take 2.8 GB, on `main` 0.35 GB, for the same 79,800
errors. It comes with the depth: a tree of 364 projects and 10 layers of
10, in error or healthy, take 142 to 201 MB and 1.1 to 4.0 s on one
thread, less memory than `main` (212 to 311 MB; 4.2 to 16.1 s) and `tsc`
6.0 (257 to 413 MB; 1.4 to 4.2 s), and print the same bytes.
- [ ] `bun check`: an expression more than 1,000 queries deep is refused
at once ("ran out of stack") where `tsc` with a large stack completes.
Going on where the work stood is written; past the real stack it printed
TS2719 for a placeholder, so it is not in. `await`, `else if` and
ternaries at the parser's limit cost the square (15 to 70 G).
- [ ] A pattern that leaves the project still loads the configs it
reaches where the working directory has a config of oxlint or of ESLint
8 (8.57.1 does the same), and `oxfmt.config.ts` where it has a
`.prettierrc` (Prettier would not). Closed for a flat config and for no
config.
- [ ] `--stdin-filename` and `--print-config` for a path outside the
project that has no `eslint.config.*`: built-in rules, where ESLint 10
says "Could not find config file".
- [ ] 37 `.resolves` / `.rejects` that nothing awaits in Bun's own
tests, found by `bun:test/valid-expect`.
- [ ] A regular expression in the options of a rule with 26,000 groups
in each other takes 15 s in Yarr's compiler.
- [ ] Beside an oxlint config the check for "this fix would leave a
syntax error" does not see a name that is declared twice (a file that is
linted without types is not bound). It would hold back most of the 259
fixes of `no-var` above, and be other bytes than oxlint's on purpose.
- [ ] The node that a rule listens on, for the core rules too, and for
the rest of eslint-plugin-react (`jsx-tag-spacing`, `jsx-key`, an
element that closes itself).

<details>
<summary><b>For reviewers: what changes outside <code>src/lint</code>,
<code>src/format</code> and <code>src/sema/parser</code></b></summary>

- **`src/js_parser`: 27,798 lines deleted.** `bun check`, `bun lint` and
`bun format` parse with `src/sema/parser`, so the mode that #44361 added
to Bun's parser for the type checker is gone. What is left is the text
from before that PR plus 141 lines, all of which are on `main`.
`Bun.Transpiler` on 751,511 files with every loader that applies
(1,949,442 rows: a hash of the output or of each thrown message with its
place, of `scan`, of `scanImports`): 0 rows differ from `main`.
- **`Bun.markdown` changes its output for 0.45 % of real documents**
(232 of 51,234 `.md` files with the GFM extensions on, 86 with them
off). The Markdown formatter is built on `src/md`; each change is a fix
with a test in `test/js/bun/md`, judged by commonmark.js 0.31 and
micromark 4: table alignment per table, `a~~b~~c`, email autolinks,
lower-case `<!doctype`, nested links, fences outside a container, `1. `
not interrupting a paragraph, `- [ ]` over `- [x]`. 70 MB of real
Markdown to HTML takes 30 % fewer instructions.
- **Moved so that lint and format can share them**: `src/node_path`
(Node's path algorithms, out of `src/runtime/node/path.rs`),
`src/text_diff` (`bun test`'s diff), `src/glob` (one matcher for
`Bun.Glob`, minimatch, picomatch and ignore files), and text helpers in
`bun_core`. `test/internal/source-lints/shared-helpers.test.ts` keeps an
inventory of private copies that can only shrink.
- **`src/yarr`** (new), `RegularExpression.cpp`: regular expressions in
configs and rules run on JavaScriptCore's Yarr. `bun test -t` goes
through the same code.
- **`src/react_compiler`**: the React Compiler's lint rules run on Bun's
port of it, which got checks of the real stack (no count and no budget),
speed fixes, and a cure for type inference that did not end on small
inputs. Every assertion that input could reach returns an error now. On
4,801 real files its lint rules take 22 % fewer instructions than before
the speed fixes (131.3 G to 102.1 G); 8,000 JSX children 0.66 s instead
of 7.3 s. 608 of 20,000 generated programs took all the memory there is
(oxlint 1.87 does not end in 60 s on the three that were tried): the
join of two ref types went down shared structure as if it were a tree.
Two types that are held by pointers are joined once now: 25.8 s for all
608 in one process, 103 MB at the peak; where the old code ended (114 of
202 tried) the output is the same. Beside oxlint 1.87 on the 104,658
real files, all 26 rules: 23,256 findings there, 15 only here, 8 only
there; `react/purity` is 1,548 in both and in 1.86, and 698 in 1.85.
- **`src/react_compiler`, `src/ast/e.rs`,
`src/js_parser/react_compiler_host.rs`: eight fixes for wrong output**,
found by executing compiled against uncompiled code over several
renders, by parsing every output of 104,658 real files, and by looking
at where the lint rules differ from oxlint's. All eight are on `main`
too (4c54c9bc6). Each has an `itBundled` test that executes; those of
the first six fail on `main`, and for 7 and 8 the same program, built
plain and compiled by `main`, prints two different things:
1. Dead code elimination removed an unused read in a `try`, whose throw
was caught.
2. A variable reassigned in a scope inside another scope was `undefined`
from the second render on.
3. A scope that depends on a variable and reassigns it stored the new
value as the dependency. `let sum = props.start; for (..) sum = sum +
x;` with `start` 0, 0, 7, 0 gives 7, 7, 7, 7 on `main` and 7, 7, 14, 7
here.
4. `a++` in a scope did not count as a reassignment, so the update was
lost in a render that reuses the scope.
5. `memo(function Tree() { .. <Tree/> .. })` was compiled with an
unbound name, a `ReferenceError`. Upstream's invariant is back: the
function is left alone, as in babel-plugin-react-compiler 1.0.0.
6. A regular expression that ends in an escaped slash (`/^https?:\/\//`)
lost its closing slash in a compiled function: a syntax error in the
bundle.
7. A function of the module's own that is called `useCallback` or
`useMemo` was taken for React's and the call was dropped: `function
useMemo() { return "mine"; }` was never called.
8. The same for a member of any namespace import: `import * as X from
"./hooks.js"` lost its `X.useMemo(..)` and `X.useCallback(..)`, and with
`--target=bun` `X.useEffect(..)` was `undefined` and `const [s] =
X.useState("init")` was `"init"`. Named imports from other packages were
right. `React.useMemo` of `"react"` is dropped as before.

  | Fix | Real files whose output changes, of 104,658 | What changes |
  | --- | ---: | --- |
  | 1 | 5 | something in a `try` is kept; 1 had the bug |
  | 2 | 4 | 1 had the bug, 3 loops are memoized that were not |
| 3 | 28 | one kind only; 15 of 30 (scope, variable) pairs could go
stale |
  | 4 | 0 | no real file has the shape |
| 5 | 4 | the function is left alone; 3 crashed, 1 was right and loses
its memoization |
  | 6 | 83 | exactly the 83 files whose output did not parse |
| 7 | 1 | a component that was left alone is compiled, with the call
kept |
  | 8 | 0 | no real file has the shape in a compiled component |

Fixes 2, 3 and 4 are one change and must not be separated. Memoized
functions stay at 122,303. After the first six: outputs that do not
parse 0; upstream's 849 fixtures executed, release and ASAN, 849 pass.
After all eight and the speed fixes: that one file is the only one of
the 104,658 whose bytes move, and no recorded fixture output does.

**Found and not fixed**, on `main` and in babel-plugin-react-compiler
1.0.0 alike: what depends on whether a `try` throws is not reactive
(`try { JSON.parse(props.text) } catch { valid = false }` keeps the
first render's answer); `b = f((e = 0))` loses the assignment in a
render that reuses the scope. A fix for the first was in 6a982bb2 and
991d1b9b and is out again: it made the parameter of a `catch` a
dependency outside the handler, a `ReferenceError` in 3 of 5,541 real
files with a `try`. The test for that stays. New:
`react-compiler-fixtures-run.test.ts` builds 849 of upstream's fixtures
with and without the compiler, executes both and compares every render
(1 s on a release build).
- **`src/lint`: a rule is a type, not a table of callbacks.** `impl
Rule` with `const META`, `const ON` (the node kinds it listens to) and
one method per kind; no `dyn`, and the enabled rules of a file are one
bitset. Against the callback version, 237 rules on vscode: 4.2 % fewer
instructions, 1,420,096 report lines byte for byte the same.
- **`src/sema`**: binder modes without the flow graph, services for lint
rules on the checker, a reduced program that answers only which globals
exist. `bun check` shares declaration files between projects with
`main`'s code, unchanged. It runs on a pool of its own ("Bun Check n")
with eight times the stack, as the two new commands do. Three walks of
the checker that never asked for the stack, `modules_declaration`,
`check_binding_name` and `implied_by_pattern_inner`, ask now: they are
on `main` as well, where a parser and a binder with larger frames gave
up before them. In `tsconfig.json`, `extends` is followed by a loop, and
`include` and `exclude` are matched without recursion or backtracking:
`**/a/` 16 times against 34 directories took over 60 s (`tsc` 6.0: 29 s)
and takes 0.4 s. The same answers as before on 1,500 random projects
(everything that is printed, and the exit code) and on the 7,908 real
configs of 372 repositories (1,112,029 files listed).
- **`src/parsers/yaml.rs`: three changes of `Bun.YAML.parse`**, beside
`parse_as_written` for the formatter (+125 lines, no branch of it taken
by `Bun.YAML.parse`). `a: [⏎]`, the end of a flow collection at its
parent's indentation, was an error and is `{a: []}`: `yaml` 2.9 and
js-yaml 4.1 read it, the specification does not, and Bun read the same
bracket behind a scalar. `a: [x]⏎: y` was an error and is read, as the
specification has it (js-yaml refuses). `{:a b}` was `{null: "a b"}` and
is `{":a b": null}`, as both and the specification. A test for each;
`test/js/bun/yaml` 2,814 pass.
- **`src/bunfig`**: `load_config_for_tool` hands the parsed file to the
command: the same search, reader and TOML parser as for every other
section. **`src/sys`**: `extended_attributes(fd)`, so that a rewritten
file keeps its xattrs, ACL entries and SELinux label.
- **`src/jsc`, `src/runtime/cli`**: the two commands, the plugin VMs
(`lint_js.rs`), `LintNodes.cpp` (the syntax tree that a JavaScript rule
sees is built natively), `VirtualMachine::exit_and_free`. How many VMs a
run starts depends on the bytes that need one and on the memory limit,
not on time: 192 of 192 runs at four sizes, two loads, with and without
a 16 GB limit, start the same number. Eight real repositories against
the build of the day before: 1.9x to 4.0x faster in seven, within the
noise in one, with less CPU time in all.
- **`src/jsc/bindings/highway_dispatch.h`**: a call site of the SIMD
dispatch macro was a guarded function-local static; it is now one
relaxed atomic load and an indirect call, and resolving the target is a
cold function of its own, so LTO no longer inlines the guard into
thousands of callers. Every Bun process goes through it, and it is
independent of the rest of this PR.
- **A script named `lint` or `format`** now sees
`BUN_INTERNAL_SCRIPTS_OF_COMMANDS` in its environment: that is how
`"lint": "bun lint"` means the linter inside the script.
- `LICENSE.md`: attributions for the ported projects.

</details>

### How did you verify your code works?

**CI.** The whole run on the head, a52c2279, with `main` merged in,
passed: Linux 120 of 120, ASAN 20 of 20, Windows 16 of 16, macOS 4 of 4;
so did the one before the merge. An earlier one, on the commit at which
the tree was closed: Linux 120 of 120, ASAN 20 of 20 (with Rust's debug
assertions), macOS 4 of 4, Windows 15 of 16. The red test was ours and
was right: 15,000 assignments in each other got five TS2719 for a
placeholder under `bun check`, from a step that went on where the
checker used to refuse; on Linux it shows from 18,000. The step is out
(see TODO). Not required, and red in all: `binary-size`, by design, and
the lane of macOS 27, which `main` does not run
(`sql-close-pending-connection`, `node-net`); nothing here touches what
they test, but that they are unrelated is not shown.

**What left the PR.** On 11 October 104,000 lines: two development
harnesses with the oracles that only they could run, fixtures that
nothing loaded, the ports of two Prettier plugins, tables stored twice,
a second algorithm for hostile sizes in 47 rules, YAML's own parser,
twin rules. On 12 October 24,500 more: the 75 rules about layout,
oxlint's `jsdoc` and `vue`. Each step that could change behaviour was
proved old against new, byte for byte, on the suites and thousands of
files.

- The seven test suites above and 55 differential tests against the real
tools run in CI (`test/cli/lint`, `test/cli/format`, `test/cli/check`).
- The real repositories in the tables above, each with its own command
and config in a sandbox, against the real tool.
- Every rule that both have, against oxlint 1.87 itself, place for
place: 709 rules on seven real repositories (vscode, next.js, PostHog,
twenty, node, workers-sdk, ant-design). 11,169,328 reports are the same
and 3,130 are not (99.97 %); 514 of the 641 rules that report anything
agree everywhere. On the 157,000 inputs of the three tools' own suites
that oxlint can read, with all 709 rules on each: 1,488,153 the same,
2,148 not. `import/*` on vscode with its packages installed: 211,422 of
211,422.
- The work on speed changes no report. Each fast path has a debug
assertion against the slow one: 76 runs of a debug build over all those
inputs and repositories, none fired, and CI's ASAN lane has them on.
`bun build --react-compiler` on 16,981 real components, before against
after: 0 outputs differ. The compiler's 20 rules alone and in groups, 90
pairs of a config and a set of files: 0 differ. What was changed for
oxlint's flavour moved nothing under an ESLint config: the whole JSON
output on 162,946 inputs has the same checksum.
- Coverage-guided fuzzing of the parsers, printers, config readers and
the glob matcher. Black-box fuzzing of CI's release and ASAN builds with
mutated configs, options and hostile files, in a clean environment and
on a pseudo-terminal: 232,294 runs on 0c0224b0, no signal, no hang, no
exit code over 2. Files that are not valid UTF-8 with a plugin that
checks every node, token and comment: 5.2 million reports compared with
ESLint 10.12 on 09859818, none differs. Extreme numbers in every flag
and every key of `bunfig.toml` (6,160 runs), in the formatter's options
(1,092) and elsewhere in config files (1,260); config files that are
deep, long or many, to a million levels, keys or overrides (4,001); file
trees with links, devices, pipes, odd names and modes (312), and config
files that name a device (280): on the last commits no signal, and
nothing written outside the project. What these found is fixed:
`threads` without a bound, and files, links and config paths that were
read without one.
- Does the code still do what it did? 4,624 self-checking programs are
run, rewritten and run again. `bun format --write`: 5 end otherwise, as
with Prettier 3.9.9. `bun lint --fix` with the 106 fixable core rules at
once: 38, all with ESLint 10.12.0's bytes. Beside an oxlint config, all
rules at once and the 68 that fix one at a time (18,547 files written):
none that oxlint 1.87.0 does not break too. For JSX the question is what
React would be given: the 30 fixable rules of eslint-plugin-react one at
a time (139,639 files written) and all at once: none that the plugin
does not change with the same bytes. This found three faults, which are
fixed: `no-implicit-coercion` and `no-var` beside an oxlint config, and
the order in number 6 of the known differences. The fix objects of all
106 fixable core rules are those of ESLint 10.12.0 (10,330 cases), those
of 42 rules of react, react-hooks and import those of the plugins
(2,132).
- Large input. 176 and 342 real files joined into one file (25,117 and
50,234 lines), the same inside one function, 300 and 600 generated
components, one generated component of 25,665 and of 51,614 lines: every
field of every diagnostic is the same as oxlint 1.87.0's (every
category, 7 plugins; up to 62,801 diagnostics a file) and as ESLint
10.12.0's (366 rules; up to 79,836), in a fifth to a three-hundredth of
the time. The React Compiler's rules on generated components with up to
4,000 hooks, callbacks or JSX children and 16,000 statements: oxlint's
findings in all 28 sizes at which it ends; at 6,000 and 8,000, where it
ends with a segmentation fault, the finding is reported.
- Depth. Where a number used to stand before the stack there is a check
of the real stack, and a check in the parser does not protect what walks
the tree afterwards. Such a crash lives in a window of depths a few
percent wide, just below where the parser refuses, and in a release
build only: with ASAN or in a debug build the check keeps four times as
much free. So: every size in steps of 1 to 3 %, one process a file, on
CI's release builds (127c0994 to 8f6f7ed9; the older of them still have
4 MB for the threads of a command and are the more sensitive).

  | What is nested | Shapes | Runs | Signals |
  | --- | ---: | ---: | ---: |
| Config files, comments, selectors, regular expressions, globs, ignore
files | 354 | 100,249 | 1, on `main` too, fixed: an `include` of 133,271
`**/` in `tsconfig.json` |
  | Source under `bun lint`, every rule | 756 | 86,522 | 0 |
| Source for the React Compiler, `bun lint` and `bun build` | 50 |
23,309 | 0 |
  | Source under `bun format` | 142 | 22,878 | 0 |

Found this way and fixed: an `/* eslint */` comment (54 signals in 500
runs on the commit before: what was left of a tree was freed at the
bottom of a nearly full stack); a suggestion of `no-unused-vars` for
patterns in each other, where oxlint 1.87 ends with a segmentation fault
too; two aborts in JSDoc; the guard of a `match` case of Flow in `bun
format` (6 of 143 sizes; 0 now, on a main thread with 4 MB too); dotted
namespaces and patterns in `bun check` (13 of 213 sizes; 0 now);
patterns in a parameter (every size tried from 5,000 to 17,500 on 4 MB;
`main` falls at 5,000); a chain of 2,287 `extends` in `tsconfig.json`,
which is read on the main thread (`main` says "Cannot read file" from
there on, `tsc` ends with a `RangeError` from 1,500; 19,061 files are
read in 1.1 s now).

And a tool for what a sweep cannot see because another function happens
to have the larger frame: a debugger tells the stack checks of one
directory that the stack ends early and sets a hardware watchpoint below
that end. What asks stops above it; what does not ask writes there, and
the backtrace names it. Each run has a control that must hit.
`src/lint`, 6,108 files with every rule in both flavours: 20 hits in 4
functions, 0 now. `src/format`, 12,272 files in ten languages: 3 kinds,
0 now. `src/react_compiler`, 295 files: 0.
- The 21 numbers that `bun format` had (depths, lengths, counts), and
the 16 and 8 bits of `printWidth` and `tabWidth`, are gone. 25 inputs
beyond them: 25 print the bytes of Prettier 3.9.9 or oxfmt 0.72. About
20,000 shapes were then run at three depths for anything whose time or
memory doubles: none. By the clock: lists in Markdown and expansions of
Angular 40,000 to 120,000 deep, and 32,000 parameters in SCSS, took 6 to
22 s (the square of the size) and take 0.17 s. For a wrong answer
without an error: 7,000 files formatted once with 200 KB of stack and
once as usual: what is not refused and differs is always another
language inside the file, left as it is written, as Prettier does when
its own stack ends there.
- What lies outside the new directories, against `main`: 11,586 files
through the transpiler (transform, minify, scan) plus 96,000 generated
and mutated programs, 20,000 random inputs through 13 `node:path`
functions, 90,000 through `Bun.semver`, TOML, JSONC, JSON5 and JSONL,
1,713 React Compiler fixture builds: 0 differences. `Bun.markdown`:
135,000 inputs against commonmark.js and micromark, 0 regressions.

To try it:

```sh
bun run build:release
BUN=$PWD/build/release/bun
cd some/project
$BUN lint
$BUN lint --type-aware
$BUN format --check
```
### What does this PR do?

`bun install` already migrates `package-lock.json`, `pnpm-lock.yaml` and
Yarn v1 `yarn.lock` when there is no `bun.lock`. A Yarn 2+ ("Berry")
`yarn.lock` (same filename, but YAML with a `__metadata:` header) was
rejected with `UnsupportedYarnLockfileVersion`, so the first `bun
install` in a Berry project resolved every dependency again. This PR
adds a Berry migration.

The rule the migration follows: every dependency in `bun.lock` is the
package, version and source that `yarn.lock` pinned, or the lockfile is
not migrated. When something cannot be carried over exactly, Bun prints
the reason and resolves from `package.json`, which is what it did before
this PR. `bun pm migrate` exits 1 in that case. A migration that is not
done writes nothing: no `bun.lock`, and `package.json` is left byte for
byte as it was.

#### What is migrated

| yarn.lock | bun.lock |
| --- | --- |
| root and workspace entries | built from their `package.json`
(workspaces from the root `workspaces` globs), so the dependency groups
are the real ones |
| `name@npm:1.2.3` | npm package. The tarball URL is `::__archiveUrl=`
when present, otherwise built on the registry Bun is configured with |
| `alias@npm:real@^1` | `npm:` aliases |
| ranges as Yarn 4 writes them (`npm:^1`) and as Yarn 2/3 write them
(`^1`, and unquoted `1` or `2.10`, which YAML reads as numbers) | the
range text, unchanged |
| tarball URLs, git URLs with `#commit=<sha>`, `github:o/r` | tarball /
git dependency pinned to the locked commit |
| `file:`, `portal:`, `link:` | folder dependency. Bun has no protocol
for a linked folder outside `workspaces`, so `portal:` and `link:`
ranges in `package.json` become `file:` and a notice says the folder is
now copied |
| `patch:<locator>#<file>` | the patch file goes to
`patchedDependencies`; the `patch:` range in `package.json` becomes the
range it patches. Yarn's builtin compatibility patches are dropped |
| `dependenciesMeta`, `peerDependencies`, `peerDependenciesMeta`,
`conditions`, `bin` | optional / peer behaviours, `os` / `cpu`, bins |
| `catalog:` ranges | resolved through `.yarnrc.yml`; the catalogs are
copied into `package.json` |
| `resolutions` | applied the way Yarn applies them: the first matching
entry wins, and `parent@version/name` names the version the parent is
locked to |
| `packageExtensions` in `.yarnrc.yml` | the `dependencies` they add are
added to the packages they match |
| `checksum` | not used. It is the hash of Yarn's own zip of the
package. Integrity comes from the registry manifest |

#### What stops a migration

- A dependency with no lockfile entry (`yarn.lock` is older than
`package.json`), or a workspace in `yarn.lock` that the `workspaces`
globs do not match.
- A registry that Yarn is configured with and Bun is not. Yarn's
registry is read from `.yarnrc.yml` in the project, in each parent
folder and in the home folder, and from `YARN_NPM_REGISTRY_SERVER`.
Without this check Bun would ask its own registry for a name that Yarn
fetched from another one.
- An npm package with no integrity from the registry manifest, for
example an `__archiveUrl` on another host.
- A patch that `patchedDependencies` cannot express: several patch files
for one package, a patch on a folder dependency, a patch file outside
the project.
- A protocol Bun does not have (`exec:`, plugin protocols), or a git
dependency on a package inside a repository (`#workspace=`).
- A root dependency that has the name of a workspace and that Yarn
installed from somewhere else. Bun links the workspace under that name.
- A name in both `dependencies` and `devDependencies` when the version
Yarn installed for the `devDependencies` range is not in the
`dependencies` range. Yarn installs the first, Bun reads the second.
- A `__metadata.version` outside 4 to 10, a key written twice, or a YAML
merge key (`<<`). Yarn and Bun's YAML parser read the last two
differently.
- A command that does not write `package.json` (`--dry-run`,
`--no-save`) when the migration has to edit it.

#### Other changes

- The edits to `package.json` (rewritten ranges, `patchedDependencies`,
catalogs) go through `package_json_write_back`, so they are written when
`bun.lock` is saved and not before. `bun pm migrate` and `bun install
--lockfile-only` now run that step.
- `bun remove` writes the cached `package.json`, not the copy printed
before the install, so an edit made while the lockfile loads is kept.
`bun patch --commit` fetches the cache entry again after the lockfile
loads. Both also apply to a pnpm migration.
- Credentials in a registry URL are not printed in the error messages
and are not written to the tarball URLs in `bun.lock`.
-
`Lockfile::fetch_necessary_package_metadata_after_yarn_or_pnpm_migration`
gets a second const parameter that fills integrity from the manifest.
Only the Berry migration turns it on.
- Docs: `docs/pm/cli/install.mdx` has a "Yarn migration" section that
lists what is migrated and what stops a migration.

#### Known limits

- The extensions built into Yarn (the `packageExtensions` Yarn ships for
well-known packages) are not applied. A fresh `bun install` does not
apply them either.
- A `github:` dependency is migrated as a git dependency pinned to the
full commit, not as a GitHub tarball, so the first install needs `git`.
- `libc` conditions, `nodeLinker` and `supportedArchitectures` are not
migrated.
- The pnpm migration writes registry credentials into tarball URLs in
the same way the Berry one did before this PR (`src/install/pnpm.rs`).
That is not changed here.

### How did you verify your code works?

- `test/cli/install/migration/yarn-berry.test.ts`: 58 tests against a
local Verdaccio registry. Every fixture pins versions that are not the
newest the registry has for the range, so a test passes only when the
pin came from `yarn.lock`. Each "not migrated" case checks the message,
exit code 1, that no `bun.lock` was written and that `package.json` is
unchanged. All 58 fail on Bun 1.4.2.
- The migration (at c114b88) was run with `bun pm migrate` on 30
public Yarn Berry projects (lockfile versions 4 to 10; babel, jest,
storybook, grafana, backstage, cal.com, AFFiNE, Rocket.Chat and others).
20 migrate. In all 20, `bun.lock` holds no npm version that `yarn.lock`
does not. 10 are not migrated, each with one of the reasons above:
nested workspaces (Yarn's own repository, 3 versions), a `condition:`
plugin protocol (babel 7.14, 7.20), a root dependency on the published
copy of a workspace (babel main), a lockfile older than `package.json`
(jest 27.0.0, storybook 6.5.0), a patch on top of a patch (MetaMask
extension), a folder in `dependencies` and an npm version in
`devDependencies` (grafana 9.0.0). None crashed.
- Those runs found four defects that are fixed here and have tests: the
workspace-name case, `packageExtensions`, a range that ends in a space,
and a name in both dependency groups.

---------

Co-authored-by: Samuel Attard <6634592+MarshallOfSound@users.noreply.github.com>
Co-authored-by: Alistair Smith <hi@alistair.sh>
…n intersection or a union, a large `paths` map, and two more differences from `tsc` (oven-sh#45001)

Errors that `tsc` 7.0.2 reports and `bun check` did not, about
`readonly`, `private` and `protected` properties of intersections and
unions. `bun check` with a large object in `tsconfig.json`, which took
quadratic time, once for every project of a build. And two more
differences from `tsc`, with a destructured parameter and with a loop.

Fixes oven-sh#44966, fixes oven-sh#44960.

## A property that several members of an intersection have (oven-sh#44966)

```ts
interface I {
  readonly id: string
}
type Writable<T> = { -readonly [K in keyof T]: T[K] }

declare const a: Writable<I> & I
declare const b: { id: string } & I

a.id = 'x'
b.id = 'x'
```

```
index.ts(9,3): error TS2540: Cannot assign to 'id' because it is a read-only property.
```

`bun check` reported nothing.

A property of a mapped type has no declaration of its own. So `id` of
`Writable<I> & I` is declared in one place, in `I`, and it has the
modifiers that are written there. In `b` it is declared in two places,
and it is read-only only if it is in both.

That decides more than assignments:

```ts
interface I {
  readonly id?: string
}
type Writable<T> = { -readonly [K in keyof T]: T[K] }
declare const a: Writable<I> & I

delete a.id
;[a.id] = ['x']
declare const copy: { [K in keyof typeof a]: (typeof a)[K] }
export const probe: never = copy
```

```
readers.ts(7,8): error TS2704: The operand of a 'delete' operator cannot be a read-only property.
readers.ts(8,5): error TS2540: Cannot assign to 'id' because it is a read-only property.
readers.ts(10,14): error TS2322: Type '{ readonly id?: string | undefined; }' is not assignable to type 'never'.
```

The first two were missing, and the third said `{ id?: string |
undefined; }`.

`private` and `protected` follow the same rule, in unions too:

```ts
declare class Account {
  protected balance: number
  private pin: string
  owner: string
}
declare const a: Record<'balance' | 'pin', unknown> & Account

a.balance
a.pin
```

```
protected.ts(8,3): error TS2445: Property 'balance' is protected and only accessible within class 'Account' and its subclasses.
protected.ts(9,3): error TS2341: Property 'pin' is private and only accessible within class 'Account'.
```

Both were missing.

## Three smaller ones that the same programs showed

What is left of an enum object after a destructuring has the number
index of the enum, which `keyof` and inference leave out:

```ts
enum E { id, other }
const { other, ...rest } = E

declare function entries<K extends PropertyKey, V>(x: { [P in K]: V }): [K, V]
export const keys: never = null! as keyof typeof rest
export const inferred: never = entries(rest)
```

```
enum.ts(5,14): error TS2322: Type '"id"' is not assignable to type 'never'.
enum.ts(6,14): error TS2322: Type '["id", E.id]' is not assignable to type 'never'.
```

`bun check` said `number | "id"` and `[number | "id", string | E.id]`.

A false error, where a variable of a namespace and an optional property
make one property:

```ts
declare namespace N {
  let other: 1
}
declare const Base: new () => { other?: 1 } & typeof N

export class Derived extends Base {
  own = 1
}
```

```
namespace.ts(6,14): error TS2415: Class 'Derived' incorrectly extends base class '{ other?: 1 | undefined; } & typeof N'.
```

And the rest of a union is one type for each member, also where they
look the same: `{ id: string; } | { id: string; }`, where `bun check`
printed `{ id: string; }`.

## A large object in `tsconfig.json` (oven-sh#44960)

Twice the keys took four times as long, whatever the object was for. In
practice it is `paths`, in a file that every project of a monorepo
extends, and `bun check -b` read that file again for each project.

Wall time of `bun check`, and of `tsc` 7.0.2, for one empty file and a
`tsconfig.json` with one object that nothing reads:

| Keys | 1.4.3 | this branch | `tsc` |
|---|---|---|---|
| 0 | 0.08 s | 0.07 s | 0.31 s |
| 10,000 | 0.31 s | 0.07 s | 0.16 s |
| 20,000 | 2.45 s | 0.15 s | 0.21 s |
| 40,000 | 10.86 s | 0.10 s | 0.35 s |

Wall time of `bun check -b`, and of `tsc -b`, for projects of one small
file each that extend one file with 5,000 `paths`:

| Projects | 1.4.3 | this branch | `tsc -b` |
|---|---|---|---|
| 200 | 14.17 s | 0.35 s | 0.15 s |
| 1,000 | 71.28 s | 1.63 s | 0.61 s |

Peak memory of the same:

| Projects | 1.4.3 | this branch | `tsc -b` |
|---|---|---|---|
| 200 | 0.99 GB | 0.44 GB | 0.12 GB |
| 1,000 | 3.08 GB | 0.62 GB | 0.16 GB |

The projects share the `paths` of the file that they extend: with none
at all, the 1,000 projects take 0.62 GB too. `tsc -b` is still ahead.
`bun check -b` checks one project after the other, and loads the default
library again for each. That is for another pull request.

An error in each entry was quadratic three times over: for each error
the file was parsed again, searched for the entry, and read again to
find the line. Wall time for `paths` that are not relative, which is
TS5090 for each, one entry on a line:

| Entries | this branch | `tsc` |
|---|---|---|
| 4,000 | 0.04 s | 0.13 s |
| 20,000 | 0.09 s | 5.31 s |
| 60,000 | 0.81 s | 56.70 s |

1.4.3 takes 35.5 s for the 4,000.

Entries of `files` that do not exist were quadratic as well, which is
TS6053 for each. Wall time, one entry on a line:

| Missing entries | 1.4.3 | this branch | `tsc` |
|---|---|---|---|
| 1,000 | 2.89 s | 0.04 s | 1.09 s |
| 4,000 | 19.17 s | 0.05 s | 6.29 s |
| 8,000 | 60.01 s | 0.13 s | 29.91 s |

Where configuration files extend each other in a circle, both tools
report TS18000. `tsc` 7 prints `{0}` where the files belong. `bun check`
lists them, as TypeScript 6 does:

```
error TS18000: Circularity detected while resolving configuration: /app/tsconfig.json -> /app/one.json -> /app/two.json -> /app/three.json -> /app/one.json
```

What `tsc -b` makes of those files for one project depends on which
project it loaded first, and it can hang. `bun check -b` gives each
project the options that it has alone, as before. The docs list both
among the differences from `tsc`.

## A destructured parameter of type `unknown`, and a loop that assigns
from a `for..of` variable

```ts
interface Sink {
  take(value: unknown): void
}

export const sink: Sink = {
  take({ first, second = first }) {
    return second
  },
}
```

```
parameter.ts(6,10): error TS2339: Property 'first' does not exist on type 'unknown'.
```

`bun check` reported TS7022 for `first`, a circle that is not there.
With the pattern over several lines the two errors are on different
lines, so a `@ts-expect-error` written for `tsc` left `bun check` with
two errors.

```ts
interface Item {
  children: Item[]
}
declare const start: Item

let current: Item | undefined = start
while (current) {
  const { children } = current
  for (const child of children) current = child
}
export {}
```

```
loop.ts(8,11): error TS7022: 'children' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.
loop.ts(9,14): error TS7022: 'child' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.
```

`bun check` reported nothing. With `current = children[0]` in the loop
it did report them.

## How it was verified

The whole output against `tsc` 7.0.2, on all threads and with
`--threads=1`.

| Programs | main | this branch |
|---|---|---|
| oven-sh#44966: 21 ways to declare `id`, 19 types that have `id` too, as an
intersection in both orders, with a third member, as a union and as a
union of intersections, with one statement for each thing that reads a
modifier, 399 files | differs in 262 files | the same as `tsc`, 56,380
lines |
| The three smaller ones and what the reviews found, with their
neighbours, also with `exactOptionalPropertyTypes`, 9 files | differs in
8 files | the same as `tsc`, 145 lines |
| oven-sh#44960: 19 builds in which what is kept of a configuration file for
one project could be wrong for the next, or in which an error is
reported at the first of several properties with one name, 37 runs | the
same as `tsc` | the same as `tsc`, 1,507 lines |
| A destructured parameter: 8 types that it has from its context, 11
patterns, 10 places for the function. A loop: 6 kinds, 17 ways in which
the value that it assigns comes from its variable, left with `break`,
`continue`, a label or not at all. 416 files | differs in 189 files |
the same as `tsc`, 4,030 lines |

For the last row, a build that was broken on purpose shows that the
builds can tell: with the errors of the extended file dropped the second
time, and the first value of a repeated key kept, 8 of the then 21 runs
differed from `tsc`. For the builds with a circle of `extends`, `tsc` is
`tsc -p` for each project, with the TS18000 line of TypeScript 6.0.2.

Three new entries in `test/cli/check/differential-cases.json` keep 49
files of the first matrix, all 9 of the second and 67 of the last in CI,
where both tools run on them again. Two entries with a circle of
`extends` are gone from that file, because `tsc` 7 is what it compares
with. Their programs are in `check.test.ts`, which has nine new tests:

| Test | 1.4.3 | this branch |
|---|---|---|
| The program of oven-sh#44966, the other order, a class, an unused
`@ts-expect-error`, `delete`, a mapped type, `private`, `protected`, a
union | fails | passes |
| 60,000 `paths` | not done in 60 s | passes |
| An error in each of 120,000 `paths` | not done in 60 s | passes |
| 5 builds whose projects share a configuration file, one of them with
the files of a circle | 4 pass | pass |
| TS18000 for a file that extends itself, for three files, and for a
name in another case | fails | passes |
…and `then` on `Promise<T> | PromiseLike<T>` (oven-sh#45066)

`bun check` 1.4.3 and `tsc` 7.0.2 disagree on 135 small programs about
circles, loops and types that are still being computed. 129 of them are
the same now. And `bun check` on tRPC no longer reports two errors that
are not there.

They were found by reading 96 functions of typescript-go next to their
ports: the ones that detect a circle, the ones that check an expression
with or without starting the analysis of the surrounding loops again,
and the ones that store a result before they are done. Each difference
in the two texts became a small program, and each program was run
through both tools.

## Errors that were missing

A loop that assigns its variable from something that depends on the
variable, through a property of a class:

```ts
interface Item {
  children: Item[]
}
declare const start: Item

let current: Item | undefined = start
while (current) {
  const { children } = current
  class Holder {
    held = children[0]
  }
  current = new Holder().held
}
export {}
```

```
loop.ts(8,11): error TS7022: 'children' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.
loop.ts(10,5): error TS7022: 'held' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.
```

`bun check` reported nothing. The same goes for a parameter with a
default, a property assigned to a function or to `this`, a parameter
property, and the return type of a method or a getter.

A setter whose annotation reads the property it declares:

```ts
class Celsius {
  get degrees(): number {
    return 1
  }
  set degrees(value: (Celsius | Kelvin)['degrees']) {}
}
class Kelvin {
  degrees = ''
}
export const reading = new Celsius().degrees
```

```
setter.ts(5,7): error TS2502: 'degrees' is referenced directly or indirectly in its own type annotation.
```

## Errors that were not there

A generator that yields inside a loop:

```ts
declare function first<T>(items: Iterable<T>): T

export function* numbers() {
  let next: string | number = 0
  while (Math.random() < 0.5) {
    yield next
    next = first(numbers()) === 0 ? 'done' : 1
  }
}
```

`tsc` reports nothing. `bun check` reported TS7023 for `numbers`.

A `return` in a function whose return type annotation is circular:

```ts
export function wrapped(): [ReturnType<typeof wrapped>] {
  return 1
}
```

```
returned.ts(1,28): error TS2577: Return type annotation circularly references itself.
```

`bun check` added `Type 'number' is not assignable to type '[any]'` on
the `return`.

## Types that were wider

```ts
declare function base<T>(value: T): new () => { value: T }

let seen: string | number = 0
while (Math.random() < 0.5) {
  class Box extends base(seen) {}
  seen = new Box().value === 0 ? 'zero' : 1
  const value: string = new Box().value
}
export {}
```

```
heritage.ts(7,9): error TS2322: Type 'number' is not assignable to type 'string'.
```

`bun check` said `string | number`. `tsc` checks the expression after
`extends` once, while it is still working out the loop, and keeps what
it finds. The same holds for the operand of `typeof` in a type and for
the operand of `yield`.

## `then` on `Promise<T> | PromiseLike<T>`

```ts
declare const p: Promise<number> | PromiseLike<number>
const r = p.then(v => {})
if ('catch' in r) {
  r.catch(reason => {})
}
export {}
```

`tsc` reports nothing: `r` is `Promise<void> | PromiseLike<void>`, and
`'catch' in r` leaves the `Promise`. `bun check` had `PromiseLike<void>`
for `r`, and reported TS18046 for `r.catch` and TS7006 for `reason`.
tRPC has this code in
`packages/server/src/vendor/unpromise/unpromise.ts`, so each of its
packages had those two errors.

## What else is in it

- The members of an enum that is declared in two parts, and of one that
refers to a later member.
- A type alias with type parameters that is referred to while it is
being resolved: TS2456 was reported where `tsc` reports nothing, and
TS2314 named the wrong number of type arguments.
- A getter of an object literal that returns itself, and a getter that
returns itself through another instantiation of its class.
- A property of a union or an intersection whose setters and getters
disagree.
- The parameters of a function that gets them from its context, when one
of them is read before all have their types.
- A member of a base class that was missing on an instantiation that is
resolved while the base types are.
- TS2490 for an iterator, which was reported twice.
- A `let` of another file's `declare global { }` is narrowed inside an
arrow function or a method once that file no longer assigns it.

## How it was verified

The whole output against `tsc` 7.0.2, on all threads and with
`--threads=1`.

| Programs | 1.4.3 | this branch |
|---|---|---|
| 649 programs, each from a difference between a function of
typescript-go and its port | differs in 135 | differs in 6 |
| A destructured parameter with a type from its context, and a loop that
assigns its variable from itself, 416 files | differs in 189 files | the
same as `tsc`, 4,030 lines |
| The 8 packages of tRPC | two errors too many in each | the same as
`tsc` |
| The 44 packages of the Sentry SDK | the same as `tsc` | the same as
`tsc` |

tRPC and Sentry were checked without their own `node_modules`, with the
packages another project has installed, so what both tools say about the
missing ones is part of the comparison.

TypeScript's own test suite and 47 open source projects give the same
output as before.

Six programs still differ. One on purpose: `bun check` says `bun add -d
@types/node` where `tsc` says `npm i --save-dev @types/node`. Two need
TS2589 for an index signature that indexes its own instantiation, three
a circle that `tsc` finds through a type alias or through the parameters
of a union of functions.

Five new entries in `test/cli/check/differential-cases.json` keep them
in CI, where both tools run on them again.

| Test | 1.4.3 | this branch |
|---|---|---|
| `99-V4`, 695 of the programs | differs in 156 files | passes |
| `99-V5`, `then` on a union and seven more calls through a union of
generic signatures | fails | passes |
| `99-V6`, a `let`, a `var` and a `const` of another file's `declare
global`, read inside closures | fails | passes |
| `99-V7`, an `accessor` field in a loop, 17 kinds of object literal
property in a circle, a rest element in an array pattern, iterators
without a `value` | fails | passes |
| `99-V8`, a `let` of `declare global` that the reading file assigns,
eight ways | fails | passes |

Instructions executed on one thread, against main: 3.5% fewer on VS
Code, 1.4% more on mikro-orm. On the other projects the difference is
smaller than between two runs of main.
…gration (oven-sh#45051)

### What does this PR do?

`Source lints` fails on main since oven-sh#40057 merged. oven-sh#44805 landed one
commit earlier and added the lint that a percent-encoding helper is
written once, in a shared crate. `src/install/yarn_berry.rs` has a
private `percent_encode` and `percent_decode`.

- `percent_encode` (JavaScript's `encodeURIComponent` on bytes, which is
how Yarn writes a locator inside a reference) moves to
`bun_core::strings::percent_encode_uri_component`.
- `percent_decode` is replaced by
`bun_url::PercentEncoding::decode_alloc`. Text that is not valid
percent-encoding is kept as written, as before.
- The private `hex_val` is replaced by `bun_core::fmt::hex_digit_value`.

No change in behavior.

### How did you verify your code works?

- `bun bd test test/internal/source-lints/shared-helpers.test.ts`: 115
pass. It fails on main.
- `bun bd test test/cli/install/migration/yarn-berry.test.ts`: 58 pass.
…ven-sh#45099)

### What does this PR do?

Bun on Linux arm64 dies with an illegal instruction at startup on
machines where the kernel reports the SVE2 CPU flag but not the base SVE
flag. Bun now uses SVE2 only when both flags are set.

Affected kernels set `HWCAP2_SVE2` from one CPU register field without
checking that SVE exists. The kernel fixed this in
[064737920bdb](torvalds/linux@064737920bdb). A
Linux VM on an Apple M4 (SME, no SVE) is one affected setup.

| Kernel versions with the bug |
|---|
| 5.4 - 6.6.78 |
| 6.7 - 6.12.13 |
| 6.13 - 6.13.2 |

Highway, the SIMD library behind Bun's string search, read `HWCAP2_SVE2`
alone and then ran `cntb`, an SVE instruction, to measure the vector
length. The fix is one condition in `hwy/targets.cc`, carried as
`patches/highway/sve2-requires-sve.patch` because upstream master has
the same code. On a kernel whose flags agree, the result is unchanged.
simdutf, simdjson, vectorscan and Rust's standard library make the same
check.

### How did you verify your code works?

- Bun 1.4.3 arm64 under `qemu-aarch64` 7.2 with `-cpu max,sve=off`,
which reports these flags: SIGILL at the `cntb`.
- Highway's detection built alone with the pinned clang, before and
after the patch, on six qemu CPU models: only `max,sve=off` changes
(SIGILL, then `NEON_BF16`). `max,sve-max-vq=1` still gets `SVE2_128`.
- The kernel table comes from reading `arch/arm64/kernel/cpufeature.c`
at 5.4.302, 5.10.240, 5.15.189, 6.1.189, the last release of 6.2 to 6.5
and 6.7 to 6.11, 6.14, and the releases on each side of the fix in 6.6,
6.12 and 6.13. Nothing older than 5.4 was read, nor the other 5.x series
or 6.0.

No test is added. Not verified: an arm64 build of this branch has not
been run on an affected machine.
Preserve the fork behavior established by same-file main, candidate, upstream, and Node controls. Keep upstream assertions as expected failures, including exact bundler variants, and quarantine only the inherited crashing Linux fuzzer. Full qualification still executes the quarantined file explicitly.
Keep HTTP error-page serialization and ModuleGraph import, disposal, and package-resolution checks active while isolating the inherited frame-position, namespace-key, and diagnostic differences.
Recreate the terminal child until real process registration reports ESRCH.
Check the complete output and exit status on every attempt, and fail with
an explicit precondition error when the bounded setup attempts are exhausted.
Merge the landed worker descriptor tracking optimization into the upstream sync. Preserve both Unreleased entries so the pull request can run its native CI gate.
Keep DYLD_INSERT_LIBRARIES in the Bun process that registers EVFILT_PROC, but omit it from the PTY child environment. The system shell can require arm64e and abort when it inherits the arm64-only test library. Preserve the real ESRCH precondition and all output and status assertions.
@steipete
steipete marked this pull request as ready for review October 17, 2026 11:40
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-17T11:43:04.613667Z 2ff6223 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@steipete
steipete merged commit 4dd73e0 into main Oct 17, 2026
12 checks passed
@steipete
steipete deleted the sync/upstream-2026-10-16 branch October 17, 2026 11:41
steipete added a commit to openclaw/openclaw that referenced this pull request Oct 18, 2026
Upgrade the shared runtime from b76e to Bun 1.4.4 at 7bd4fdff3d with WebKit f8521bb454, preserving the fork's documented compatibility divergences. Includes the upstream sync in openclaw/bun#177, the disabled-source-map startup fix in #189, and VM source-buffer/URI sharing in #190. Align the CI guide with the exact pin.

Qualified by full Linux Gate A and Mac Gate B, including paired CI replay, targeted Node controls, Node-hidden installation, readiness and cold-start checks, HTTP, and memory workloads. The fixed 30-file Linux workload retained all assertions and reduced median final RSS by 35.41%; Mac VM messaging RSS was 0.848x. Dual-all trial 39063500503 had 4 Bun-only failures versus 5, with agent-chat RSS down 27%; all three newly observed failures were classified as OpenClaw test issues and repaired by #175846/#175872. Preserved UI build-race, staged-read ordering and Rosetta noise qualifications are detailed in the PR.

The pin matches the approved candidate byte for byte, including all six platform archive and executable hashes. Focused staging tests pass on Node and S2, and independent P2 review is scoped-clean.
github-actions Bot pushed a commit to jonasbkk1188/openclaw that referenced this pull request Oct 18, 2026
Upgrade the shared runtime from b76e to Bun 1.4.4 at 7bd4fdff3d with WebKit f8521bb454, preserving the fork's documented compatibility divergences. Includes the upstream sync in openclaw/bun#177, the disabled-source-map startup fix in #189, and VM source-buffer/URI sharing in #190. Align the CI guide with the exact pin.

Qualified by full Linux Gate A and Mac Gate B, including paired CI replay, targeted Node controls, Node-hidden installation, readiness and cold-start checks, HTTP, and memory workloads. The fixed 30-file Linux workload retained all assertions and reduced median final RSS by 35.41%; Mac VM messaging RSS was 0.848x. Dual-all trial 39063500503 had 4 Bun-only failures versus 5, with agent-chat RSS down 27%; all three newly observed failures were classified as OpenClaw test issues and repaired by #175846/#175872. Preserved UI build-race, staged-read ordering and Rosetta noise qualifications are detailed in the PR.

The pin matches the approved candidate byte for byte, including all six platform archive and executable hashes. Focused staging tests pass on Node and S2, and independent P2 review is scoped-clean.
github-actions Bot pushed a commit to student-sneha74student-sneha74625/openclaw that referenced this pull request Oct 18, 2026
Upgrade the shared runtime from b76e to Bun 1.4.4 at 7bd4fdff3d with WebKit f8521bb454, preserving the fork's documented compatibility divergences. Includes the upstream sync in openclaw/bun#177, the disabled-source-map startup fix in #189, and VM source-buffer/URI sharing in #190. Align the CI guide with the exact pin.

Qualified by full Linux Gate A and Mac Gate B, including paired CI replay, targeted Node controls, Node-hidden installation, readiness and cold-start checks, HTTP, and memory workloads. The fixed 30-file Linux workload retained all assertions and reduced median final RSS by 35.41%; Mac VM messaging RSS was 0.848x. Dual-all trial 39063500503 had 4 Bun-only failures versus 5, with agent-chat RSS down 27%; all three newly observed failures were classified as OpenClaw test issues and repaired by #175846/#175872. Preserved UI build-race, staged-read ordering and Rosetta noise qualifications are detailed in the PR.

The pin matches the approved candidate byte for byte, including all six platform archive and executable hashes. Focused staging tests pass on Node and S2, and independent P2 review is scoped-clean.
github-actions Bot pushed a commit to Chulixx162Chulixx1622624/openclaw that referenced this pull request Oct 18, 2026
Upgrade the shared runtime from b76e to Bun 1.4.4 at 7bd4fdff3d with WebKit f8521bb454, preserving the fork's documented compatibility divergences. Includes the upstream sync in openclaw/bun#177, the disabled-source-map startup fix in #189, and VM source-buffer/URI sharing in #190. Align the CI guide with the exact pin.

Qualified by full Linux Gate A and Mac Gate B, including paired CI replay, targeted Node controls, Node-hidden installation, readiness and cold-start checks, HTTP, and memory workloads. The fixed 30-file Linux workload retained all assertions and reduced median final RSS by 35.41%; Mac VM messaging RSS was 0.848x. Dual-all trial 39063500503 had 4 Bun-only failures versus 5, with agent-chat RSS down 27%; all three newly observed failures were classified as OpenClaw test issues and repaired by #175846/#175872. Preserved UI build-race, staged-read ordering and Rosetta noise qualifications are detailed in the PR.

The pin matches the approved candidate byte for byte, including all six platform archive and executable hashes. Focused staging tests pass on Node and S2, and independent P2 review is scoped-clean.
github-actions Bot pushed a commit to dhenriqueunespdhenriqueunesp8076/openclaw that referenced this pull request Oct 18, 2026
Upgrade the shared runtime from b76e to Bun 1.4.4 at 7bd4fdff3d with WebKit f8521bb454, preserving the fork's documented compatibility divergences. Includes the upstream sync in openclaw/bun#177, the disabled-source-map startup fix in #189, and VM source-buffer/URI sharing in #190. Align the CI guide with the exact pin.

Qualified by full Linux Gate A and Mac Gate B, including paired CI replay, targeted Node controls, Node-hidden installation, readiness and cold-start checks, HTTP, and memory workloads. The fixed 30-file Linux workload retained all assertions and reduced median final RSS by 35.41%; Mac VM messaging RSS was 0.848x. Dual-all trial 39063500503 had 4 Bun-only failures versus 5, with agent-chat RSS down 27%; all three newly observed failures were classified as OpenClaw test issues and repaired by #175846/#175872. Preserved UI build-race, staged-read ordering and Rosetta noise qualifications are detailed in the PR.

The pin matches the approved candidate byte for byte, including all six platform archive and executable hashes. Focused staging tests pass on Node and S2, and independent P2 review is scoped-clean.
github-actions Bot pushed a commit to u2022Ljw/openclaw that referenced this pull request Oct 18, 2026
Upgrade the shared runtime from b76e to Bun 1.4.4 at 7bd4fdff3d with WebKit f8521bb454, preserving the fork's documented compatibility divergences. Includes the upstream sync in openclaw/bun#177, the disabled-source-map startup fix in #189, and VM source-buffer/URI sharing in #190. Align the CI guide with the exact pin.

Qualified by full Linux Gate A and Mac Gate B, including paired CI replay, targeted Node controls, Node-hidden installation, readiness and cold-start checks, HTTP, and memory workloads. The fixed 30-file Linux workload retained all assertions and reduced median final RSS by 35.41%; Mac VM messaging RSS was 0.848x. Dual-all trial 39063500503 had 4 Bun-only failures versus 5, with agent-chat RSS down 27%; all three newly observed failures were classified as OpenClaw test issues and repaired by #175846/#175872. Preserved UI build-race, staged-read ordering and Rosetta noise qualifications are detailed in the PR.

The pin matches the approved candidate byte for byte, including all six platform archive and executable hashes. Focused staging tests pass on Node and S2, and independent P2 review is scoped-clean.
github-actions Bot pushed a commit to NehaChoudhary6NehaChoudhary69104/openclaw that referenced this pull request Oct 18, 2026
Upgrade the shared runtime from b76e to Bun 1.4.4 at 7bd4fdff3d with WebKit f8521bb454, preserving the fork's documented compatibility divergences. Includes the upstream sync in openclaw/bun#177, the disabled-source-map startup fix in #189, and VM source-buffer/URI sharing in #190. Align the CI guide with the exact pin.

Qualified by full Linux Gate A and Mac Gate B, including paired CI replay, targeted Node controls, Node-hidden installation, readiness and cold-start checks, HTTP, and memory workloads. The fixed 30-file Linux workload retained all assertions and reduced median final RSS by 35.41%; Mac VM messaging RSS was 0.848x. Dual-all trial 39063500503 had 4 Bun-only failures versus 5, with agent-chat RSS down 27%; all three newly observed failures were classified as OpenClaw test issues and repaired by #175846/#175872. Preserved UI build-race, staged-read ordering and Rosetta noise qualifications are detailed in the PR.

The pin matches the approved candidate byte for byte, including all six platform archive and executable hashes. Focused staging tests pass on Node and S2, and independent P2 review is scoped-clean.
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Oct 19, 2026
Upgrade the shared runtime from b76e to Bun 1.4.4 at 7bd4fdff3d with WebKit f8521bb454, preserving the fork's documented compatibility divergences. Includes the upstream sync in openclaw/bun#177, the disabled-source-map startup fix in openclaw#189, and VM source-buffer/URI sharing in openclaw#190. Align the CI guide with the exact pin.

Qualified by full Linux Gate A and Mac Gate B, including paired CI replay, targeted Node controls, Node-hidden installation, readiness and cold-start checks, HTTP, and memory workloads. The fixed 30-file Linux workload retained all assertions and reduced median final RSS by 35.41%; Mac VM messaging RSS was 0.848x. Dual-all trial 39063500503 had 4 Bun-only failures versus 5, with agent-chat RSS down 27%; all three newly observed failures were classified as OpenClaw test issues and repaired by openclaw#175846/openclaw#175872. Preserved UI build-race, staged-read ordering and Rosetta noise qualifications are detailed in the PR.

The pin matches the approved candidate byte for byte, including all six platform archive and executable hashes. Focused staging tests pass on Node and S2, and independent P2 review is scoped-clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants