Skip to content
Merged
Show file tree
Hide file tree
Changes from 89 commits
Commits
Show all changes
90 commits
Select commit Hold shift + click to select a range
ec274c8
node:repl: port Node v26.3.0 REPL + readline stack, vendor node repl …
cirospaciari Jun 4, 2026
f78894e
node:repl: error-name decoration, REPL frame trimming, builtin libs, …
cirospaciari Jun 4, 2026
cd02f77
node:repl: keep decorated ERR_* names (matches more vendored tests)
cirospaciari Jun 4, 2026
eae1477
[autofix.ci] apply automated fixes
autofix-ci[bot] Jun 4, 2026
917df0e
lint: ignore verbatim Node ports in oxlint, fix shim unused-vars
cirospaciari Jun 4, 2026
f42fc1c
address review: frames guard in prepareStackTrace fallback, Rust doc …
cirospaciari Jun 4, 2026
2ca63e1
vendor acorn minified to keep the embedded-source literal within comp…
cirospaciari Jun 4, 2026
2d6029b
[autofix.ci] apply automated fixes
autofix-ci[bot] Jun 4, 2026
8889bdf
address review: import referrer, capture decline path, scheme-only names
cirospaciari Jun 4, 2026
5ea8b59
build: raise -fconstexpr-steps for the larger embedded builtin sources
cirospaciari Jun 4, 2026
4022f6e
address review round 2: readline internals export, --interactive sema…
cirospaciari Jun 4, 2026
12d9ff1
[autofix.ci] apply automated fixes
autofix-ci[bot] Jun 4, 2026
ecdb2ed
docs: node-repl.ts header no longer claims -i (taken by --install=fal…
cirospaciari Jun 4, 2026
67f2809
repl: tolerate a pre-existing uncaughtException capture callback
cirospaciari Jun 4, 2026
8db1820
test: re-sync util-inspect null-prototype error assertions toward ups…
cirospaciari Jun 5, 2026
03225e5
make the Bun-only symbol hooks non-enumerable
cirospaciari Jun 5, 2026
460b0a3
[autofix.ci] apply automated fixes
autofix-ci[bot] Jun 5, 2026
d662e21
repl: keep the eval property name out of member-access position in bu…
cirospaciari Jun 5, 2026
2702f64
module: check exceptions in the resolveLookupPaths parent-paths loop
cirospaciari Jun 5, 2026
5d550c2
test: exclude test-repl-inspect-defaults from exception-check validation
cirospaciari Jun 5, 2026
063469f
fix promisify(question).name after bundler dedupe-rename; gate 5 more…
cirospaciari Jun 5, 2026
cdaf2a9
address review: inert overrideStackTrace, hardened primordials shim, …
cirospaciari Jun 5, 2026
ddfdeba
[autofix.ci] apply automated fixes
autofix-ci[bot] Jun 5, 2026
6ab687f
test: exclude test-repl-dynamic-import from LSAN on the asan lane
cirospaciari Jun 5, 2026
19d8c87
vm: finalize the heap on exit even when tick-scoped VM refs remain
cirospaciari Jun 5, 2026
3c869c3
prettier: ignore the verbatim Node.js ports
cirospaciari Jun 5, 2026
3454375
Merge branch 'main' into ciro/repl-node-tests
robobun Jun 10, 2026
5fbfc32
Merge branch 'main' into ciro/repl-node-tests [build images]
cirospaciari Jun 18, 2026
0235b36
Merge branch 'main' into ciro/repl-node-tests [build images]
cirospaciari Jun 22, 2026
2fab595
repl: filter Bun-specific builtins from completion; vendor only the p…
cirospaciari Jun 24, 2026
7da5859
Merge branch 'main' into ciro/repl-node-tests [build images]
cirospaciari Jun 24, 2026
955d627
[autofix.ci] apply automated fixes
autofix-ci[bot] Jun 24, 2026
86cc7bb
repl: don't throw when decorateErrorStack can't write a frozen .stack…
cirospaciari Jun 24, 2026
bd536eb
repl: guard _handleError's e.stack/e.message rewrites for frozen erro…
cirospaciari Jun 24, 2026
e69f1b4
cli: --interactive -e runs the eval then enters the REPL [build images]
cirospaciari Jun 24, 2026
a8289b0
cli: scope-isolate the --interactive -e user script from the REPL boo…
cirospaciari Jun 24, 2026
cb7a4e7
cli: document the static-import trade-off of the --interactive -e blo…
cirospaciari Jun 24, 2026
15c4ccd
Merge branch 'main' into ciro/repl-node-tests [build images]
cirospaciari Jun 25, 2026
6bf0206
repl: trim review-round comments to the new 3-line limit [build images]
cirospaciari Jun 25, 2026
066e6dc
test(napi): split on /\r?\n/ so the napi_is_arraybuffer assertion pas…
cirospaciari Jun 26, 2026
a090188
Merge branch 'main' into ciro/repl-node-tests [build images]
cirospaciari Jun 26, 2026
0455a5c
Merge branch 'main' into ciro/repl-node-tests
cirospaciari Jun 26, 2026
0a13938
repl: don't let process.cwd() ENOENT break repl.start() from a delete…
cirospaciari Jun 27, 2026
f25d419
repl: address review — bootstrap rewrite, lazy acorn, ErrorCode regis…
alii Jul 8, 2026
71c7172
repl: make --interactive -e errors fatal; decorate vm.Script parse er…
alii Jul 8, 2026
5fe7881
repl: append new ErrorCode entries (fixes Rust index drift); drop dea…
alii Jul 8, 2026
09015d9
Merge branch 'main' into ciro/repl-node-tests
cirospaciari Jul 8, 2026
0c9e692
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 8, 2026
61f3396
repl: address second-pass review — process._eval, --interactive routi…
alii Jul 9, 2026
9412fa9
repl: decode process._eval as UTF-8; drop dispatch positionals in exe…
alii Jul 10, 2026
b5b5003
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 10, 2026
a92cab1
Merge remote-tracking branch 'origin/main' into ciro/repl-node-tests
cirospaciari Jul 14, 2026
7e11a12
repl: vendor only the upstream tests that pass; drop the expectations…
cirospaciari Jul 14, 2026
60e2a2a
repl: cover the process._eval UTF-8 decode and the argv 'run' leak
cirospaciari Jul 14, 2026
5118400
run-eval: cover the process._eval UTF-8 decode for plain -e/--print
cirospaciari Jul 14, 2026
bfc54fe
Merge origin/main into ciro/repl-node-tests
cirospaciari Jul 14, 2026
00df8dc
Merge branch 'main' into ciro/repl-node-tests
cirospaciari Jul 15, 2026
c68ad8f
vm: keep the SyntaxError when a user prepareStackTrace throws
cirospaciari Jul 15, 2026
d7d7177
Merge branch 'main' into ciro/repl-node-tests
cirospaciari Jul 16, 2026
1ac506d
Merge branch 'main' into ciro/repl-node-tests
dylan-conway Jul 17, 2026
7bf7a3e
node:vm: decorate compileFunction parse errors like new Script
cirospaciari Jul 17, 2026
f9490f3
node:readline: name Interface's Symbol.dispose method as a string
cirospaciari Jul 17, 2026
ebe8705
cli: make `-i` mean --interactive under node emulation
cirospaciari Jul 17, 2026
15aab6a
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 17, 2026
b7aaf14
node:repl: document why `-e` runs before the REPL starts
cirospaciari Jul 17, 2026
f7b10e3
node:repl: evaluate `-e` after the REPL starts, with node's bindings
cirospaciari Jul 17, 2026
817a588
node:repl: vendor three upstream tests that already pass
cirospaciari Jul 17, 2026
a2c033b
node:repl: vendor the .load multiline tests and their fixtures
cirospaciari Jul 17, 2026
0858117
node:repl: vendor the nested-repls completion test and its fixture
cirospaciari Jul 17, 2026
6488283
node:repl: vendor sigint/autocomplete tests; stop spreading in ALS.run
cirospaciari Jul 17, 2026
e691d59
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 17, 2026
64ccb74
Merge branch 'main' into ciro/repl-node-tests
robobun Jul 17, 2026
0e0d9e4
node:repl: signed lineOffset in arrow headers; ALS run short-circuit …
robobun Jul 18, 2026
b7588cd
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 18, 2026
527b2b2
node:repl: neutralize upstream author notes so diff-hygiene passes; c…
robobun Jul 18, 2026
8cc2f12
acorn: reword the \uXXXX placeholder in the vendored bundle's error m…
robobun Jul 18, 2026
fcafdeb
ci: retrigger
robobun Jul 18, 2026
ed8df5c
docs+tests: mark the three vendored-test deviations with bun:; fix th…
robobun Jul 18, 2026
b05ef0a
Merge branch 'main' into ciro/repl-node-tests
robobun Jul 19, 2026
39b90ae
readline: correct the ReplHistory laziness comment
robobun Jul 19, 2026
e0c3731
Merge branch 'main' into ciro/repl-node-tests
robobun Jul 23, 2026
6398622
node:repl,readline: defer loading so bare require stays near-free
robobun Jul 23, 2026
c089c02
cli(node-emulation): bare `node` on a TTY enters the REPL
robobun Jul 23, 2026
97d8107
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 23, 2026
1ed9551
test(as-node): pin piped stdin for the bare-node case
robobun Jul 23, 2026
dd1a095
test(vm): hoist the 5 Script-construction tests out of the 6x helper
robobun Jul 23, 2026
31b294c
cli(node-emulation): use Output::is_stdin_tty() for the bare-node REP…
robobun Jul 23, 2026
ede4f5c
node:repl: hollow out the public surface so only calling start() load…
robobun Jul 23, 2026
a4af413
repl(node-shims): route decorateErrorStack's frame rewrite through th…
robobun Jul 23, 2026
0a2d409
Merge branch 'main' into ciro/repl-node-tests
dylan-conway Jul 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,17 @@ docs/bundler/minifier.mdx
# `_`/`*` differently every pass), so autofix.ci ping-pongs commits forever.
# Not authored by hand; no value in normalizing it.
docs/.rust-rewrite-verified-claims.md

# Verbatim ports from Node.js v26.3.0 (lib/) — keep close to upstream
# (mirrors the oxlint.json ignore block for the same files)
src/js/node/repl.js
src/js/node/readline.js
src/js/node/readline.promises.js
src/js/internal/repl.js
src/js/internal/readline
src/js/internal/repl/history.js
src/js/internal/repl/utils.js
src/js/internal/repl/completion.js
src/js/internal/repl/await.js
src/js/internal/repl/acorn.js
src/js/internal/repl/acorn-walk.js
2 changes: 2 additions & 0 deletions LICENSE.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ For compatibility reasons, the following packages are embedded into Bun's binary

| Package | License |
|---------|---------|
| [`acorn`](https://github.com/acornjs/acorn) | MIT |
| [`acorn-walk`](https://github.com/acornjs/acorn) | MIT |
| [`assert`](https://npmjs.com/package/assert) | MIT |
| [`browserify-zlib`](https://npmjs.com/package/browserify-zlib) | MIT |
| [`buffer`](https://npmjs.com/package/buffer) | MIT |
Expand Down
2 changes: 2 additions & 0 deletions docs/project/license.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ For compatibility, Bun embeds the following packages into its binary and injects

| Package | License |
| ------------------------------------------------------------------------ | ------- |
| [`acorn`](https://github.com/acornjs/acorn) | MIT |
| [`acorn-walk`](https://github.com/acornjs/acorn) | MIT |
| [`assert`](https://npmjs.com/package/assert) | MIT |
| [`browserify-zlib`](https://npmjs.com/package/browserify-zlib) | MIT |
| [`buffer`](https://npmjs.com/package/buffer) | MIT |
Expand Down
2 changes: 1 addition & 1 deletion docs/runtime/nodejs-compat.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ This page is updated regularly and reflects the latest version of Bun's compatib

### [`node:repl`](https://nodejs.org/api/repl.html)

🔴 Not implemented.
🟡 Mostly implemented. `bun --interactive` starts a Node.js-compatible REPL. Result previews (which need V8's inspector-based side-effect-free eval), tab-completion of `let`/`const`/`class` bindings in `useGlobal: true` mode, and some V8-specific error-message wording differ.

### [`node:sqlite`](https://nodejs.org/api/sqlite.html)

Expand Down
7 changes: 7 additions & 0 deletions docs/snippets/cli/run.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ bun run <file or script>
Control the shell used for <code>package.json</code> scripts. Supports either <code>bun</code> or <code>system</code>
</ParamField>

<ParamField path="--interactive" type="boolean">
Open the Node.js-compatible REPL (<code>node:repl</code>). When combined with <code>-e</code>, starts the REPL and
then evaluates the script. Under <code>--interactive</code>, <code>-e</code> is raw JavaScript (matching{" "}
<code>node -i -e</code>); use <code>bun repl</code> for TypeScript. Distinct from <code>bun repl</code>, which is
Bun's native REPL.
</ParamField>

<ParamField path="--smol" type="boolean">
Use less memory, but run garbage collection more often
</ParamField>
Expand Down
13 changes: 13 additions & 0 deletions oxlint.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,19 @@
"bench/react-hello-world/*.js",
"bun.lock",

// Verbatim ports from Node.js v26.3.0 (lib/) — keep close to upstream
"src/js/node/repl.js",
"src/js/node/readline.js",
"src/js/node/readline.promises.js",
"src/js/internal/repl.js",
"src/js/internal/readline/**",
"src/js/internal/repl/history.js",
"src/js/internal/repl/utils.js",
"src/js/internal/repl/completion.js",
"src/js/internal/repl/await.js",
"src/js/internal/repl/acorn.js",
"src/js/internal/repl/acorn-walk.js",
Comment thread
cirospaciari marked this conversation as resolved.

"test/js/node/**/parallel/**",
"test/js/node/test/fixtures", // full of JS with intentional syntax errors
"test/snippets/**",
Expand Down
2 changes: 1 addition & 1 deletion scripts/build/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -633,7 +633,7 @@ export const bunOnlyFlags: Flag[] = [
flag: ["-fconstexpr-steps=6000000", "-fconstexpr-depth=54"],
when: c => c.unix,
lang: "cxx",
desc: "Raise constexpr limits (JSC uses heavy constexpr; the embedded module registry literals are large)",
desc: "Raise constexpr limits (JSC uses heavy constexpr; under ASSERT_ENABLED, ASCIILiteral::fromLiteralUnsafe constexpr-validates the largest embedded builtin source in InternalModuleRegistryConstants.h char by char)",
},
{
flag: ["-fno-pic", "-fno-pie"],
Expand Down
9 changes: 9 additions & 0 deletions src/codegen/bundle-modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,15 @@ for (let i = 0; i < nativeStartIndex; i++) {
true,
x => requireTransformer(x, moduleList[i]),
);
// Guard rail: builtin-parser.ts's regex-position heuristic only recognises
// `/` as regex-start after `[(,=;:{]|return|=>`; a regex whose body has `)`
// or `}` in any other position silently truncates. Fail loudly here.
if (processed.rest.trim() !== "") {
throw new Error(
`sliceSourceCode truncated ${moduleList[i]} — likely a regex literal in a position builtin-parser.ts doesn't recognise. ` +
`Leftover starts: ${processed.rest.slice(0, 80)}`,
);
}
let fileToTranspile = `// GENERATED TEMP FILE - DO NOT EDIT
// Sourced from src/js/${moduleList[i]}
${importStatements.join("\n")}
Expand Down
90 changes: 90 additions & 0 deletions src/js/eval/node-repl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Entry script for `bun --interactive` (`-i` is taken by `--install=fallback`): starts the Node.js-compatible
// REPL (the ported node:repl) the way Node's internal/main/repl.js does. This
// file runs as a regular entrypoint (not a builtin), so it reaches
// createInternalRepl via a Symbol.for hook on node:repl and never re-implements
// the NODE_REPL_* env parsing that internal/repl.js already owns.

// exec_node_repl stashes the user's `-e` bytes on `process._eval` (undefined
// when no `-e`), so no source splicing — a syntax error or unterminated
// token in `-e` cannot bleed into this bootstrap.
const evalScript: string | undefined = (process as { _eval?: string })._eval;

const ext = process.env.NODE_REPL_EXTERNAL_MODULE;
if (ext) {
// Node loads this in place of the built-in REPL (lib/internal/main/repl.js).
require(require("node:path").resolve(ext));
Comment thread
cirospaciari marked this conversation as resolved.
} else {
const REPL = require("node:repl");
const createInternalRepl = (REPL as Record<symbol, Function>)[Symbol.for("bun.repl.createInternalRepl")];

console.log(
`Welcome to Bun v${(globalThis as any).Bun.version} (Node.js-compatible REPL, node:repl ${process.version}).\n` +
'Type ".help" for more information.',
);

createInternalRepl(process.env, (err: Error | null, replServer: any) => {
if (err) throw err;

replServer.on("exit", () => {
if (replServer.historyManager?.isFlushing) {
replServer.once("flushHistory", () => process.exit());
return;
}
process.exit();
});
});

// `node -i -e`: node evaluates AFTER createInternalRepl (which starts the
// REPL synchronously), so the REPL's globals are already in place.
if (evalScript !== undefined) {
evalWithNodeBindings(evalScript);
}
}

// Mirrors node's runScriptInContext: it does NOT wrap the body as a CJS
// module — it publishes the bindings onto the global and runs the body in
// global scope, so `var`/`function` still land on globalThis while
// require/module/exports/__dirname/__filename resolve.
function evalWithNodeBindings(code: string) {
const Module = require("node:module");
// process.cwd() throws when the working directory has been deleted; node's
// evalScript uses tryGetCwd() here, falling back to the executable's dir.
let cwd: string;
try {
cwd = process.cwd();
} catch {
cwd = require("node:path").dirname(process.execPath);
}
const name = "[eval]";

const mod = new Module(name);
mod.filename = require("node:path").join(cwd, name);
mod.paths = Module._nodeModulePaths(cwd);

const global_ = globalThis as any;
const origModule = global_.module;
global_.module = mod;
global_.exports = mod.exports;
// node's wrapper is compiled as `${name}-wrapper`, so its __dirname is
// dirname("[eval]-wrapper") === "." — decoupled from module.filename, which
// stays the cwd-joined path used for require resolution.
global_.__dirname = ".";
global_.__filename = name;
global_.require = Module.createRequire(mod.filename);

try {
require("node:vm").runInThisContext(code, { filename: name, displayErrors: true });
} catch (e) {
// An -e error is fatal in node even with the REPL up. Report and exit here
// rather than rethrowing: the REPL is already live, so an uncaught throw
// races its EOF-driven exit and the process can leave 0 with the error
// unreported (empty stdin loses that race every time).
try {
process.setUncaughtExceptionCaptureCallback(null);
} catch {}
console.error(e);
process.exit(1);
} finally {
if (origModule !== undefined) global_.module = origModule;
}
}
122 changes: 122 additions & 0 deletions src/js/internal/readline/callbacks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Ported from Node.js v26.3.0 lib/internal/readline/callbacks.js for Bun's node:repl.
// Attribution: derived from Node.js, MIT licensed (Node.js contributors).
// prettier-ignore
const primordials = require("internal/repl/node-primordials");
var __node_module__ = { exports: {} };

const { NumberIsNaN } = primordials;

const {
codes: { ERR_INVALID_ARG_VALUE, ERR_INVALID_CURSOR_POS },
} = require("internal/repl/node-errors");

const { validateFunction } = require("internal/validators");
const { CSI } = require("internal/readline/utils");

const { kClearLine, kClearScreenDown, kClearToLineBeginning, kClearToLineEnd } = CSI;

/**
* moves the cursor to the x and y coordinate on the given stream
*/

function cursorTo(stream, x, y, callback) {
if (callback !== undefined) {
validateFunction(callback, "callback");
}

if (typeof y === "function") {
callback = y;
y = undefined;
}

if (NumberIsNaN(x)) throw new ERR_INVALID_ARG_VALUE("x", x);
if (NumberIsNaN(y)) throw new ERR_INVALID_ARG_VALUE("y", y);

if (stream == null || (typeof x !== "number" && typeof y !== "number")) {
if (typeof callback === "function") process.nextTick(callback, null);
return true;
}

if (typeof x !== "number") throw new ERR_INVALID_CURSOR_POS();

const data = typeof y !== "number" ? CSI`${x + 1}G` : CSI`${y + 1};${x + 1}H`;
return stream.write(data, callback);
}

/**
* moves the cursor relative to its current location
*/

function moveCursor(stream, dx, dy, callback) {
if (callback !== undefined) {
validateFunction(callback, "callback");
}

if (stream == null || !(dx || dy)) {
if (typeof callback === "function") process.nextTick(callback, null);
return true;
}

let data = "";

if (dx < 0) {
data += CSI`${-dx}D`;
} else if (dx > 0) {
data += CSI`${dx}C`;
}

if (dy < 0) {
data += CSI`${-dy}A`;
} else if (dy > 0) {
data += CSI`${dy}B`;
}

return stream.write(data, callback);
}

/**
* clears the current line the cursor is on:
* -1 for left of the cursor
* +1 for right of the cursor
* 0 for the entire line
*/

function clearLine(stream, dir, callback) {
if (callback !== undefined) {
validateFunction(callback, "callback");
}

if (stream === null || stream === undefined) {
if (typeof callback === "function") process.nextTick(callback, null);
return true;
}

const type = dir < 0 ? kClearToLineBeginning : dir > 0 ? kClearToLineEnd : kClearLine;
return stream.write(type, callback);
}

/**
* clears the screen from the current position of the cursor down
*/

function clearScreenDown(stream, callback) {
if (callback !== undefined) {
validateFunction(callback, "callback");
}

if (stream === null || stream === undefined) {
if (typeof callback === "function") process.nextTick(callback, null);
return true;
}

return stream.write(kClearScreenDown, callback);
}

__node_module__.exports = {
clearLine,
clearScreenDown,
cursorTo,
moveCursor,
};

export default __node_module__.exports;
Loading
Loading