Conversation
|
Warning Review limit reached
Next review available in: 36 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (4)
Comment |
|
Updated 3:20 PM PT - Jul 6th, 2026
❌ @robobun, your commit 5cf9502 has some failures in 🧪 To try this PR locally: bunx bun-pr 33434That installs a local version of the PR into your bun-33434 --bun |
|
@coderabbitai review |
✅ Action performedReview finished.
|
|
Found 3 issues this PR may fix:
🤖 Generated with Claude Code |
|
I checked each of these against the actual diff and I don't think any of the three should be auto-closed by this PR, so I'm leaving the #13165 (shimmer / Sentry): the failure there is #7022 (graceful-fs): that issue asks for an integration test, and import * as fsns from "node:fs"; // hoisted, namespace snapshotted here
import { createRequire } from "node:module";
createRequire(import.meta.url)("fs").readFileSync = () => "patched";
fsns.readFileSync(); // still the original, in bun AND node#3775 (OpenTelemetry): that one is about For what it's worth, I grepped the installed sources of What it does do is make the documented API behave like node's for code that does call it, instead of silently returning |
It was a stub that returned undefined, so a patch applied to a builtin's CommonJS exports object never reached the ESM namespace bindings that were snapshotted from it at import time. Walk the ESM registry for builtin modules, and for each one re-read every named export off the builtin's CommonJS exports object and re-bind the namespace slot through JSModuleNamespaceObject::overrideExportValue. `default` is left alone, matching node.
64a568d to
1d3bb8d
Compare
…tins
node:buffer, node:constants, node:string_decoder, node:util/types, bun:jsc,
bun:test, bun:app, utf-8-validate and abort-controller all have a CommonJS
entry in InternalModuleRegistry, but their ESM namespace was built by running
the native generator a second time. That produced a separate exports object
whose function exports were distinct JSFunction instances, so
import('node:buffer').default !== require('node:buffer') and, worse,
syncBuiltinESMExports() re-read the CommonJS object and swapped every ESM
binding for an identity-different twin even when nothing had been patched.
Route their ESM namespace through InternalModuleRegistry the way every other
builtin already does, so the two resolve to one object. Builtins whose CommonJS
value is a canonical singleton instead of a registry entry (node:module,
node:process, bun) keep the native generator.
Also skip records that have not linked yet, matching the other registry walkers.
|
Both findings were right. Fixed in 6bd6ff3. The identity swap (blocker)Reproduced exactly as described, and the diagnosis was correct down to the mechanism: the ESM path ran import { isUtf8 } from "node:buffer";
import { syncBuiltinESMExports } from "node:module";
const before = isUtf8;
syncBuiltinESMExports(); // nothing patched
console.log(isUtf8 === before);The note that my I went with the second suggestion (share the cache) rather than skipping natives, because skipping would have made This also fixes a latent divergence for free:
Verified across all nine rerouted modules that ESM and CommonJS now agree on the export names, the export values, and The unlinked-record guard (nit)Added |
Status: ready for review, CI red only on a starved darwin laneLatest build (#69132, sha The single failure is Why this is infrastructure and not the diff:
The earlier red I have used my one re-run and will not push again, since only a push re-triggers CI and more empty commits are noise. What changedTwo commits:
Verification
|
|
Closing as part of a cleanup of stale pull requests. This PR has had no new commits since 2026-07-06, it conflicts with main, and its last CI run failed. This is not a judgment on the fix itself. If the problem still reproduces on a current build, reopen this PR after a rebase or open a new one against main. |
module.syncBuiltinESMExports()was a stub that returnedundefined. A patch applied to a builtin's CommonJSexportsobject (the graceful-fs / APM instrument-and-patch pattern) never reached the ESM namespace bindings that were snapshotted from it at import time. Patch libraries saw a successful call and kept running, while everyimport ... from 'node:fs'consumer in the same process held the unpatched binding.Repro
Cause
jsFunctionSyncBuiltinESMExportsinsrc/jsc/modules/NodeModuleModule.cppwasreturn jsUndefined();. Importing a builtin creates aSyntheticModuleRecordwhose export values are copied out of the builtin's CommonJS exports object once, at instantiation. Nothing re-read them afterwards.Fix
Walk the ESM registry for builtin module keys, and for each one fetch the builtin's CommonJS exports object (the same lookup
process.getBuiltinModule()uses) and re-bind every named export throughJSModuleNamespaceObject::overrideExportValue, which writes the module environment slot and touches its watchpoint set. That is the same pathmock.module()andspyOn()already use, so both named imports and the namespace object observe the new value.Semantics follow node's
syncBuiltinESMExportsinlib/internal/modules/esm/utils.js:undefined(the export itself stays)defaultis left pointing at the exports objectRegistry keys are snapshotted before any JS runs, because reading an export can invoke a getter that loads another module. Records that have not linked yet are skipped, matching the other registry walkers.
Prerequisite: one exports object per native builtin
This relies on an invariant node guarantees and bun was violating for nine builtins.
node:buffer,node:constants,node:string_decoder,node:util/types,bun:jsc,bun:test,bun:app,utf-8-validateandabort-controllereach have a CommonJS entry inInternalModuleRegistry, but their ESM namespace was built by running the native generator a second time.INIT_NATIVE_MODULE'sputNativeFnmints a freshJSFunctionper run, sorequire()andimportended up with distinct-but-equivalent twins. Syncing would then swap every ESM binding for its twin even when nothing had been patched, breaking===,WeakMapkeys, and spies held against stored references.They all already carry
InternalModuleRegistryFlag, so they now fall through togenerateInternalModuleSourceCode(which resolves viarequireId) instead of being intercepted by the ESM switch.node:module,node:processandbunkeep the native generator: their CommonJS value is a canonical singleton rather than a registry entry, and their generators already read off that same singleton.This also closes a latent divergence:
(await import('node:buffer')).default === require('node:buffer')falsetruetrueVerification
bun bd test test/js/node/module/node-module-module.test.js— 36 pass. The six new cases cover the repro, named-import bindings, delete/add semantics, a native builtin, identity stability of unpatched exports, and the untouched-until-called control. Three of the six fail onUSE_SYSTEM_BUN=1.The repro now prints node's output byte for byte. Also checked, not in the test file: a throwing getter propagates out of
syncBuiltinESMExports()exactly as in node, the function works inside workers, and 200 syncs across 10 imported builtins underBun.gc(true)plus a getter that loads a module mid-iteration run clean underBUN_JSC_validateExceptionChecks=1.bun:testis one of the rerouted modules and is imported by every test file, so the blast radius got the most attention. Verified all nine now agree withrequire()on export names, export values anddefault, and rantest/js/bun/test/mock/+mock-fn(95 pass),test/js/node/module/(95 pass),buffer.test.js(543 pass),string_decoder/(86 pass).