Skip to content

node:repl: stop aliasing globalThis as repl.context in the stub - #34286

Closed
robobun wants to merge 4 commits into
mainfrom
farm/1bcc0143/repl-stub-module-shape
Closed

node:repl: stop aliasing globalThis as repl.context in the stub#34286
robobun wants to merge 4 commits into
mainfrom
farm/1bcc0143/repl-stub-module-shape

Conversation

@robobun

@robobun robobun commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

What

The node:repl stub's default export was shaped like a REPLServer instance with context: globalThis. This is the one place the stub silently lied instead of throwing: libraries that feature-detect a REPL by probing repl.context and writing helpers into it (the standard REPL-embedding pattern) would silently pollute the real global object.

Repro

const repl = require("node:repl");
"context" in repl;              // bun: true    node: false
repl.context === globalThis;    // bun: true    node: false (undefined at module level)
repl.context.__polluted = 42;   // bun: succeeds silently
globalThis.__polluted;          // bun: 42      node: TypeError (cannot set properties of undefined)

In Node, context is a REPLServer instance property (and defaults to a fresh vm context since useGlobal defaults to false); it does not exist on the module export.

Fix

src/js/node/repl.ts now exports the module-level shape Node actually has instead of a fake instance:

  • dropped the instance-shaped fields: context, terminal, useGlobal, lines, history, historyIndex, cursor, historySize, removeHistoryDuplicates, crlfDelay, completer, _initialPrompt, input, output, line, eval, isCompletionEnabled, escapeCodeTimeout, tabSize, breakEvalOnSigint, underscoreAssigned, last, _domain, allowBlockingCompletions, useColors
  • start() and REPLServer() now throw ERR_NOT_IMPLEMENTED pointing at repl.start not implemented in Bun #28478
  • Recoverable is a minimal SyntaxError subclass matching Node
  • REPL_MODE_SLOPPY / REPL_MODE_STRICT are the Symbol("repl-sloppy") / Symbol("repl-strict") symbols
  • writer wraps util.inspect with inspect.replDefaults
  • _builtinLibs / builtinModules are unchanged

Verification

USE_SYSTEM_BUN=1 bun test test/js/node/stubs.test.js -t "node:repl stub"   # 6/6 fail
bun bd test test/js/node/stubs.test.js -t "node:repl stub"                 # 6/6 pass
bun bd test test/js/node/stubs.test.js                                     # 383/383 pass

Note: #28480 is a full REPLServer/start() implementation; this PR only fixes the stub's shape so it is safe and honest until that lands.


[review] gate passed · iteration 1 · 2 files touched

fails on main (without fix)
ASAN without fix: 7 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/node/stubs.test.js
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (9ba941089)

test/js/node/stubs.test.js:
(pass) stubbed CJS import.meta.require _http_agent [133.09ms]
(pass) stubbed CJS require _http_agent [1.26ms]
(pass) stubbed import _http_agent [6.32ms]
(pass) stubbed CJS import.meta.require _http_client [308.60ms]
(pass) stubbed CJS require _http_client [0.37ms]
(pass) stubbed import _http_client [1.89ms]
(pass) stubbed CJS import.meta.require _http_common [0.30ms]
(pass) stubbed CJS require _http_common [0.25ms]
(pass) stubbed import _http_common [3.86ms]
(pass) stubbed CJS import.meta.require _http_incoming [0.31ms]
(pass) stubbed CJS require _http_incoming [0.24ms]
(pass) stubbed import _http_incoming [1.73ms]
(pass) stubbed CJS import.meta.require _http_outgoing [0.28ms]
(pass) stubbed CJS requ
... (truncated)

release without fix: all passed
bun test v1.4.0-canary.1 (9ba941089)

test/js/node/stubs.test.js:
(pass) stubbed CJS import.meta.require _http_agent [1.82ms]
(pass) stubbed CJS require _http_agent [0.02ms]
(pass) stubbed import _http_agent [0.12ms]
(pass) stubbed CJS import.meta.require _http_client [4.64ms]
(pass) stubbed CJS require _http_client
(pass) stubbed import _http_client [0.03ms]
(pass) stubbed CJS import.meta.require _http_common
(pass) stubbed CJS require _http_common
(pass) stubbed import _http_common [0.02ms]
(pass) stubbed CJS import.meta.require _http_incoming
(pass) stubbed CJS require _http_incoming
(pass) stubbed import _http_incoming [0.01ms]
(pass) stubbed CJS import.meta.require _http_outgoing
(pass) stubbed CJS require _http_outgoing
(pass) stubbed import _http_outgoing [0.02ms]
(pass) stubbed CJS import.meta.require _http_server [1.58ms]
(pass) stubbed CJS require _http_server
(pass) stubbed import _http_server [0.02ms]
(pass) stubbed CJS import.meta.require _stream_duplex [0.02ms]
(pass) stubbed CJS require _stream_duplex
(pass) stubbed import _stream_duplex [0.01ms]
(pass) stubbed CJS import.meta.require _stream_passthrough [0.02ms]
(pass) stubbed CJS require _stream_pas
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/node/stubs.test.js
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (9ba941089)

test/js/node/stubs.test.js:
(pass) stubbed CJS import.meta.require _http_agent [130.21ms]
(pass) stubbed CJS require _http_agent [1.23ms]
(pass) stubbed import _http_agent [6.31ms]
(pass) stubbed CJS import.meta.require _http_client [301.33ms]
(pass) stubbed CJS require _http_client [0.36ms]
(pass) stubbed import _http_client [2.04ms]
(pass) stubbed CJS import.meta.require _http_common [0.30ms]
(pass) stubbed CJS require _http_common [0.26ms]
(pass) stubbed import _http_common [3.52ms]
(pass) stubbed CJS import.meta.require _http_incoming [0.30ms]
(pass) stubbed CJS require _http_incoming [0.26ms]
(pass) stubbed import _http_incoming [1.69ms]
(pass) stubbed CJS import.meta.require _http_outgoing [0.31ms]
(pass) stubbed CJS requ
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
[configured] bun-profile → bun (stripped) in 721ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/22] gen generated_host_exports.rs
generated_host_exports.rs: 91 exports (host=3, lazy=10, generic=78, rust=0); 243 extern-C blocks audited
[2/22] gen JS modules (bundle-modules)
Preprocess modules (6629ms)
Bundle modules (32ms)
Postprocesss modules (19ms)
Bundle Functions (691ms)
Generate Code (68ms)

[7.45s] Bundled "src/js" for production
  1911 kb
  162 internal modules
  12 native modules
  90 internal functions across 19 files
[2/7] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: component rust-std is up to date

  nightly
... (truncated)
diff hotspot
src/js/node/repl.ts        | 89 ++++++++++++++++++----------------------------
 test/js/node/stubs.test.js | 84 +++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 118 insertions(+), 55 deletions(-)

gate history · 2 passed · 0 rejected · iteration 1

evidence per changed file
file                        reads  edits  tests
src/js/node/repl.ts             1      1      0
test/js/node/stubs.test.js      3      8      0

root cause · written by the author bot

The node:repl stub exported an object shaped like a REPLServer instance, including context: globalThis and other instance fields, rather than the module-level surface Node actually returns from require('node:repl'). Libraries that feature-detect a REPL by writing helpers to repl.context would therefore silently mutate the real global object. The fix replaces the stub with Node's true module-level exports (start, REPLServer, Recoverable, mode constants, writer, builtinModules) so context is undefined and such writes fail instead of polluting globalThis.

The stub's default export was shaped like a REPLServer instance with
context set to globalThis. Libraries that feature-detect a REPL by
probing repl.context and writing helpers into it (the standard
embedding pattern) would silently pollute the real global object,
with no error anywhere. Node's require('node:repl') has no context
property at the module level; that belongs to a REPLServer instance
whose default is a fresh vm context.

Drop the instance-shaped fields and export the module-level shape
Node actually has: start and REPLServer throw ERR_NOT_IMPLEMENTED,
Recoverable extends SyntaxError, REPL_MODE_SLOPPY and REPL_MODE_STRICT
are symbols, writer wraps util.inspect, and builtinModules plus
_builtinLibs remain.
@robobun

robobun commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 7:50 PM PT - Jul 15th, 2026

@robobun, your commit 9ba9410 has 2 failures in Build #73517 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 34286

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

bun-34286 --bun

@github-actions

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. Bun breaks when using ts-node API ReplService.start #31470 - ts-node's ReplService.start fails with TypeError: (0, repl_1.start) is not a function because the old stub didn't export start() as a callable function; this PR adds it as a proper module-level export

If this is helpful, copy the block below into the PR description to auto-close this issue on merge.

Fixes #31470

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

The node:repl stub now exposes module-level exports, REPL mode symbols, startup and server stubs, Recoverable, and writer(). Tests verify the export shape, isolation, error codes, writer behavior, and error wrapping.

Node REPL stub

Layer / File(s) Summary
Module-level REPL exports
src/js/node/repl.ts
Replaces the instance-like placeholder with module-level REPL exports, symbols, stubs, Recoverable, and an inspect-based writer.
REPL export and isolation tests
test/js/node/stubs.test.js
Validates the export shape, prevents global pollution through context, checks ERR_NOT_IMPLEMENTED, verifies writer() behavior, and confirms Recoverable preserves its wrapped error.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly states the main change: fixing the node:repl stub to stop exposing globalThis as repl.context.
Description check ✅ Passed The description covers the change, repro, fix, and verification, so it satisfies the template despite different headings.

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

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/js/node/stubs.test.js`:
- Around line 180-189: Extend the “exposes Node’s module-level exports” test to
invoke repl.writer with a representative value and compare the result against
inspect(value, repl.writer.options). Also verify that repl.writer.options
initially matches inspect.replDefaults, while preserving the existing type and
export assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 10785747-6966-4e81-a2b9-ab00cf9040c9

📥 Commits

Reviewing files that changed from the base of the PR and between 871c5ee and 0e14c29.

📒 Files selected for processing (2)
  • src/js/node/repl.ts
  • test/js/node/stubs.test.js

Comment thread test/js/node/stubs.test.js
@robobun

robobun commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator Author

Re: the issue-linker suggestion for #31470: this PR does not fix that. repl.start is now a function (so the TypeError goes away), but it throws ERR_NOT_IMPLEMENTED pointing at #28478. ts-node's ReplService.start will now get a clear not-implemented error instead of (0, repl_1.start) is not a function, which is better diagnostics but still not a working REPL. A functional start() is tracked in #28478 / #28480.

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. Implement node:repl start(), REPLServer, and Recoverable #28480 - Also implements start(), REPLServer, Recoverable, REPL_MODE_SLOPPY/STRICT, and writer on the node:repl module stub in src/js/node/repl.ts, targeting the same issue repl.start not implemented in Bun #28478
  2. node:repl: replace the stub with Node v26.3.0's REPL — v26 readline stack, acorn recoverable-parse + top-level await, completion, history, --interactive (82 vendored upstream tests) #31827 - Replaces the node:repl stub wholesale with a full Node v26.3.0 port, which is a superset of all changes in this PR

🤖 Generated with Claude Code

@robobun

robobun commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator Author

Re: the duplicate detector: #28480 (+534 lines) and #31827 (+13226 lines) are full node:repl implementations that have been open for a while. This PR is a targeted 35-line safety fix for one hazard: the stub exporting context: globalThis, which lets any library that writes through repl.context silently pollute the real global. Either of those PRs would supersede this when merged (they replace the file wholesale), but this makes the stub safe in the meantime and is a trivial rebase for them.

@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.

I didn't find any bugs. The stub reshape looks correct and the context: globalThis removal fixes a real hazard, but this changes the user-visible export shape of require("node:repl") while two full-implementation PRs (#28480, #31827) are open for the same file — a maintainer should make the call on landing the interim fix vs. waiting.

Checked: new export shape matches Node's module-level repl (start/REPLServer/Recoverable/writer/mode symbols); inspect.replDefaults exists in Bun's util.inspect; throwNotImplemented(feature, issue) signature is correct.
Checked: no in-tree consumers of the removed instance fields — src/runtime/cli/repl.rs hits are an unrelated Rust struct; test-repl-preview-without-inspector.js already fails on REPLServer either way.
Ruled out: eager top-level require("node:util") — only runs when node:repl is imported, and the module is a stub.

Extended reasoning...

Overview

Reshapes the node:repl stub in src/js/node/repl.ts from a fake REPLServer-instance-shaped object (with context: globalThis, terminal, useGlobal, Proxy-trapped input/output, etc.) to Node's actual module-level export shape: start()/REPLServer() that throw ERR_NOT_IMPLEMENTED pointing at #28478, a minimal Recoverable extends SyntaxError, REPL_MODE_SLOPPY/REPL_MODE_STRICT symbols, and writer() wrapping util.inspect with inspect.replDefaults. _builtinLibs/builtinModules are unchanged. Adds a 7-test describe block to test/js/node/stubs.test.js.

Security risks

None introduced. The change actually removes a hazard: the old context: globalThis let any library that feature-detects a REPL and writes helpers into repl.context silently pollute the real global object. Now that write throws (since repl.context is undefined), matching Node.

Level of scrutiny

Moderate. It's a ~35-line net change to an explicitly-labeled stub module, but it is a user-facing Node-compat surface change: packages that were (incorrectly) probing removed fields like repl.terminal or repl.useGlobal will now see undefined instead of true. That's more Node-compatible, but it's still a behavior change. I verified inspect.replDefaults exists (defined via getter in src/js/internal/util/inspect.js:767), throwNotImplemented accepts (feature, issue), and grepped for in-tree consumers of the removed fields — the only hits are the Rust CLI REPL's own repl.history struct field (unrelated) and a Node parallel test that already fails on REPLServer before and after.

Other factors

Two open PRs (#28480 at +534 lines, #31827 at +13226 lines) fully implement node:repl and would replace this file wholesale. The author's position — land this as a targeted safety fix now, trivial rebase for the full impls — is reasonable, but whether to take an interim stub change vs. push one of the full implementations across the line is a maintainer/product call I shouldn't make. Tests are solid (subprocess isolation for the pollution check with bunEnv and expect.any(String) for stderr, concurrent pipe draining, ERR_NOT_IMPLEMENTED code assertions), and the CodeRabbit nit about writer() behavior was addressed in 97d95b9.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
test/js/node/stubs.test.js (1)

124-143: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Strengthen the module-shape assertions.

The test only checks property absence for context; other instance fields could be reintroduced with undefined values and still pass. Likewise, any arrays or symbols satisfy the export test. Assert every instance field is absent, compare both builtin lists against the expected values, and verify the two REPL mode symbols are distinct.

Also applies to: 182-191

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/js/node/stubs.test.js` around lines 124 - 143, Strengthen the
module-shape assertions in the REPL stub tests: verify each REPLServer instance
field is absent from repl, not merely undefined, and tighten the export checks
to compare both builtin lists with their expected values. Also assert that the
two REPL mode symbols are distinct, updating the related assertions near the
second referenced section.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/js/node/stubs.test.js`:
- Line 194: Move the node:util inspect import from its current location into the
module-scope import block in stubs.test.js, keeping the existing inspect usage
unchanged and avoiding a runtime require() in the test body.

---

Outside diff comments:
In `@test/js/node/stubs.test.js`:
- Around line 124-143: Strengthen the module-shape assertions in the REPL stub
tests: verify each REPLServer instance field is absent from repl, not merely
undefined, and tighten the export checks to compare both builtin lists with
their expected values. Also assert that the two REPL mode symbols are distinct,
updating the related assertions near the second referenced section.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 73d1641e-fbc1-499d-8891-2183887446b2

📥 Commits

Reviewing files that changed from the base of the PR and between 0e14c29 and f42d5bd.

📒 Files selected for processing (1)
  • test/js/node/stubs.test.js

Comment thread test/js/node/stubs.test.js Outdated

@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 — targeted safety fix that reshapes the node:repl stub to match Node's actual module-level export surface and removes the context: globalThis alias that could silently pollute the real global.

What was reviewed:

  • Verified throwNotImplemented(feature, issue) signature in internal/shared.ts matches the new call sites.
  • Confirmed inspect.replDefaults exists as a getter in internal/util/inspect.js, so { ...inspect.replDefaults } is well-defined.
  • Grepped for in-tree consumers of the removed instance-shaped fields (repl.context, .input, .output, etc.) — none outside the tests being updated.
  • Subprocess test follows repo conventions: bunEnv, concurrent pipe drain, stderr: expect.any(String), combined-object assertion; JSON.stringify dropping polluted: undefined is fine under .toEqual semantics.
Extended reasoning...

Overview

Two files touched: src/js/node/repl.ts (the stub itself, ~55 lines removed / ~35 added) and test/js/node/stubs.test.js (+87 lines of coverage). The stub previously exported an object shaped like a REPLServer instance — including context: globalThis — which is not what Node's require('node:repl') returns. The PR replaces it with the real module-level surface: start, REPLServer, Recoverable, REPL_MODE_SLOPPY/STRICT, writer, plus the pre-existing _builtinLibs/builtinModules. start() and REPLServer() throw ERR_NOT_IMPLEMENTED referencing issue 28478.

Security risks

The change is a net security/robustness improvement: it removes the one path by which importing node:repl and following the standard repl.context.helper = ... pattern would mutate globalThis in Bun. No new attack surface is introduced — writer is a thin wrapper over util.inspect, and everything else either throws or is a plain constant. No auth/crypto/permissions code involved.

Level of scrutiny

Low-to-moderate. This is a stub module explicitly marked "None of this is actually implemented yet"; the change makes its shape more honest (matches Node) and safer (no silent global aliasing). The only behavioral question is whether anything in-tree depended on the removed instance fields — a repo-wide grep for repl.context/repl.input/repl.output/repl.useGlobal/repl.terminal found only the new test and an upstream Node parallel test that operates on an actual REPLServer instance, not the module export. No packages/bun-types declarations describe these fields either.

Other factors

  • throwNotImplemented already accepts (feature, issue?), so passing 28478 is correct.
  • inspect.replDefaults is a real getter on Bun's util.inspect returning an options object, so the spread and the toEqual assertion are sound.
  • Tests follow CLAUDE.md conventions: bunEnv, await using on the spawn, concurrent Promise.all drain of stdout/stderr/exited, loose stderr match via expect.any(String), combined-object assertion. The PR description shows the suite fails on system Bun and passes on the debug build.
  • All CodeRabbit review threads are resolved; the author already clarified the relationship to #28480/#31827 (full implementations that would supersede this) and #31470 (not fully fixed here). No outstanding human reviewer comments.

@robobun

robobun commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator Author

CI status: the diff itself is green. test/js/node/stubs.test.js passes on every lane in builds 73503 and 73517. The remaining red is unrelated to this change:

  • test-worker-message-port-transfer-terminate.js (pre-existing on main, JSC assertion)
  • 30205.test.ts (napi finalizer LeakSanitizer on debian x64-asan; flaky, already reported)
  • complex-workspace.test.ts, webview-chrome.test.ts, net-mongodb-pattern-leak.test.ts, es-module-lexer.test.ts, test-fs-promises-file-handle-readFile.js (all flaky, pass on retry)

None of these import node:repl or touch stubs.test.js. Ready for review.

@robobun

robobun commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator Author

The node:repl stub this PR reshapes was replaced by the full implementation in #31827 (which closed #28478), so src/js/node/repl.ts no longer exists. On main, "context" is not a property of the node:repl module and assigning through repl.context throws, matching Node, so the bug this PR fixed is gone. Closing.

@robobun robobun closed this Aug 12, 2026
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