Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
89 changes: 34 additions & 55 deletions src/js/node/repl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// This is a stub! None of this is actually implemented yet.
// It only exists to make some packages which import this module work.
const { throwNotImplemented } = require("internal/shared");
const { inspect } = require("node:util");

const builtinModules = [
"bun",
Expand Down Expand Up @@ -63,62 +64,40 @@ const builtinModules = [
"node:test",
];

const REPL_MODE_SLOPPY = Symbol("repl-sloppy");
const REPL_MODE_STRICT = Symbol("repl-strict");

function start() {
throwNotImplemented("node:repl", 28478);
}

function REPLServer() {
throwNotImplemented("node:repl REPLServer", 28478);
}

class Recoverable extends SyntaxError {
err;
constructor(err) {
super();
this.err = err;
}
}

function writer(obj) {
return inspect(obj, writer.options);
}
writer.options = { ...inspect.replDefaults };

// The module-level exports match Node's `require("node:repl")` shape. Instance
// fields like `context`/`terminal`/`useGlobal` belong to a REPLServer instance,
// not this module, so they are intentionally absent.
export default {
lines: [],
context: globalThis,
historyIndex: -1,
cursor: 0,
historySize: 1000,
removeHistoryDuplicates: false,
crlfDelay: 100,
completer: () => {
throwNotImplemented("node:repl");
},
history: [],
_initialPrompt: "> ",
terminal: true,
input: new Proxy(
{},
{
get() {
throwNotImplemented("node:repl");
},
has: () => false,
ownKeys: () => [],
getOwnPropertyDescriptor: () => undefined,
set() {
throwNotImplemented("node:repl");
},
},
),
line: "",
eval: () => {
throwNotImplemented("node:repl");
},
isCompletionEnabled: true,
escapeCodeTimeout: 500,
tabSize: 8,
breakEvalOnSigint: true,
useGlobal: true,
underscoreAssigned: false,
last: undefined,
_domain: undefined,
allowBlockingCompletions: false,
useColors: true,
output: new Proxy(
{},
{
get() {
throwNotImplemented("node:repl");
},
has: () => false,
ownKeys: () => [],
getOwnPropertyDescriptor: () => undefined,
set() {
throwNotImplemented("node:repl");
},
},
),
start,
REPLServer,
Recoverable,
REPL_MODE_SLOPPY,
REPL_MODE_STRICT,
writer,
_builtinLibs: builtinModules,
builtinModules: builtinModules,
};
84 changes: 84 additions & 0 deletions test/js/node/stubs.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";
import repl from "node:repl";
import { inspect } from "node:util";

const weirdInternalSpecifiers = [
"_http_agent",
Expand Down Expand Up @@ -114,6 +117,87 @@ describe("v8.getHeapStatistics", () => {
}
});

describe("node:repl stub", () => {
// The module export used to masquerade as a REPLServer instance with
// `context: globalThis`, so libraries that feature-detect a REPL by probing
// `repl.context` and writing to it would silently pollute the real global.
test("does not expose REPLServer instance fields on the module", () => {
const instanceFields = ["context", "terminal", "useGlobal", "lines", "history", "input", "output", "eval"];
expect(Object.fromEntries(instanceFields.map(k => [k, { in: k in repl, value: repl[k] }]))).toEqual(
Object.fromEntries(instanceFields.map(k => [k, { in: false, value: undefined }])),
);
});

test("writing through repl.context cannot pollute globalThis", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const repl = require("node:repl");
let threw = false;
try {
repl.context.__pollutedByReplStub = 42;
} catch {
threw = true;
}
console.log(JSON.stringify({
threw,
contextIsGlobalThis: repl.context === globalThis,
polluted: globalThis.__pollutedByReplStub,
}));
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect({ stdout: JSON.parse(stdout), stderr, exitCode }).toEqual({
stdout: {
threw: true,
contextIsGlobalThis: false,
polluted: undefined,
},
stderr: expect.any(String),
exitCode: 0,
});
});

test("exposes Node's module-level exports", () => {
expect(typeof repl.start).toBe("function");
expect(typeof repl.REPLServer).toBe("function");
expect(typeof repl.Recoverable).toBe("function");
expect(typeof repl.writer).toBe("function");
expect(typeof repl.REPL_MODE_SLOPPY).toBe("symbol");
expect(typeof repl.REPL_MODE_STRICT).toBe("symbol");
expect(repl.REPL_MODE_SLOPPY).not.toBe(repl.REPL_MODE_STRICT);
expect(Array.isArray(repl._builtinLibs)).toBe(true);
expect(Array.isArray(repl.builtinModules)).toBe(true);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

test("writer() forwards to util.inspect with writer.options", () => {
const value = { a: 1, b: [2, 3], c: { d: 4 } };
expect(repl.writer.options).toEqual(inspect.replDefaults);
expect(repl.writer(value)).toBe(inspect(value, repl.writer.options));
});

test("start() throws ERR_NOT_IMPLEMENTED", () => {
expect(() => repl.start()).toThrow(expect.objectContaining({ code: "ERR_NOT_IMPLEMENTED" }));
});

test("REPLServer() throws ERR_NOT_IMPLEMENTED", () => {
expect(() => new repl.REPLServer()).toThrow(expect.objectContaining({ code: "ERR_NOT_IMPLEMENTED" }));
});

test("Recoverable wraps an error", () => {
const cause = new SyntaxError("boom");
const r = new repl.Recoverable(cause);
expect(r).toBeInstanceOf(SyntaxError);
expect(r.err).toBe(cause);
});
});

describe("v8.startupSnapshot", () => {
// https://github.com/oven-sh/bun/issues/32501
test("isBuildingSnapshot() returns false", () => {
Expand Down
Loading