Skip to content
Open
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
17 changes: 12 additions & 5 deletions src/js/internal/readline/interface.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Ported from Node.js v26.3.0 lib/internal/readline/interface.js for Bun's node:repl.
// The history options record in InterfaceConstructor is from Node.js v26.7.0 (nodejs/node c25b8e3331).
// Attribution: derived from Node.js, MIT licensed (Node.js contributors).
// prettier-ignore
const primordials = require("internal/repl/node-primordials");
Expand Down Expand Up @@ -156,6 +157,7 @@ declare global {
| ReadlineCallbackCompleter;

interface ReadlineHistoryOptions {
__proto__?: null;
size?: number;
history?: string[];
removeHistoryDuplicates?: boolean;
Expand All @@ -164,7 +166,7 @@ declare global {
onHistoryFileLoaded?(err: Error | null, context: object): void;
}

interface ReadlineInput extends Readable, ReadlineHistoryOptions {
interface ReadlineInput extends Readable {
input?: undefined;
isRaw?: boolean;
setRawMode?(mode: boolean): unknown;
Expand Down Expand Up @@ -231,6 +233,7 @@ function InterfaceConstructor(
let crlfDelay: number | undefined;
let prompt = "> ";
let signal: AbortSignal | undefined;
let historyOptions: HistoryOptions | undefined;

if (input?.input) {
// An options object was given
Expand Down Expand Up @@ -266,12 +269,16 @@ function InterfaceConstructor(
crlfDelay = input.crlfDelay;
input = input.input;

input.size = historySize;
input.history = history;
input.removeHistoryDuplicates = removeHistoryDuplicates;
historyOptions = {
__proto__: null,
size: historySize,
history,
removeHistoryDuplicates,
};
}

this.setupHistoryManager(input);
// Without an options object, the stream itself is the history options, as in Node: https://github.com/nodejs/node/blob/v26.12.1/lib/internal/readline/interface.js#L226
this.setupHistoryManager(historyOptions ?? (input as HistoryOptions));

if (completer !== undefined && typeof completer !== "function") {
throw new ERR_INVALID_ARG_VALUE("completer", completer);
Expand Down
138 changes: 138 additions & 0 deletions test/js/node/readline/history-options-off-input-fixture.cjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

53 changes: 53 additions & 0 deletions test/js/node/readline/readline.node.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// @ts-nocheck
import { bunEnv, bunExe, isDebug, nodeExeAtLeast } from "harness";
import { createTest } from "node-harness";
import { EventEmitter } from "node:events";
import { join } from "node:path";
import readline from "node:readline";
import { PassThrough, Readable, Writable } from "node:stream";
const { beforeEach, describe, it, createDoneDotAll, createCallCheckCtx, assert } = createTest(import.meta.path);
Expand Down Expand Up @@ -1942,6 +1944,57 @@ describe("readline.createInterface()", () => {
expect(result).toEqual(lines);
});

const historyOptionsOffInput = [
"history options on the stream after readline.createInterface({ input }): none",
"history options on the stream after readline.createInterface({ input, output, terminal: true }): none",
"history options on the stream after new readline.Interface({ input }): none",
"history options on the stream after readline/promises createInterface({ input }): none",
"history options on the stream after repl.start({ input, output }): none",
"history options on the stream after repl.start({ input, output, terminal: true }): none",
"history options on process.stdin after readline.createInterface({ input: process.stdin }): none",
"history options on the stream after FileHandle.readLines(): none",
"own size of the stream after createInterface({ input }): mine",
"history options that createInterface({ input }) reads from a stream with a getter for each: none",
"createInterface({ input, terminal: false }) on a stream that is not extensible: made",
"the history of the interface is the array that was given: true",
"createInterface(input) after createInterface({ input, historySize: 3, history: ['x'] }): historySize 30, history []",
"createInterface(input, output, undefined, true) after createInterface({ input, output, terminal: true, historySize: 3 }): historySize 30, history []",
"createInterface({ input, historySize: -1 }): threw ERR_OUT_OF_RANGE",
"createInterface(input) after that: historySize 30, history []",
"setupHistoryManager receives from new Interface({ input }): a null-prototype object with size,history,removeHistoryDuplicates",
// Without an options object Node.js reads the history options off the stream (nodejs/node#61526). This row keeps Bun the same.
"setupHistoryManager receives from new Interface(input): the stream",
];
for (const [name, runtime] of [
["bun", bunExe()],
// Before v26.7.0 Node.js stored the history options on the stream (nodejs/node#64585).
["node", nodeExeAtLeast("26.7.0")],
]) {
it.skipIf(!runtime)(
`should keep the history options off the input stream (${name})`,
async () => {
await using proc = Bun.spawn({
cmd: [runtime, join(import.meta.dir, "history-options-off-input-fixture.cjs")],
env: bunEnv,
stdin: "ignore",
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// A warning that Node.js writes to stderr (for NODE_OPTIONS, for a deprecation) is not part of the comparison.
const errors = name === "node" && exitCode === 0 ? "" : stderr;
expect({ rows: stdout.trimEnd().split(/\r?\n/), stderr: errors, exitCode }).toEqual({
rows: historyOptionsOffInput,
stderr: "",
Comment thread
claude[bot] marked this conversation as resolved.
Resolved
exitCode: 0,
});
},
// `bun test` gives a test 5 s and scripts/runner.node.ts gives it 90 s. A debug build needs 2 to 4 s to load the
// readline and REPL stacks, and more on a cold start. It gets the 90 s also without the runner.
isDebug ? 90_000 : undefined,
);
}

it("should respond to home and end sequences for common pttys ", () => {
const input = new PassThrough();
const rl = readline.createInterface({
Expand Down
Loading