Skip to content

node:module: list node:test in builtinModules - #39700

Open
robobun wants to merge 4 commits into
mainfrom
farm/dd1f684a/builtin-modules-node-test
Open

node:module: list node:test in builtinModules#39700
robobun wants to merge 4 commits into
mainfrom
farm/dd1f684a/builtin-modules-node-test

Conversation

@robobun

@robobun robobun commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Problem

Fix

Background

  • module.builtinModules is the public array of builtin names. Bundlers use it as the default external list. Bun's array also holds Bun's own modules and the packages Bun replaces, such as ws.
  • module.isBuiltin(name) has its own table. It also accepts internal names such as bun:wrap, which Bun.plugin must refuse to override.
  • A prefix-only builtin resolves as node:x but not as x. In Node 26: sea, sqlite, test, test/reporters. Bun has sqlite and test (node_entry_only_prefix!, src/resolve_builtins/HardcodedModule.rs:472).
  • Source lints (test/internal/source-lints/) read the source tree and run on GitHub Actions without a build.
Notes
  • Repro: bun -e 'const m=require("node:module"); console.log(m.isBuiltin("node:test"), m.builtinModules.includes("node:test"), typeof require("node:test"))' prints true false function. Node prints true true. With this change Bun prints true true function.
  • History: Initial support for node:test #18140 (v1.2.6) listed node:test, require.resolve with paths option #18851 (v1.2.9) dropped it again and bumped the length assertion in node-module-module.test.js from 77 to 76. That is why the runtime test no longer pins the length. It still asserts that bun:wrap is not listed, which is what the length covered since process: port Node.js v26.3.0 process compatibility tests and fix the gaps they surface (env exotic-object/TZ semantics, warnings pipeline + CLI flags, uncaught origin/exit codes, execve throw, threadCpuUsage/finalization/loadEnvFile, native-module identity; +26 tests) #31831.
  • Other suites run with the debug build, all green: test/js/bun/test/parallel/test-require-builtins.ts (it now also requires node:test in a subprocess), test/js/node/test/parallel/test-process-get-builtin.mjs (it now checks process.getBuiltinModule("node:test") === require("node:test") through the array), test-require-resolve.js, test-module-builtin.js, test/js/bun/resolve/builtin-esm-lazy-exports.test.ts, test/js/node/sqlite/node-sqlite.test.ts, the builtin-related tests in test/js/bun/repl/repl.test.ts, and all of test/internal/source-lints/.
  • The lint against the shape of node:test: --test CLI mode, node:test/reporters, standalone execution, and reporter-output parity (+18 tests, test_runner 32%→55%) #34515 (add node:test/reporters to isBuiltinModule.cpp and HardcodedModule.rs only): it fails with ["node:test/reporters"]. Node 26.3.0 lists node:test/reporters, so that is the wanted outcome. The lint also fails when an allowlist entry goes stale.
  • Set difference against Node 26.3.0 before this change: Node lists node:sea, node:test and node:test/reporters, Bun lists none of them. require of node:sea and node:test/reporters throws ERR_UNKNOWN_BUILTIN_MODULE in Bun. After this change the only entries Bun lacks are modules Bun does not have.
  • node:quic: Bun resolves it without a flag and prints an ExperimentalWarning. The stock node 26.3.0 binary does not list it, also not with --experimental-quic, because QUIC is compiled out. To list it is a separate decision. The allowlist entry records the current one.
  • Not covered by the lint: the non-prefix entries of the resolver (they also hold flag-gated modules such as stream/iter and third-party aliases), and the separate table behind process.binding("natives") in src/jsc/bindings/ProcessBindingNatives.cpp. A table derived from the resolver's Alias entries would remove the copies. That is a larger change than this fix.
  • node:module: build builtinModules from the same table as isBuiltin, and freeze it #33430 fixed the node:test entry by merging the two tables into one. That would list bun:wrap and bun:main again, and the branch conflicted with main, so I closed it. The freeze of builtinModules that it also did is independent of this bug and is not part of this change.
  • Other consumers of the array: the REPL's getBuiltinLibs() already filters out node: names (src/js/internal/repl/node-shims.js:183). util.inspect only looks up bare names taken from stack frames.

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/node/module/node-module-module.test.js

Node lists the modules that only resolve with the node: prefix under
that prefixed name in module.builtinModules. Bun already does this for
node:sqlite. node:test was missing from the list although
isBuiltin("node:test") is true and require("node:test") works.
@coderabbitai

coderabbitai Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 45eb3373-5f4d-4158-9980-093a7a9bd761

📥 Commits

Reviewing files that changed from the base of the PR and between 8baf99b and f06a92c.

📒 Files selected for processing (4)
  • .github/workflows/source-lints.yml
  • src/jsc/modules/NodeModuleModule.cpp
  • test/internal/source-lints/builtin-module-tables.test.ts
  • test/js/node/module/node-module-module.test.js

Included review availability: Your plan provides up to 5 included reviews per hour; 0 remain after this review.


Walkthrough

Changes

Builtin module support

Layer / File(s) Summary
Builtin module metadata
src/jsc/modules/NodeModuleModule.cpp
The builtin module names include node:test. Comments identify node:test and node:sqlite as prefix-only modules.
Builtin module validation
test/js/node/module/node-module-module.test.js, test/internal/source-lints/builtin-module-tables.test.ts, .github/workflows/source-lints.yml
Tests validate prefix-only listings, builtin recognition, resolution equivalence, and consistency between source tables and runtime behavior. Source-lint workflows now run when the builtin module table changes.

Possibly related PRs

  • oven-sh/bun#39316: Both changes update Node builtin module handling and validate node:-prefixed specifiers.

Suggested reviewers: cirospaciari, jarred-sumner

Merge Risk: 🔵 Low · up to f06a9

The PR adds coverage for builtin-module consistency, but its CI workflow may run that lint under the wrong build configuration. The change is otherwise localized and mergeable with owner awareness or a follow-up to use the required debug-build test command.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies the primary change: adding node:test to builtinModules.
Description check ✅ Passed The description explains the problem, fix, scope, exceptions, and verification results, although it uses different headings from the template.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

@robobun

robobun commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 10:00 PM PT - Aug 19th, 2026

@robobun, your commit f06a92c3f44de5ce9ed8afb4433407c6dc331071 passed in Build #101489! 🎉


🧪   To try this PR locally:

bunx bun-pr 39700

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

bun-39700 --bun

@robobun

robobun commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator Author

Status

Reproduced on main (cfa9f8e15b) with the released binary:

$ bun -e 'const m=require("node:module"); console.log(m.isBuiltin("node:test"), m.builtinModules.includes("node:test"), typeof require("node:test"))'
true false function
$ node -e 'const m=require("node:module"); console.log(m.isBuiltin("node:test"), m.builtinModules.includes("node:test"))'
true true

With this branch the debug build prints true true function. Two tests fail on main and pass with the change: the new source lint test/internal/source-lints/builtin-module-tables.test.ts (on main it reports ["node:test"] as missing from the table) and test/js/node/module/node-module-module.test.js.

Fix: this PR (#39700). #33430 was an earlier attempt with a single table. It is closed.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM — one string literal added to a static table, following the existing node:sqlite pattern.

What was reviewed:

  • Counted the table: 77 entries after the addition, matching the updated toHaveLength(77).
  • isBuiltinModule.cpp already lists node:test, so the new isBuiltin/process.getBuiltinModule assertions hold.
  • Only other consumer of node:-prefixed entries is the REPL's getBuiltinLibs(), which already filters them out (src/js/internal/repl/node-shims.js:184).
  • New tests assert the exact prefixed set and that bare "test" stays out; they fail on main per the PR notes.
Extended reasoning...

Overview

Adds "node:test"_s to the static builtinModuleNames array in src/jsc/modules/NodeModuleModule.cpp, alongside the existing "node:sqlite"_s entry, plus a three-line comment explaining the prefix-only convention. Bumps the length assertion in node-module-module.test.js from 76 to 77 and adds two focused tests: one enumerating the node:-prefixed subset and asserting each is prefix-only and requireable, and one in node-test.test.ts asserting node:test is a builtin.

Security risks

None. The change is a compile-time string literal in a constexpr array; no user input, no allocation, no new code paths, no exception scopes touched.

Level of scrutiny

Low. This is a one-line data change to a lookup table that already contains an entry of the exact same shape (node:sqlite, added in #32498). The array is consumed only by getBuiltinModulesObject via countof(), so no hardcoded size to update elsewhere. I verified the entry count (77) by hand, confirmed isBuiltinModule.cpp:48 already recognizes node:test (so the new test's isBuiltin and process.getBuiltinModule assertions are consistent), and confirmed the REPL consumer at src/js/internal/repl/node-shims.js:184 filters node:-prefixed names so completion is unaffected.

Other factors

The PR description is unusually thorough — it enumerates the set difference against Node 26.3.0, justifies leaving out node:quic (not in stock Node builds), bun:wrap/bun:main (deliberately removed in #31831), and defers freezing the array as an independent change. Tests are placed in the correct existing files, use exact-value assertions (toEqual(["node:sqlite", "node:test"]) rather than toContain), and cover the negative contract (bare "test" absent, isBuiltin("test") false). No outstanding reviewer comments; no prior reviews from me.

@robobun

robobun commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks, the review asks for no changes.

@coderabbitai review

(The first attempt hit the review rate limit before it started.)

@coderabbitai

coderabbitai Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

@robobun, I will review PR #39700.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

module.builtinModules and module.isBuiltin() read two hand-written
tables, and the module loader has a third marker for the builtins that
resolve with the node: prefix only. A new module tends to reach the
last two and not the first. The new source lint requires every name
isBuiltin() accepts to be listed in builtinModules, with an allowlist
that names the reasons for the deliberate exceptions.

The runtime test now checks the array's properties instead of its
length, and the copy of those assertions in node-test.test.ts is gone.
Comment thread src/jsc/modules/NodeModuleModule.cpp Outdated
Comment thread src/jsc/modules/NodeModuleModule.cpp Outdated
Comment thread src/jsc/modules/NodeModuleModule.cpp
@robobun

robobun commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator Author

Update after a self-review of the first version. The head is now f06a92c.

  • New source lint test/internal/source-lints/builtin-module-tables.test.ts. It compares the isBuiltin() table with builtinModules. On main it fails with ["node:test"]. It also fails on the shape of node:test: --test CLI mode, node:test/reporters, standalone execution, and reporter-output parity (+18 tests, test_runner 32%→55%) #34515, which adds node:test/reporters to the isBuiltin() table only.
  • The length assertion in node-module-module.test.js is gone. The test checks the properties of the array instead. The duplicate test in node-test.test.ts is gone too.
  • The comment above the table only has its first sentence corrected. The three comment-cop threads about it are answered and resolved.

The PR description describes the current state.

@robobun

robobun commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

The three commits after 8baf99b (the source lint, the test rework and the comment change) have not been reviewed yet. The earlier attempts hit the review rate limit.

@coderabbitai

coderabbitai Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

@robobun, I will review the commits after 8baf99b07d.

⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@robobun

robobun commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM — one-line data addition to a static table, matching Node's behavior for prefix-only builtins.

What was reviewed:

  • Verified node:test is already in isBuiltinModule.cpp and node_entry_only_prefix!(), so this only closes the gap in builtinModules.
  • Checked the new source-lint's notListed entries (bun:main, bun:wrap, node:quic) against both tables — all consistent; the regex parsers match the actual table declarations.
  • Confirmed the runtime test's new loop over node:-prefixed entries only hits node:sqlite and node:test, both requireable in-process; the removed .toHaveLength(76) is subsumed by the cross-table lint.
Extended reasoning...

Overview

The PR adds "node:test"_s to the builtinModuleNames[] table in src/jsc/modules/NodeModuleModule.cpp so that require("node:module").builtinModules includes it, matching Node 26. It also corrects the first sentence of the pre-existing comment above that table, adds a source-lint test that cross-checks the three hand-written builtin-name tables (builtinModuleNames, builtinModuleNamesSortedLength, node_entry_only_prefix!), wires the new file into the source-lints workflow path filter, and replaces the fragile .toHaveLength(76) runtime assertion with invariant-based checks.

Security risks

None. This is a metadata list of module names; adding an entry does not change what require() resolves — node:test was already resolvable and already accepted by isBuiltin(). The source-lint test only reads source files with regex.

Level of scrutiny

Low. The production change is a single string literal appended to a static constexpr ASCIILiteral[], following the exact precedent set by node:sqlite immediately above it. No control flow, no allocation, no exception paths are touched. The test and workflow changes are defensive tooling.

Other factors

  • I verified the source-lint test's assumptions hold: isBuiltinModule.cpp contains bun:main, bun:wrap, node:quic, node:test, node:sqlite; HardcodedModule.rs has node_entry_only_prefix! for node:sqlite, node:test, node:quic. The notListed map exactly covers the intended isBuiltin-only names, and the reverse check ensures the map cannot go stale.
  • The runtime test dropped the hardcoded length in favor of stronger invariants (every listed name passes isBuiltin, prefix-only entries are not resolvable bare, process.getBuiltinModule matches require). The lint test enforces completeness in the other direction, so no coverage is lost.
  • The comment-cop bot flagged the table comment three times; the author iterated down to a minimal one-sentence correction of the pre-existing comment, and all threads are resolved.
  • The PR description references assertions in node-test.test.ts that are not in the final diff — presumably reverted in a later commit — but this does not affect the change's correctness or test coverage.
  • The PR author enumerated downstream consumers of builtinModules (REPL, util.inspect, test-require-builtins, test-process-get-builtin) and explained why each is unaffected or improved.

@robobun

robobun commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks. On the one observation: the PR description was rewritten for the current diff before this review ran and no longer mentions node-test.test.ts. Only the update comment above mentions it, as the test that 08b77b7 removed. No changes needed.

@robobun

robobun commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator Author

On the "wrong build configuration" note in the merge risk summary: the new test is a source lint. It reads three source files and never runs the bun binary, so the source-lints workflow runs it with a released bun on purpose. That is the documented setup for every test in test/internal/source-lints/ (see the README there). The workflow change in this PR only adds NodeModuleModule.cpp to the path filter, so that an edit to the table triggers the lint. The Buildkite lanes are unaffected: .buildkite/ci.mjs excludes this directory.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant