node:module: build builtinModules from the same table as isBuiltin, and freeze it - #33430
node:module: build builtinModules from the same table as isBuiltin, and freeze it#33430robobun wants to merge 1 commit into
Conversation
|
Updated 10:16 AM PT - Jul 6th, 2026
❌ @robobun, your commit 0cb6a72 has some failures in 🧪 To try this PR locally: bunx bun-pr 33430That installs a local version of the PR into your bun-33430 --bun |
|
Warning Review limit reached
Next review available in: 20 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 |
|
@coderabbitai review (The previous attempt hit the review rate limit before it started.) |
There was a problem hiding this comment.
LGTM — clean consolidation of the two duplicate builtin-module tables into one source of truth, plus freezing to match Node.
Extended reasoning...
Overview
This PR eliminates a duplicate hardcoded table: module.builtinModules and module.isBuiltin() previously read from two separate 76- and 78-entry ASCIILiteral[] tables in different files, which had drifted apart (node:test and bun:main were in one but not the other). The fix collapses them into one table in isBuiltinModule.cpp, exposes it via Bun::builtinModuleNames() returning a std::span, and has getBuiltinModulesObject build the JS array from that span and freeze it via JSC::objectConstructorFreeze. Four files touched: two C++ source files, one header, one test file.
Security risks
None. This is a static compile-time list of module name string literals; no user input flows into it, no auth/crypto/permissions surface is touched.
Level of scrutiny
Low-to-moderate. The change is fundamentally a table dedup plus a one-line Object.freeze for Node compat. I verified:
- The consolidated 78-entry table is set-identical to the old
builtinModuleNamesSortedLength(just reordered from length-sort to alpha-sort);isBuiltinModule()does a linear scan so order is irrelevant to its behavior. - The
builtinModulesarray keeps the same relative order as the old NodeModuleModule.cpp table (both alpha-sorted), gainingbun:mainin thebun:*block andnode:testat the end. - No other files reference the removed
builtinModuleNames/builtinModuleNamesSortedLengthsymbols. bun:mainis a real resolvable module (src/resolve_builtins/HardcodedModule.rs), so adding it tobuiltinModuleswon't breaktest-require-builtins.ts.
Other factors
- Exception handling is correct:
constructArrayover owned JSStrings andobjectConstructorFreezeon a plain dense array cannot run user code, soscope.assertNoException()(rather thanRETURN_IF_EXCEPTION) is the right choice, and the author verified withBUN_JSC_validateExceptionChecks=1. - Freezing
builtinModulesis a behavior change but strictly Node-aligning; only code that mutates the shared array (which was always a bug) is affected. - Tests cover the new length,
isBuiltinagreement,node:testpresence / baretestabsence, and frozenness with apush()throwingTypeError. - No CODEOWNERS on these paths; no outstanding human review comments.
|
Thanks, no changes needed from the review. One judgement call worth flagging, since it is the only user-visible addition beyond `node:test`: `bun:main` now shows up in `builtinModules`. It is there because `isBuiltin("bun:main")` already returned true and the loader resolves it, so keeping it out of the array would mean either a second table or a filter, which is exactly the drift this PR removes. `bun:wrap` was already in the list, so it is consistent with what shipped before. Happy to drop it if you would rather `builtinModules` not carry that one. |
d50c6e7 to
308fc63
Compare
|
Rebased onto main and force-pushed ( The red CI on the old sha was not from this PR. Every test lane failed I branched off Confirmed by running both test-file versions against the same rebased binary:
The other failures in build 68811 were single-lane one-offs (
|
There was a problem hiding this comment.
I didn't find any bugs, but you flagged the bun:main inclusion as a judgement call you'd like a maintainer to weigh in on, and freezing builtinModules is a user-visible behavior change — worth a quick human sign-off.
Extended reasoning...
Overview
This PR consolidates two duplicate hardcoded builtin-module tables into a single source of truth in isBuiltinModule.cpp, exposed via Bun::builtinModuleNames(). getBuiltinModulesObject in NodeModuleModule.cpp now builds module.builtinModules from that shared table and freezes the resulting array to match Node.js. The net user-visible effect is that builtinModules gains two entries (node:test and bun:main, going from 76 → 78) and becomes immutable. isBuiltin() behavior is unchanged since it already consulted the 78-entry table (just reordered from length-sorted to alphabetical, which doesn't matter for a linear scan).
Security risks
None. This is a static string table and array construction; no untrusted input is parsed, no auth/crypto/permissions paths are touched.
Level of scrutiny
Medium. The refactor itself is mechanical and well-motivated (eliminating drift between two tables). However, this is user-facing Node-compat API surface: freezing builtinModules will make previously-silent .push()/.sort() calls throw, and adding bun:main to the public list is an API-shape decision the author explicitly flagged as open ("Happy to drop it if you would rather builtinModules not carry that one"). The C++ side also adds a ThrowScope with scope.assertNoException() after constructArray/objectConstructorFreeze — the reasoning (neither runs user code on a dense string array) looks sound and was validated with BUN_JSC_validateExceptionChecks=1, but JSC exception-scope handling in a lazy PropertyCallback is worth a maintainer glance.
Other factors
- I verified the new 78-entry table is set-identical to the old
builtinModuleNamesSortedLengthtable, soisBuiltin()semantics are preserved. - No internal code mutates
builtinModules(grep confirms), so freezing won't break Bun itself. - Tests are solid: length assertion updated, new invariant test that every
builtinModulesentry passesisBuiltin(), and a freeze test. The PR description lists several downstream test suites that were run. - The author's own comment on the thread is soliciting maintainer input on the
bun:maininclusion, which is a clear signal this shouldn't be auto-approved.
|
Agreed both points want a human call. Here is the data so it is a quick one.
|
| specifier | isBuiltin() |
in builtinModules |
|---|---|---|
bun:main |
true | true (was false) |
bun:wrap |
true | true |
bun:ffi |
true | true |
node:test |
true | true (was false) |
bun:app |
false | false |
bun:internal-for-testing |
false | false |
isBuiltin("bun:main") already returns true on shipped Bun. This PR only makes the array agree with the predicate that was already there, and bun:wrap (equally internal plumbing, also a synthetic entry! in HardcodedModule.rs) has been in the array all along.
Omitting bun:main from the array alone would re-create the exact bug this PR fixes, just under a different name. If you would rather it be hidden, the consistent change is to drop it from the shared table, which also flips isBuiltin("bun:main") to false. That is a one-line change and I am happy to make it, but it is a behavior change to isBuiltin on top of this one, so I did not fold it in unasked.
Freezing
Matches Node, which has frozen it for years. Nothing inside Bun mutates the array (the one build-time consumer, src/node-fallbacks/build-fallbacks.ts, already copies with [...builtins]). The only code that breaks is code mutating a process-wide shared array, which was never safe.
CI
Green on the rebased sha so far: https://buildkite.com/bun/bun/builds/68961 (0 failed jobs; the previous build's 14 red lanes were the stale cookie-map base, fixed by the rebase).
308fc63 to
f925866
Compare
|
Force-pushed For the record, build 68961's failures were unrelated to this PR:
No annotation in that build mentions Also confirmed the release profile, not just debug: The release binary reports |
…nd freeze it module.builtinModules and module.isBuiltin() read from two separate hardcoded tables that had drifted apart: the builtinModules table was missing node:test and bun:main, both of which isBuiltin() reports as builtins and the module loader resolves. Tools that decide "is this a builtin?" with builtinModules.includes(x) therefore disagreed with Bun's own loader. The array was also mutable, so a push() from one module was visible to every later reader in the process. Node freezes it. Build the array from the table isBuiltinModule consults, and freeze it.
f925866 to
0cb6a72
Compare
Status: diff is green, CI is red on infrastructure onlyBuild 69024 at Both failures are the same job ( Every other The test files that appear in the build annotations ( I am not going to force-push again to re-roll a 503, since that just queues another 280-job build without learning anything. A maintainer re-running the darwin job should clear it. What this PR needs
Verified locally on both profiles: debug+ASAN and release each build clean and pass |
|
Closing this. Main has moved the other way since this PR was opened. #31831 removed The one entry this PR found missing, |
Repro
Node prints
true,true,"function",true, then throws aTypeErroron thepush.Cause
module.builtinModulesandmodule.isBuiltin()read from two separate hardcoded tables:builtinModuleNamesinsrc/jsc/modules/NodeModuleModule.cppfeedsbuiltinModules(76 names)builtinModuleNamesSortedLengthinsrc/jsc/bindings/isBuiltinModule.cppfeedsisBuiltin()(78 names)The second is a strict superset: it has
node:testandbun:main, both of which the module loader resolves. SobuiltinModuleslisted nonode:-prefixed name at all, and tools that decide "is this a builtin?" withbuiltinModules.includes(x)(bundlerexternallists, module mocks, dependency analyzers) disagreed with Bun's own loader.The array was also never frozen, and
builtinModulesis a lazily-reified property that caches one array per global object. Apush()from one module was visible to every later reader in the process.Fix
One table, in
isBuiltinModule.cpp, exposed asBun::builtinModuleNames().getBuiltinModulesObjectbuilds the array from it and freezes it, matching Node.builtinModulesgains exactly the two entries it was missing:node:test(listed with the prefix, since the bare name is not a builtin, same as Node does fornode:sea/node:sqlite/node:test) andbun:main.isBuiltin()consults the same set it did before, so its behavior is unchanged.Bun only ships
node:testas a prefix-only builtin, so that is the onlynode:-prefixed entry;node:sea,node:sqliteandnode:test/reportersdo not exist in Bun.Verification
Three new/updated assertions fail on
mainand pass with the fix: the length is now 78,builtinModules.filter(n => !isBuiltin(n))is empty and containsnode:test, andObject.isFrozen(builtinModules)is true withpush()throwing.Also run:
test/js/bun/test/parallel/test-require-builtins.ts(requires every entry ofbuiltinModulesin a subprocess, now includingnode:testandbun:main)test/js/node/test/parallel/test-module-builtin.js,test/js/node/test/parallel/test-require-resolve.jstest/js/node/module/(91 pass),test/js/bun/plugin/(35 pass)Exception-check validation
constructArrayandobjectConstructorFreezeeach open aThrowScopeat the same recursion depth inside the lazy property callback, whichBUN_JSC_validateExceptionChecks=1flags unless the first is checked before the second opens. Both the CJS and ESM paths are clean: