node compat batch: callback-throw dispatch, Assert class + native deep-equality parity, Intl gate + URL/buffer fallout, compile cache, watch kill-signal, profilers (+98 tests)
Batch of node-compat fixes that unblock each other, verified against the node v26.3.0 binary throughout. Adds 98 vendored upstream tests (parallel + sequential), all passing.
Callback-throw dispatch (the original headline)
A throw inside a node-style callback in node:fs, node:dns or crypto.pbkdf2 surfaced as an unhandledRejection instead of an uncaughtException, because those callbacks dispatch from a promise reaction. guardCallback in internal/shared.ts routes the exception to the same native uncaught path timers already use, applied at the existing chokepoints (ensureCallback in fs, validateResolve in dns, pbkdf2). Happy-path timing is byte-identical to base, verified by diffing a full event-loop ordering trace. The throw reaches uncaughtException synchronously from the callback's own frame, matching node's measured semantics.
Assert class + node-correct deep equality
Node v26's Assert class (per-instance diff/strict/skipPrototype, ERR_CONSTRUCT_CALL_REQUIRED). Deep equality is confined by API ownership: the node entry points (assert strict variants, util.isDeepStrictEqual) compare prototypes and own-enumerable properties like node, while Bun.deepEquals and every expect() instantiation are verified behavior-identical to before. assert.partialDeepStrictEqual's comparison engine is implemented natively in C++ (object/array/Map/Set subset semantics, Date/RegExp/boxed-primitive/DataView arms, cycle guards), pinned by a cumulative matrix run byte-identical against node.
Intl gate fix + fallout, solved together
process.config.variables spelled the key v8_enable_i8n_support, so every vendored test gated on common.hasIntl silently skipped its Intl half. This lands the typo fix with the fallout: buffer.transcode (simdutf fast paths), node's host-parse semantics for domainToASCII/Unicode including the Unicode 16 IDNA delta node ships via ada (Bun's ICU is older; the delta is applied at the node:url entry points, cited in NodeURL.cpp), url.format/url.parse host rules, TextDecoder fatal messages, util.inspect URL formatting.
Two user-visible changes called out for review: new URL(bad) now throws node's exact TypeError: Invalid URL with code/input, and invalid-punycode xn-- hosts are rejected for special schemes (matching node, Chrome, Firefox, and WPT).
Also here
module.enableCompileCache()/NODE_COMPILE_CACHE: bytecode cache with atomic persist at exit (including the self-kill signal path), persist pass runs unlocked so Worker module loads never stall.--watch-kill-signal: watch reloads deliver the configured signal to JS listeners before execve, on POSIX and Windows (real Windows signal numbers, not Linux discriminants).--cpu-profand--heap-profwrite real V8-format profiles/snapshots with node's shared filename sequence.- async_hooks: AsyncResource lifecycle + tick/timer hooks behind the existing enable-gates (promise hooks need a WebKit-fork patch and are explicitly out of scope).
bun -pealias, internal-module shims for vendored tests (internal/*interceptor merges native implementations over the vendored ports),require.resolvebuiltin handling and paths validation moved native,isURLandstringWidth(perCodePoint)native.
Verification
Interleaved same-commit sweeps with zero regressions across fs/dns/domain/pbkdf2 (356 vendored files), assert (361), url, util, expect (510), plus the bun-side suites for each subsystem. Deleted along the way rather than shipped red: test-child-process-advanced-serialization.js (passes vacuously on main via the runner's swallowed-rejection bug #34859; the real gap is #34860) and test-whatwg-url-toascii.js (WHATWG URL constructor goes through WebKit + system ICU, so distro data decides; returns with the ICU-bundling decision).
no test proof · iteration 48 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/cli/heap-prof.test.ts test/cli/watch/watch.test.ts test/js/bun/resolve/resolve-error.test.ts test/js/bun/spawn/spawn.ipc.test.ts test/js/node/fs/fs.test.ts test/js/node/module/node-module-module.test.js test/js/node/process/process.test.js
Compat impact (upstream node v26.3.0 test files vendored, in-tree = passing)
- module.enableCompileCache: 5% → 64% (1 → 14 of 22)
- --cpu-prof: 9% → 73% (1 → 8 of 11)
- --heap-prof: 0% → 18% (0 → 2 of 11)
- watch mode: 0% → 30% (0 → 3 of 10)
- node:url: 80% → 100% (12 → 15 of 15)
- node:util: 67% → 70% (20 → 21 of 30)
- node:fs: 98% → 99% (340 → 342 of 346)
- node:assert: 14 → 16 vendored (upstream parallel/sequential set: 13; bun's set carries one file from an earlier upstream)