diff --git a/.prettierignore b/.prettierignore index 536b6b45fb7e..c9c05f29f64c 100644 --- a/.prettierignore +++ b/.prettierignore @@ -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 diff --git a/LICENSE.md b/LICENSE.md index 8fe4234725d4..1c928cc17409 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -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 | diff --git a/docs/project/license.mdx b/docs/project/license.mdx index 55dae5fa10e6..8f4fa65e7c4d 100644 --- a/docs/project/license.mdx +++ b/docs/project/license.mdx @@ -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 | diff --git a/docs/runtime/nodejs-compat.mdx b/docs/runtime/nodejs-compat.mdx index eb7babc4ec38..f1e851986e0f 100644 --- a/docs/runtime/nodejs-compat.mdx +++ b/docs/runtime/nodejs-compat.mdx @@ -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) diff --git a/docs/snippets/cli/run.mdx b/docs/snippets/cli/run.mdx index 367972bba28e..9530cf60fa7d 100644 --- a/docs/snippets/cli/run.mdx +++ b/docs/snippets/cli/run.mdx @@ -62,6 +62,13 @@ bun run Control the shell used for package.json scripts. Supports either bun or system + + Open the Node.js-compatible REPL (node:repl). When combined with -e, starts the REPL and + then evaluates the script. Under --interactive, -e is raw JavaScript (matching{" "} + node -i -e); use bun repl for TypeScript. Distinct from bun repl, which is + Bun's native REPL. + + Use less memory, but run garbage collection more often diff --git a/oxlint.json b/oxlint.json index 5935bd0054be..e91a5acf35d6 100644 --- a/oxlint.json +++ b/oxlint.json @@ -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", + "test/js/node/**/parallel/**", "test/js/node/test/fixtures", // full of JS with intentional syntax errors "test/snippets/**", diff --git a/scripts/build/flags.ts b/scripts/build/flags.ts index c31aaa2de3c8..11077235f547 100644 --- a/scripts/build/flags.ts +++ b/scripts/build/flags.ts @@ -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"], diff --git a/src/codegen/bundle-modules.ts b/src/codegen/bundle-modules.ts index 1ed4fd6f03d5..d7098f987c3a 100644 --- a/src/codegen/bundle-modules.ts +++ b/src/codegen/bundle-modules.ts @@ -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")} diff --git a/src/js/eval/node-repl.ts b/src/js/eval/node-repl.ts new file mode 100644 index 000000000000..8af436f57d5d --- /dev/null +++ b/src/js/eval/node-repl.ts @@ -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)); +} else { + const REPL = require("node:repl"); + const createInternalRepl = (REPL as Record)[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; + } +} diff --git a/src/js/internal/readline/callbacks.js b/src/js/internal/readline/callbacks.js new file mode 100644 index 000000000000..03e4f87b5e45 --- /dev/null +++ b/src/js/internal/readline/callbacks.js @@ -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; diff --git a/src/js/internal/readline/emitKeypressEvents.js b/src/js/internal/readline/emitKeypressEvents.js new file mode 100644 index 000000000000..dbcb1240cad9 --- /dev/null +++ b/src/js/internal/readline/emitKeypressEvents.js @@ -0,0 +1,94 @@ +// Ported from Node.js v26.3.0 lib/internal/readline/emitKeypressEvents.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 { SafeStringIterator, Symbol } = primordials; + +const { charLengthAt, CSI, emitKeys } = require("internal/readline/utils"); +const { kSawKeyPress } = require("internal/readline/interface"); + +const { clearTimeout, setTimeout } = require("node:timers"); +const { kEscape } = CSI; + +const { StringDecoder } = require("node:string_decoder"); + +const KEYPRESS_DECODER = Symbol("keypress-decoder"); +const ESCAPE_DECODER = Symbol("escape-decoder"); + +// GNU readline library - keyseq-timeout is 500ms (default) +const ESCAPE_CODE_TIMEOUT = 500; + +/** + * accepts a readable Stream instance and makes it emit "keypress" events + */ + +function emitKeypressEvents(stream, iface = {}) { + if (stream[KEYPRESS_DECODER]) return; + + stream[KEYPRESS_DECODER] = new StringDecoder("utf8"); + + stream[ESCAPE_DECODER] = emitKeys(stream); + stream[ESCAPE_DECODER].next(); + + const triggerEscape = () => stream[ESCAPE_DECODER].next(""); + const { escapeCodeTimeout = ESCAPE_CODE_TIMEOUT } = iface; + let timeoutId; + + function onData(input) { + if (stream.listenerCount("keypress") > 0) { + const string = stream[KEYPRESS_DECODER].write(input); + if (string) { + clearTimeout(timeoutId); + + // This supports characters of length 2. + iface[kSawKeyPress] = charLengthAt(string, 0) === string.length; + iface.isCompletionEnabled = false; + + let length = 0; + for (const character of new SafeStringIterator(string)) { + length += character.length; + if (length === string.length) { + iface.isCompletionEnabled = true; + } + + try { + stream[ESCAPE_DECODER].next(character); + // Escape letter at the tail position + if (length === string.length && character === kEscape) { + timeoutId = setTimeout(triggerEscape, escapeCodeTimeout); + } + } catch (err) { + // If the generator throws (it could happen in the `keypress` + // event), we need to restart it. + stream[ESCAPE_DECODER] = emitKeys(stream); + stream[ESCAPE_DECODER].next(); + throw err; + } + } + } + } else { + // Nobody's watching anyway + stream.removeListener("data", onData); + stream.on("newListener", onNewListener); + } + } + + function onNewListener(event) { + if (event === "keypress") { + stream.on("data", onData); + stream.removeListener("newListener", onNewListener); + } + } + + if (stream.listenerCount("keypress") > 0) { + stream.on("data", onData); + } else { + stream.on("newListener", onNewListener); + } +} + +__node_module__.exports = emitKeypressEvents; + +export default __node_module__.exports; diff --git a/src/js/internal/readline/interface.js b/src/js/internal/readline/interface.js new file mode 100644 index 000000000000..29c878e4fd6e --- /dev/null +++ b/src/js/internal/readline/interface.js @@ -0,0 +1,1558 @@ +// Ported from Node.js v26.3.0 lib/internal/readline/interface.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 { + ArrayFrom, + ArrayPrototypeFilter, + ArrayPrototypeJoin, + ArrayPrototypeMap, + ArrayPrototypePop, + ArrayPrototypePush, + ArrayPrototypeReverse, + ArrayPrototypeShift, + ArrayPrototypeUnshift, + DateNow, + FunctionPrototypeCall, + MathCeil, + MathFloor, + MathMax, + MathMaxApply, + NumberIsFinite, + ObjectDefineProperty, + ObjectSetPrototypeOf, + RegExpPrototypeExec, + SafeStringIterator, + StringPrototypeCodePointAt, + StringPrototypeEndsWith, + StringPrototypeIncludes, + StringPrototypeRepeat, + StringPrototypeReplaceAll, + StringPrototypeSlice, + StringPrototypeSplit, + StringPrototypeStartsWith, + Symbol, + SymbolAsyncIterator, + SymbolDispose, +} = primordials; + +const { + AbortError, + codes: { ERR_INVALID_ARG_VALUE, ERR_USE_AFTER_CLOSE }, +} = require("internal/repl/node-errors"); + +const { validateAbortSignal, validateString, validateUint32 } = require("internal/validators"); +// node-shims eagerly loads node:{util,module,path,vm}; readline only needs +// kEmptyObject/addAbortListener, so import them from their tiny sources. +const { kEmptyObject } = require("internal/shared"); +// Don't destructure `inspect` — reading it loads internal/util/inspect (99 KB). +// Readline only touches it on the tab-completion error path. +const nodeInspect = require("internal/repl/node-inspect"); +const { getStringWidth, stripVTControlCharacters } = nodeInspect; +const EventEmitter = require("node:events"); +const { addAbortListener } = require("internal/abort_listener"); +const { charLengthAt, charLengthLeft, commonPrefix, kSubstringSearch } = require("internal/readline/utils"); +let emitKeypressEvents; +let kFirstEventParam; +const { clearScreenDown, cursorTo, moveCursor } = require("internal/readline/callbacks"); + +const { StringDecoder } = require("node:string_decoder"); +// history.js eagerly loads node:{fs,os,path,timers}; keep it lazy so a bare +// require("node:readline") for cursorTo/clearLine stays cheap. Constructing +// an Interface always calls setupHistoryManager, so readLines() still loads it. +let ReplHistory; + +const kMaxUndoRedoStackSize = 2048; +const kMincrlfDelay = 100; +/** + * The end of a line is signaled by either one of the following: + * - \r\n + * - \n + * - \r followed by something other than \n + * - \u2028 (Unicode 'LINE SEPARATOR') + * - \u2029 (Unicode 'PARAGRAPH SEPARATOR') + */ +const lineEnding = /\r?\n|\r(?!\n)|\u2028|\u2029/g; + +const kLineObjectStream = Symbol("line object stream"); +const kQuestionCancel = Symbol("kQuestionCancel"); +const kQuestion = Symbol("kQuestion"); + +// GNU readline library - keyseq-timeout is 500ms (default) +const ESCAPE_CODE_TIMEOUT = 500; + +// Max length of the kill ring +const kMaxLengthOfKillRing = 32; + +const kMultilinePrompt = Symbol("| "); + +const kAddHistory = Symbol("_addHistory"); +const kBeforeEdit = Symbol("_beforeEdit"); +const kDecoder = Symbol("_decoder"); +const kDeleteLeft = Symbol("_deleteLeft"); +const kDeleteLineLeft = Symbol("_deleteLineLeft"); +const kDeleteLineRight = Symbol("_deleteLineRight"); +const kDeleteRight = Symbol("_deleteRight"); +const kDeleteWordLeft = Symbol("_deleteWordLeft"); +const kDeleteWordRight = Symbol("_deleteWordRight"); +const kGetDisplayPos = Symbol("_getDisplayPos"); +const kHistoryNext = Symbol("_historyNext"); +const kMoveDownOrHistoryNext = Symbol("_moveDownOrHistoryNext"); +const kHistoryPrev = Symbol("_historyPrev"); +const kMoveUpOrHistoryPrev = Symbol("_moveUpOrHistoryPrev"); +const kInsertString = Symbol("_insertString"); +const kLine = Symbol("_line"); +const kLine_buffer = Symbol("_line_buffer"); +const kKillRing = Symbol("_killRing"); +const kKillRingCursor = Symbol("_killRingCursor"); +const kMoveCursor = Symbol("_moveCursor"); +const kNormalWrite = Symbol("_normalWrite"); +const kOldPrompt = Symbol("_oldPrompt"); +const kOnLine = Symbol("_onLine"); +const kSetLine = Symbol("_setLine"); +const kPreviousKey = Symbol("_previousKey"); +const kPrompt = Symbol("_prompt"); +const kPushToKillRing = Symbol("_pushToKillRing"); +const kPushToUndoStack = Symbol("_pushToUndoStack"); +const kQuestionCallback = Symbol("_questionCallback"); +const kLastCommandErrored = Symbol("_lastCommandErrored"); +const kQuestionReject = Symbol("_questionReject"); +const kRedo = Symbol("_redo"); +const kRedoStack = Symbol("_redoStack"); +const kRefreshLine = Symbol("_refreshLine"); +const kSawKeyPress = Symbol("_sawKeyPress"); +const kSawReturnAt = Symbol("_sawReturnAt"); +const kSetRawMode = Symbol("_setRawMode"); +const kTabComplete = Symbol("_tabComplete"); +const kTabCompleter = Symbol("_tabCompleter"); +const kTtyWrite = Symbol("_ttyWrite"); +const kUndo = Symbol("_undo"); +const kUndoStack = Symbol("_undoStack"); +const kIsMultiline = Symbol("_isMultiline"); +const kWordLeft = Symbol("_wordLeft"); +const kWordRight = Symbol("_wordRight"); +const kWriteToOutput = Symbol("_writeToOutput"); +const kYank = Symbol("_yank"); +const kYanking = Symbol("_yanking"); +const kYankPop = Symbol("_yankPop"); +const kSavePreviousState = Symbol("_savePreviousState"); +const kRestorePreviousState = Symbol("_restorePreviousState"); +const kPreviousLine = Symbol("_previousLine"); +const kPreviousCursor = Symbol("_previousCursor"); +const kPreviousCursorCols = Symbol("_previousCursorCols"); +const kMultilineMove = Symbol("_multilineMove"); +const kPreviousPrevRows = Symbol("_previousPrevRows"); +const kAddNewLineOnTTY = Symbol("_addNewLineOnTTY"); + +function InterfaceConstructor(input, output, completer, terminal) { + this[kSawReturnAt] = 0; + // upstream-todo(BridgeAR): Document this property. The name is not ideal, so we + // might want to expose an alias and document that instead. + this.isCompletionEnabled = true; + this[kSawKeyPress] = false; + this[kPreviousKey] = null; + this.escapeCodeTimeout = ESCAPE_CODE_TIMEOUT; + this.tabSize = 8; + + FunctionPrototypeCall(EventEmitter, this); + + let crlfDelay; + let prompt = "> "; + let signal; + + if (input?.input) { + // An options object was given + output = input.output; + completer = input.completer; + terminal = input.terminal; + signal = input.signal; + + // It is possible to configure the history through the input object + const historySize = input.historySize; + const history = input.history; + const removeHistoryDuplicates = input.removeHistoryDuplicates; + + if (input.tabSize !== undefined) { + validateUint32(input.tabSize, "tabSize", true); + this.tabSize = input.tabSize; + } + if (input.prompt !== undefined) { + prompt = input.prompt; + } + if (input.escapeCodeTimeout !== undefined) { + if (NumberIsFinite(input.escapeCodeTimeout)) { + this.escapeCodeTimeout = input.escapeCodeTimeout; + } else { + throw new ERR_INVALID_ARG_VALUE("input.escapeCodeTimeout", this.escapeCodeTimeout); + } + } + + if (signal) { + validateAbortSignal(signal, "options.signal"); + } + + crlfDelay = input.crlfDelay; + input = input.input; + + input.size = historySize; + input.history = history; + input.removeHistoryDuplicates = removeHistoryDuplicates; + } + + this.setupHistoryManager(input); + + if (completer !== undefined && typeof completer !== "function") { + throw new ERR_INVALID_ARG_VALUE("completer", completer); + } + + // Backwards compat; check the isTTY prop of the output stream + // when `terminal` was not specified + if (terminal === undefined && !(output === null || output === undefined)) { + terminal = !!output.isTTY; + } + + const self = this; + + this.line = ""; + this[kIsMultiline] = false; + this[kSubstringSearch] = null; + this.output = output; + this.input = input; + this[kUndoStack] = []; + this[kRedoStack] = []; + this[kPreviousCursorCols] = -1; + + // The kill ring is a global list of blocks of text that were previously + // killed (deleted). If its size exceeds kMaxLengthOfKillRing, the oldest + // element will be removed to make room for the latest deletion. With kill + // ring, users are able to recall (yank) or cycle (yank pop) among previously + // killed texts, quite similar to the behavior of Emacs. + this[kKillRing] = []; + this[kKillRingCursor] = 0; + + this.crlfDelay = crlfDelay ? MathMax(kMincrlfDelay, crlfDelay) : kMincrlfDelay; + this.completer = completer; + + this.setPrompt(prompt); + + this.terminal = !!terminal; + + function onerror(err) { + self.emit("error", err); + } + + function ondata(data) { + self[kNormalWrite](data); + } + + function onend() { + if (typeof self[kLine_buffer] === "string" && self[kLine_buffer].length > 0) { + self.emit("line", self[kLine_buffer]); + } + self.close(); + } + + function ontermend() { + if (typeof self.line === "string" && self.line.length > 0) { + self.emit("line", self.line); + } + self.close(); + } + + function onkeypress(s, key) { + self[kTtyWrite](s, key); + if (key?.sequence) { + // If the key.sequence is half of a surrogate pair + // (>= 0xd800 and <= 0xdfff), refresh the line so + // the character is displayed appropriately. + const ch = StringPrototypeCodePointAt(key.sequence, 0); + if (ch >= 0xd800 && ch <= 0xdfff) self[kRefreshLine](); + } + } + + function onresize() { + self[kRefreshLine](); + } + + this[kLineObjectStream] = undefined; + + input.on("error", onerror); + + if (!this.terminal) { + function onSelfCloseWithoutTerminal() { + input.removeListener("data", ondata); + input.removeListener("error", onerror); + input.removeListener("end", onend); + } + + input.on("data", ondata); + input.on("end", onend); + self.once("close", onSelfCloseWithoutTerminal); + this[kDecoder] = new StringDecoder("utf8"); + } else { + function onSelfCloseWithTerminal() { + input.removeListener("keypress", onkeypress); + input.removeListener("error", onerror); + input.removeListener("end", ontermend); + if (output !== null && output !== undefined) { + output.removeListener("resize", onresize); + } + } + + emitKeypressEvents ??= require("internal/readline/emitKeypressEvents"); + emitKeypressEvents(input, this); + + // `input` usually refers to stdin + input.on("keypress", onkeypress); + input.on("end", ontermend); + + this[kSetRawMode](true); + this.terminal = true; + + // Cursor position on the line. + this.cursor = 0; + + if (output !== null && output !== undefined) output.on("resize", onresize); + + self.once("close", onSelfCloseWithTerminal); + } + + if (signal) { + const onAborted = () => self.close(); + if (signal.aborted) { + process.nextTick(onAborted); + } else { + const disposable = addAbortListener(signal, onAborted); + self.once("close", disposable[SymbolDispose]); + } + } + + // Current line + this[kSetLine](""); + + input.resume(); +} + +$toClass(InterfaceConstructor, "InterfaceConstructor", EventEmitter); + +class Interface extends InterfaceConstructor { + get columns() { + if (this.output?.columns) return this.output.columns; + return Infinity; + } + + /** + * Sets the prompt written to the output. + * @param {string} prompt + * @returns {void} + */ + setPrompt(prompt) { + this[kPrompt] = prompt; + } + + /** + * Returns the current prompt used by `rl.prompt()`. + * @returns {string} + */ + getPrompt() { + return this[kPrompt]; + } + + setupHistoryManager(options) { + ReplHistory ??= require("internal/repl/history").ReplHistory; + this.historyManager = new ReplHistory(this, options); + + if (options.onHistoryFileLoaded) { + this.historyManager.initialize(options.onHistoryFileLoaded); + } + + ObjectDefineProperty(this, "history", { + __proto__: null, + configurable: true, + enumerable: true, + get() { + return this.historyManager.history; + }, + set(newHistory) { + return (this.historyManager.history = newHistory); + }, + }); + + ObjectDefineProperty(this, "historyIndex", { + __proto__: null, + configurable: true, + enumerable: true, + get() { + return this.historyManager.index; + }, + set(historyIndex) { + return (this.historyManager.index = historyIndex); + }, + }); + + ObjectDefineProperty(this, "historySize", { + __proto__: null, + configurable: true, + enumerable: true, + get() { + return this.historyManager.size; + }, + }); + + ObjectDefineProperty(this, "isFlushing", { + __proto__: null, + configurable: true, + enumerable: true, + get() { + return this.historyManager.isFlushing; + }, + }); + } + + [kSetRawMode](mode) { + const wasInRawMode = this.input.isRaw; + + if (typeof this.input.setRawMode === "function") { + this.input.setRawMode(mode); + } + + return wasInRawMode; + } + + /** + * Writes the configured `prompt` to a new line in `output`. + * @param {boolean} [preserveCursor] + * @returns {void} + */ + prompt(preserveCursor) { + if (this.paused) this.resume(); + if (this.terminal && process.env.TERM !== "dumb") { + if (!preserveCursor) this.cursor = 0; + this[kRefreshLine](); + } else { + this[kWriteToOutput](this[kPrompt]); + } + } + + [kQuestion](query, cb) { + if (this.closed) { + throw new ERR_USE_AFTER_CLOSE("readline"); + } + if (this[kQuestionCallback]) { + this.prompt(); + } else { + this[kOldPrompt] = this[kPrompt]; + this.setPrompt(query); + this[kQuestionCallback] = cb; + this.prompt(); + } + } + + [kSetLine](line = "") { + this.line = line; + this[kIsMultiline] = StringPrototypeIncludes(line, "\n"); + } + + [kOnLine](line) { + if (this[kQuestionCallback]) { + const cb = this[kQuestionCallback]; + this[kQuestionCallback] = null; + this.setPrompt(this[kOldPrompt]); + cb(line); + } else { + this.emit("line", line); + } + } + + [kBeforeEdit](oldText, oldCursor) { + this[kPushToUndoStack](oldText, oldCursor); + } + + [kQuestionCancel]() { + if (this[kQuestionCallback]) { + this[kQuestionCallback] = null; + this.setPrompt(this[kOldPrompt]); + this.clearLine(); + } + } + + [kWriteToOutput](stringToWrite) { + validateString(stringToWrite, "stringToWrite"); + + if (this.output !== null && this.output !== undefined) { + this.output.write(stringToWrite); + } + } + + [kAddHistory]() { + return this.historyManager.addHistory(this[kIsMultiline], this[kLastCommandErrored]); + } + + [kRefreshLine]() { + // line length + const line = this[kPrompt] + this.line; + const dispPos = this[kGetDisplayPos](line); + const lineCols = dispPos.cols; + const lineRows = dispPos.rows; + + // cursor position + const cursorPos = this.getCursorPos(); + + // First move to the bottom of the current line, based on cursor pos + const prevRows = this.prevRows || 0; + if (prevRows > 0) { + moveCursor(this.output, 0, -prevRows); + } + + // Cursor to left edge. + cursorTo(this.output, 0); + // erase data + clearScreenDown(this.output); + + if (this[kIsMultiline]) { + const lines = StringPrototypeSplit(this.line, "\n"); + // Write first line with normal prompt + this[kWriteToOutput](this[kPrompt] + lines[0]); + + // For continuation lines, add the "|" prefix + for (let i = 1; i < lines.length; i++) { + this[kWriteToOutput](`\n${kMultilinePrompt.description}` + lines[i]); + } + } else { + // Write the prompt and the current buffer content. + this[kWriteToOutput](line); + } + + // Force terminal to allocate a new line + if (lineCols === 0) { + this[kWriteToOutput](" "); + } + + // Move cursor to original position. + cursorTo(this.output, cursorPos.cols); + + const diff = lineRows - cursorPos.rows; + if (diff > 0) { + moveCursor(this.output, 0, -diff); + } + + this.prevRows = cursorPos.rows; + } + + /** + * Closes the `readline.Interface` instance. + * @returns {void} + */ + close() { + if (this.closed) return; + this.pause(); + if (this.terminal) { + this[kSetRawMode](false); + } + this.closed = true; + this.emit("close"); + } + + /** + * Pauses the `input` stream. + * @returns {void | Interface} + */ + pause() { + if (this.closed) { + throw new ERR_USE_AFTER_CLOSE("readline"); + } + if (this.paused) return; + this.input.pause(); + this.paused = true; + this.emit("pause"); + return this; + } + + /** + * Resumes the `input` stream if paused. + * @returns {void | Interface} + */ + resume() { + if (this.closed) { + throw new ERR_USE_AFTER_CLOSE("readline"); + } + if (!this.paused) return; + this.input.resume(); + this.paused = false; + this.emit("resume"); + return this; + } + + /** + * Writes either `data` or a `key` sequence identified by + * `key` to the `output`. + * @param {string} d + * @param {{ + * ctrl?: boolean; + * meta?: boolean; + * shift?: boolean; + * name?: string; + * }} [key] + * @returns {void} + */ + write(d, key) { + if (this.closed) { + throw new ERR_USE_AFTER_CLOSE("readline"); + } + if (this.paused) this.resume(); + if (this.terminal) { + this[kTtyWrite](d, key); + } else { + this[kNormalWrite](d); + } + } + + [kNormalWrite](b) { + if (b === undefined) { + return; + } + let string = this[kDecoder].write(b); + if (this[kSawReturnAt] && DateNow() - this[kSawReturnAt] <= this.crlfDelay) { + if (StringPrototypeCodePointAt(string) === 10) string = StringPrototypeSlice(string, 1); + this[kSawReturnAt] = 0; + } + + // Run test() on the new string chunk, not on the entire line buffer. + let newPartContainsEnding = RegExpPrototypeExec(lineEnding, string); + if (newPartContainsEnding !== null) { + if (this[kLine_buffer]) { + string = this[kLine_buffer] + string; + this[kLine_buffer] = null; + lineEnding.lastIndex = 0; // Start the search from the beginning of the string. + newPartContainsEnding = RegExpPrototypeExec(lineEnding, string); + } + this[kSawReturnAt] = StringPrototypeEndsWith(string, "\r") ? DateNow() : 0; + + const indexes = [0, newPartContainsEnding.index, lineEnding.lastIndex]; + let nextMatch; + while ((nextMatch = RegExpPrototypeExec(lineEnding, string)) !== null) { + ArrayPrototypePush(indexes, nextMatch.index, lineEnding.lastIndex); + } + const lastIndex = indexes.length - 1; + // Either '' or (conceivably) the unfinished portion of the next line + this[kLine_buffer] = StringPrototypeSlice(string, indexes[lastIndex]); + for (let i = 1; i < lastIndex; i += 2) { + this[kOnLine](StringPrototypeSlice(string, indexes[i - 1], indexes[i])); + } + } else if (string) { + // No newlines this time, save what we have for next time + if (this[kLine_buffer]) { + this[kLine_buffer] += string; + } else { + this[kLine_buffer] = string; + } + } + } + + [kInsertString](c) { + this[kBeforeEdit](this.line, this.cursor); + if (!this.isCompletionEnabled) { + if (this.cursor < this.line.length) { + const beg = StringPrototypeSlice(this.line, 0, this.cursor); + const end = StringPrototypeSlice(this.line, this.cursor, this.line.length); + this.line = beg + c + end; + } else { + this.line += c; + } + this.cursor += c.length; + this[kWriteToOutput](c); + return; + } + if (this.cursor < this.line.length) { + const beg = StringPrototypeSlice(this.line, 0, this.cursor); + const end = StringPrototypeSlice(this.line, this.cursor, this.line.length); + this[kSetLine](beg + c + end); + this.cursor += c.length; + this[kRefreshLine](); + } else { + const oldPos = this.getCursorPos(); + this.line += c; + this.cursor += c.length; + const newPos = this.getCursorPos(); + + if (oldPos.rows < newPos.rows) { + this[kRefreshLine](); + } else { + this[kWriteToOutput](c); + } + } + } + + async [kTabComplete](lastKeypressWasTab) { + this.pause(); + const string = StringPrototypeSlice(this.line, 0, this.cursor); + let value; + try { + value = await this.completer(string); + } catch (err) { + this[kWriteToOutput](`Tab completion error: ${nodeInspect.inspect(err)}`); + return; + } finally { + this.resume(); + } + this[kTabCompleter](lastKeypressWasTab, value); + } + + [kTabCompleter](lastKeypressWasTab, { 0: completions, 1: completeOn }) { + // Result and the text that was completed. + + if (!completions || completions.length === 0) { + return; + } + + // If there is a common prefix to all matches, then apply that portion. + const prefix = commonPrefix(ArrayPrototypeFilter(completions, e => e !== "")); + if (StringPrototypeStartsWith(prefix, completeOn) && prefix.length > completeOn.length) { + this[kInsertString](StringPrototypeSlice(prefix, completeOn.length)); + return; + } else if (!StringPrototypeStartsWith(completeOn, prefix)) { + this[kSetLine]( + StringPrototypeSlice(this.line, 0, this.cursor - completeOn.length) + + prefix + + StringPrototypeSlice(this.line, this.cursor, this.line.length), + ); + this.cursor = this.cursor - completeOn.length + prefix.length; + this[kRefreshLine](); + return; + } + + if (!lastKeypressWasTab) { + return; + } + + this[kBeforeEdit](this.line, this.cursor); + + // Apply/show completions. + const completionsWidth = ArrayPrototypeMap(completions, e => getStringWidth(e)); + const width = MathMaxApply(completionsWidth) + 2; // 2 space padding + let maxColumns = MathFloor(this.columns / width) || 1; + if (maxColumns === Infinity) { + maxColumns = 1; + } + let output = "\r\n"; + let lineIndex = 0; + let whitespace = 0; + for (let i = 0; i < completions.length; i++) { + const completion = completions[i]; + if (completion === "" || lineIndex === maxColumns) { + output += "\r\n"; + lineIndex = 0; + whitespace = 0; + } else { + output += StringPrototypeRepeat(" ", whitespace); + } + if (completion !== "") { + output += completion; + whitespace = width - completionsWidth[i]; + lineIndex++; + } else { + output += "\r\n"; + } + } + if (lineIndex !== 0) { + output += "\r\n\r\n"; + } + this[kWriteToOutput](output); + this[kRefreshLine](); + } + + [kWordLeft]() { + if (this.cursor > 0) { + // Reverse the string and match a word near beginning + // to avoid quadratic time complexity + const leading = StringPrototypeSlice(this.line, 0, this.cursor); + const reversed = ArrayPrototypeJoin(ArrayPrototypeReverse(ArrayFrom(leading)), ""); + const match = RegExpPrototypeExec(/^\s*(?:[^\w\s]+|\w+)?/, reversed); + this[kMoveCursor](-match[0].length); + } + } + + [kWordRight]() { + if (this.cursor < this.line.length) { + const trailing = StringPrototypeSlice(this.line, this.cursor); + const match = RegExpPrototypeExec(/^(?:\s+|[^\w\s]+|\w+)\s*/, trailing); + this[kMoveCursor](match[0].length); + } + } + + [kDeleteLeft]() { + if (this.cursor > 0 && this.line.length > 0) { + this[kBeforeEdit](this.line, this.cursor); + // The number of UTF-16 units comprising the character to the left + const charSize = charLengthLeft(this.line, this.cursor); + this.line = + StringPrototypeSlice(this.line, 0, this.cursor - charSize) + + StringPrototypeSlice(this.line, this.cursor, this.line.length); + + this.cursor -= charSize; + this[kRefreshLine](); + } + } + + [kDeleteRight]() { + if (this.cursor < this.line.length) { + this[kBeforeEdit](this.line, this.cursor); + // The number of UTF-16 units comprising the character to the left + const charSize = charLengthAt(this.line, this.cursor); + this.line = + StringPrototypeSlice(this.line, 0, this.cursor) + + StringPrototypeSlice(this.line, this.cursor + charSize, this.line.length); + this[kRefreshLine](); + } + } + + [kDeleteWordLeft]() { + if (this.cursor > 0) { + this[kBeforeEdit](this.line, this.cursor); + // Reverse the string and match a word near beginning + // to avoid quadratic time complexity + let leading = StringPrototypeSlice(this.line, 0, this.cursor); + const reversed = ArrayPrototypeJoin(ArrayPrototypeReverse(ArrayFrom(leading)), ""); + const match = RegExpPrototypeExec(/^\s*(?:[^\w\s]+|\w+)?/, reversed); + leading = StringPrototypeSlice(leading, 0, leading.length - match[0].length); + this.line = leading + StringPrototypeSlice(this.line, this.cursor, this.line.length); + this.cursor = leading.length; + this[kRefreshLine](); + } + } + + [kDeleteWordRight]() { + if (this.cursor < this.line.length) { + this[kBeforeEdit](this.line, this.cursor); + const trailing = StringPrototypeSlice(this.line, this.cursor); + const match = RegExpPrototypeExec(/^(?:\s+|\W+|\w+)\s*/, trailing); + this.line = StringPrototypeSlice(this.line, 0, this.cursor) + StringPrototypeSlice(trailing, match[0].length); + this[kRefreshLine](); + } + } + + [kDeleteLineLeft]() { + this[kBeforeEdit](this.line, this.cursor); + const del = StringPrototypeSlice(this.line, 0, this.cursor); + this[kSetLine](StringPrototypeSlice(this.line, this.cursor)); + this.cursor = 0; + this[kPushToKillRing](del); + this[kRefreshLine](); + } + + [kDeleteLineRight]() { + this[kBeforeEdit](this.line, this.cursor); + const del = StringPrototypeSlice(this.line, this.cursor); + this[kSetLine](StringPrototypeSlice(this.line, 0, this.cursor)); + this[kPushToKillRing](del); + this[kRefreshLine](); + } + + [kPushToKillRing](del) { + if (!del || del === this[kKillRing][0]) return; + ArrayPrototypeUnshift(this[kKillRing], del); + this[kKillRingCursor] = 0; + while (this[kKillRing].length > kMaxLengthOfKillRing) ArrayPrototypePop(this[kKillRing]); + } + + [kYank]() { + if (this[kKillRing].length > 0) { + this[kYanking] = true; + this[kInsertString](this[kKillRing][this[kKillRingCursor]]); + } + } + + [kYankPop]() { + if (!this[kYanking]) { + return; + } + if (this[kKillRing].length > 1) { + const lastYank = this[kKillRing][this[kKillRingCursor]]; + this[kKillRingCursor]++; + if (this[kKillRingCursor] >= this[kKillRing].length) { + this[kKillRingCursor] = 0; + } + const currentYank = this[kKillRing][this[kKillRingCursor]]; + const head = StringPrototypeSlice(this.line, 0, this.cursor - lastYank.length); + const tail = StringPrototypeSlice(this.line, this.cursor); + this[kSetLine](head + currentYank + tail); + this.cursor = head.length + currentYank.length; + this[kRefreshLine](); + } + } + + [kSavePreviousState]() { + this[kPreviousLine] = this.line; + this[kPreviousCursor] = this.cursor; + this[kPreviousPrevRows] = this.prevRows; + } + + [kRestorePreviousState]() { + this[kSetLine](this[kPreviousLine]); + this.cursor = this[kPreviousCursor]; + this.prevRows = this[kPreviousPrevRows]; + } + + clearLine() { + this[kMoveCursor](+Infinity); + this[kWriteToOutput]("\r\n"); + this[kSetLine](""); + this.cursor = 0; + this.prevRows = 0; + } + + [kLine]() { + this[kSavePreviousState](); + const line = this[kAddHistory](); + this[kUndoStack] = []; + this[kRedoStack] = []; + this.clearLine(); + this[kOnLine](line); + } + + // upstream-todo(puskin94): edit [kTtyWrite] to make call this function on a new key combination + // to make it add a new line in the middle of a "complete" multiline. + // I tried with shift + enter but it is not detected. Find a new one. + // Make sure to call this[kSavePreviousState](); && this.clearLine(); + // before calling this[kAddNewLineOnTTY] to simulate what [kLine] is doing. + + // When this function is called, the actual cursor is at the very end of the whole string, + // No matter where the new line was entered. + // This function should only be used when the output is a TTY + [kAddNewLineOnTTY]() { + // Restore terminal state and store current line + this[kRestorePreviousState](); + const originalLine = this.line; + + // Split the line at the current cursor position + const beforeCursor = StringPrototypeSlice(this.line, 0, this.cursor); + let afterCursor = StringPrototypeSlice(this.line, this.cursor, this.line.length); + + // Add the new line where the cursor is at + this[kSetLine](`${beforeCursor}\n${afterCursor}`); + + // To account for the new line + this.cursor += 1; + + const hasContentAfterCursor = afterCursor.length > 0; + const cursorIsNotOnFirstLine = this.prevRows > 0; + let needsRewriteFirstLine = false; + + // Handle cursor positioning based on different scenarios + if (hasContentAfterCursor) { + const splitBeg = StringPrototypeSplit(beforeCursor, "\n"); + // Determine if we need to rewrite the first line + needsRewriteFirstLine = splitBeg.length < 2; + + // If the cursor is not on the first line + if (cursorIsNotOnFirstLine) { + const splitEnd = StringPrototypeSplit(afterCursor, "\n"); + + // If the cursor when I pressed enter was at least on the second line + // I need to completely erase the line where the cursor was pressed because it is possible + // That it was pressed in the middle of the line, hence I need to write the whole line. + // To achieve that, I need to reach the line above the current line coming from the end + const dy = splitEnd.length + 1; + + // Calculate how many Xs we need to move on the right to get to the end of the line + const dxEndOfLineAbove = (splitBeg[splitBeg.length - 2] || "").length + kMultilinePrompt.description.length; + moveCursor(this.output, dxEndOfLineAbove, -dy); + + // This is the line that was split in the middle + // Just add it to the rest of the line that will be printed later + afterCursor = `${splitBeg[splitBeg.length - 1]}\n${afterCursor}`; + } else { + // Otherwise, go to the very beginning of the first line and erase everything + const dy = StringPrototypeSplit(originalLine, "\n").length; + moveCursor(this.output, 0, -dy); + } + + // Erase from the cursor to the end of the line + clearScreenDown(this.output); + + if (cursorIsNotOnFirstLine) { + this[kWriteToOutput]("\n"); + } + } + + if (needsRewriteFirstLine) { + this[kWriteToOutput](`${this[kPrompt]}${beforeCursor}\n${kMultilinePrompt.description}`); + } else { + this[kWriteToOutput](kMultilinePrompt.description); + } + + // Write the rest and restore the cursor to where the user left it + if (hasContentAfterCursor) { + // Save the cursor pos, we need to come back here + const oldCursor = this.getCursorPos(); + + // Write everything after the cursor which has been deleted by clearScreenDown + const formattedEndContent = StringPrototypeReplaceAll(afterCursor, "\n", `\n${kMultilinePrompt.description}`); + + this[kWriteToOutput](formattedEndContent); + + const newCursor = this[kGetDisplayPos](this.line); + + // Go back to where the cursor was, with relative movement + moveCursor(this.output, oldCursor.cols - newCursor.cols, oldCursor.rows - newCursor.rows); + + // Setting how many rows we have on top of the cursor + // Necessary for kRefreshLine + this.prevRows = oldCursor.rows; + } else { + // Setting how many rows we have on top of the cursor + // Necessary for kRefreshLine + this.prevRows = StringPrototypeSplit(this.line, "\n").length - 1; + } + } + + [kPushToUndoStack](text, cursor) { + if (ArrayPrototypePush(this[kUndoStack], { text, cursor }) > kMaxUndoRedoStackSize) { + ArrayPrototypeShift(this[kUndoStack]); + } + } + + [kUndo]() { + if (this[kUndoStack].length <= 0) return; + + ArrayPrototypePush(this[kRedoStack], { text: this.line, cursor: this.cursor }); + + const entry = ArrayPrototypePop(this[kUndoStack]); + this[kSetLine](entry.text); + this.cursor = entry.cursor; + + this[kRefreshLine](); + } + + [kRedo]() { + if (this[kRedoStack].length <= 0) return; + + ArrayPrototypePush(this[kUndoStack], { text: this.line, cursor: this.cursor }); + + const entry = ArrayPrototypePop(this[kRedoStack]); + this[kSetLine](entry.text); + this.cursor = entry.cursor; + + this[kRefreshLine](); + } + + [kMultilineMove](direction, splitLines, { rows, cols }) { + const curr = splitLines[rows]; + const down = direction === 1; + const adj = splitLines[rows + direction]; + const promptLen = kMultilinePrompt.description.length; + let amountToMove; + // Clamp distance to end of current + prompt + next/prev line + newline + const clamp = down ? curr.length - cols + promptLen + adj.length + 1 : -cols + 1; + const shouldClamp = cols > adj.length + 1; + + if (shouldClamp) { + if (this[kPreviousCursorCols] === -1) { + this[kPreviousCursorCols] = cols; + } + amountToMove = clamp; + } else { + if (down) { + amountToMove = curr.length + 1; + } else { + amountToMove = -adj.length - 1; + } + if (this[kPreviousCursorCols] !== -1) { + if (this[kPreviousCursorCols] <= adj.length) { + amountToMove += this[kPreviousCursorCols] - cols; + this[kPreviousCursorCols] = -1; + } else { + amountToMove = clamp; + } + } + } + + this[kMoveCursor](amountToMove); + } + + [kMoveDownOrHistoryNext]() { + const cursorPos = this.getCursorPos(); + const splitLines = StringPrototypeSplit(this.line, "\n"); + if (this[kIsMultiline] && cursorPos.rows < splitLines.length - 1) { + this[kMultilineMove](1, splitLines, cursorPos); + return; + } + this[kPreviousCursorCols] = -1; + this[kHistoryNext](); + } + + // upstream-todo(BridgeAR): Add underscores to the search part and a red background in + // case no match is found. This should only be the visual part and not the + // actual line content! + // upstream-todo(BridgeAR): In case the substring based search is active and the end is + // reached, show a comment how to search the history as before. E.g., using + // + N. Only show this after two/three UPs or DOWNs, not on the first + // one. + [kHistoryNext]() { + if (!this.historyManager.canNavigateToNext()) { + return; + } + + this[kBeforeEdit](this.line, this.cursor); + this[kSetLine](this.historyManager.navigateToNext(this[kSubstringSearch])); + this.cursor = this.line.length; // Set cursor to end of line. + this[kRefreshLine](); + } + + [kMoveUpOrHistoryPrev]() { + const cursorPos = this.getCursorPos(); + if (this[kIsMultiline] && cursorPos.rows > 0) { + const splitLines = StringPrototypeSplit(this.line, "\n"); + this[kMultilineMove](-1, splitLines, cursorPos); + return; + } + this[kPreviousCursorCols] = -1; + this[kHistoryPrev](); + } + + [kHistoryPrev]() { + if (!this.historyManager.canNavigateToPrevious()) { + return; + } + + this[kBeforeEdit](this.line, this.cursor); + this[kSetLine](this.historyManager.navigateToPrevious(this[kSubstringSearch])); + this.cursor = this.line.length; // Set cursor to end of line. + this[kRefreshLine](); + } + + // Returns the last character's display position of the given string + [kGetDisplayPos](str) { + let offset = 0; + const col = this.columns; + let rows = 0; + str = stripVTControlCharacters(str); + + for (const char of new SafeStringIterator(str)) { + if (char === "\n") { + // Rows must be incremented by 1 even if offset = 0 or col = +Infinity. + rows += MathCeil(offset / col) || 1; + // Only add prefix offset for continuation lines in user input (not prompts) + offset = this[kIsMultiline] ? kMultilinePrompt.description.length : 0; + continue; + } + // Tabs must be aligned by an offset of the tab size. + if (char === "\t") { + offset += this.tabSize - (offset % this.tabSize); + continue; + } + const width = getStringWidth(char, false /* stripVTControlCharacters */); + if (width === 0 || width === 1) { + offset += width; + } else { + // width === 2 + if ((offset + 1) % col === 0) { + offset++; + } + offset += 2; + } + } + + const cols = offset % col; + rows += (offset - cols) / col; + + return { cols, rows }; + } + + /** + * Returns the real position of the cursor in relation + * to the input prompt + string. + * @returns {{ + * rows: number; + * cols: number; + * }} + */ + getCursorPos() { + const strBeforeCursor = this[kPrompt] + StringPrototypeSlice(this.line, 0, this.cursor); + + return this[kGetDisplayPos](strBeforeCursor); + } + + // This function moves cursor dx places to the right + // (-dx for left) and refreshes the line if it is needed. + [kMoveCursor](dx) { + if (dx === 0) { + return; + } + const oldPos = this.getCursorPos(); + this.cursor += dx; + + // Bounds check + if (this.cursor < 0) { + this.cursor = 0; + } else if (this.cursor > this.line.length) { + this.cursor = this.line.length; + } + + const newPos = this.getCursorPos(); + + // Check if cursor stayed on the line. + if (oldPos.rows === newPos.rows) { + const diffWidth = newPos.cols - oldPos.cols; + moveCursor(this.output, diffWidth, 0); + } else { + this[kRefreshLine](); + } + } + + // Handle a write from the tty + [kTtyWrite](s, key) { + const previousKey = this[kPreviousKey]; + key ||= kEmptyObject; + this[kPreviousKey] = key; + let shouldResetPreviousCursorCols = true; + + if (!key.meta || key.name !== "y") { + // Reset yanking state unless we are doing yank pop. + this[kYanking] = false; + } + + // Activate or deactivate substring search. + if ((key.name === "up" || key.name === "down") && !key.ctrl && !key.meta && !key.shift) { + if (this[kSubstringSearch] === null && !this[kIsMultiline]) { + this[kSubstringSearch] = StringPrototypeSlice(this.line, 0, this.cursor); + } + } else if (this[kSubstringSearch] !== null) { + this[kSubstringSearch] = null; + // Reset the index in case there's no match. + if (this.history.length === this.historyIndex) { + this.historyIndex = -1; + } + } + + // Undo & Redo + if (typeof key.sequence === "string") { + switch (StringPrototypeCodePointAt(key.sequence, 0)) { + case 0x1f: + this[kUndo](); + return; + case 0x1e: + this[kRedo](); + return; + default: + break; + } + } + + // Ignore escape key, fixes + // https://github.com/nodejs/node-v0.x-archive/issues/2876. + if (key.name === "escape") return; + + if (key.ctrl && key.shift) { + /* Control and shift pressed */ + switch (key.name) { + // upstream-todo(BridgeAR): The transmitted escape sequence is `\b` and that is + // identical to -h. It should have a unique escape sequence. + case "backspace": + this[kDeleteLineLeft](); + break; + + case "delete": + this[kDeleteLineRight](); + break; + } + } else if (key.ctrl) { + /* Control key pressed */ + + switch (key.name) { + case "c": + if (this.listenerCount("SIGINT") > 0) { + this.emit("SIGINT"); + } else { + // This readline instance is finished + this.close(); + this[kQuestionReject]?.(new AbortError("Aborted with Ctrl+C")); + } + break; + + case "h": // delete left + this[kDeleteLeft](); + break; + + case "d": // delete right or EOF + if (this.cursor === 0 && this.line.length === 0) { + // This readline instance is finished + this.close(); + this[kQuestionReject]?.(new AbortError("Aborted with Ctrl+D")); + } else if (this.cursor < this.line.length) { + this[kDeleteRight](); + } + break; + + case "u": // Delete from current to start of line + this[kDeleteLineLeft](); + break; + + case "k": // Delete from current to end of line + this[kDeleteLineRight](); + break; + + case "a": // Go to the start of the line + this[kMoveCursor](-Infinity); + break; + + case "e": // Go to the end of the line + this[kMoveCursor](+Infinity); + break; + + case "b": // back one character + this[kMoveCursor](-charLengthLeft(this.line, this.cursor)); + break; + + case "f": // Forward one character + this[kMoveCursor](+charLengthAt(this.line, this.cursor)); + break; + + case "l": // Clear the whole screen + cursorTo(this.output, 0, 0); + clearScreenDown(this.output); + this[kRefreshLine](); + break; + + case "n": // next history item + this[kHistoryNext](); + break; + + case "p": // Previous history item + this[kHistoryPrev](); + break; + + case "y": // Yank killed string + this[kYank](); + break; + + case "z": + if (process.platform === "win32") break; + if (this.listenerCount("SIGTSTP") > 0) { + this.emit("SIGTSTP"); + } else { + process.once("SIGCONT", () => { + // Don't raise events if stream has already been abandoned. + if (!this.paused) { + // Stream must be paused and resumed after SIGCONT to catch + // SIGINT, SIGTSTP, and EOF. + this.pause(); + this.emit("SIGCONT"); + } + // Explicitly re-enable "raw mode" and move the cursor to + // the correct position. + // See https://github.com/joyent/node/issues/3295. + this[kSetRawMode](true); + this[kRefreshLine](); + }); + this[kSetRawMode](false); + process.kill(process.pid, "SIGTSTP"); + } + break; + + case "w": // Delete backwards to a word boundary + // upstream-todo(BridgeAR): The transmitted escape sequence is `\b` and that is + // identical to -h. It should have a unique escape sequence. + // Falls through + case "backspace": + this[kDeleteWordLeft](); + break; + + case "delete": // Delete forward to a word boundary + this[kDeleteWordRight](); + break; + + case "left": + this[kWordLeft](); + break; + + case "right": + this[kWordRight](); + break; + } + } else if (key.meta) { + /* Meta key pressed */ + + switch (key.name) { + case "b": // backward word + this[kWordLeft](); + break; + + case "f": // forward word + this[kWordRight](); + break; + + case "d": // delete forward word + case "delete": + this[kDeleteWordRight](); + break; + + case "backspace": // Delete backwards to a word boundary + this[kDeleteWordLeft](); + break; + + case "y": // Doing yank pop + this[kYankPop](); + break; + } + } else { + /* No modifier keys used */ + + // \r bookkeeping is only relevant if a \n comes right after. + if (this[kSawReturnAt] && key.name !== "enter") this[kSawReturnAt] = 0; + + switch (key.name) { + case "return": // Carriage return, i.e. \r + this[kSawReturnAt] = DateNow(); + this[kLine](); + break; + + case "enter": + // When key interval > crlfDelay + if (this[kSawReturnAt] === 0 || DateNow() - this[kSawReturnAt] > this.crlfDelay) { + this[kLine](); + } + this[kSawReturnAt] = 0; + break; + + case "backspace": + this[kDeleteLeft](); + break; + + case "delete": + this[kDeleteRight](); + break; + + case "left": + // Obtain the code point to the left + this[kMoveCursor](-charLengthLeft(this.line, this.cursor)); + break; + + case "right": + this[kMoveCursor](+charLengthAt(this.line, this.cursor)); + break; + + case "home": + this[kMoveCursor](-Infinity); + break; + + case "end": + this[kMoveCursor](+Infinity); + break; + + case "up": + shouldResetPreviousCursorCols = false; + this[kMoveUpOrHistoryPrev](); + break; + + case "down": + shouldResetPreviousCursorCols = false; + this[kMoveDownOrHistoryNext](); + break; + + case "tab": + // If tab completion enabled, do that... + if (typeof this.completer === "function" && this.isCompletionEnabled) { + const lastKeypressWasTab = previousKey && previousKey.name === "tab"; + this[kTabComplete](lastKeypressWasTab); + break; + } + // falls through + default: + if (typeof s === "string" && s) { + // Erase state of previous searches. + lineEnding.lastIndex = 0; + let nextMatch; + // Keep track of the end of the last match. + let lastIndex = 0; + while ((nextMatch = RegExpPrototypeExec(lineEnding, s)) !== null) { + this[kInsertString](StringPrototypeSlice(s, lastIndex, nextMatch.index)); + ({ lastIndex } = lineEnding); + this[kLine](); + // Restore lastIndex as the call to kLine could have mutated it. + lineEnding.lastIndex = lastIndex; + } + // This ensures that the last line is written if it doesn't end in a newline. + // Note that the last line may be the first line, in which case this still works. + this[kInsertString](StringPrototypeSlice(s, lastIndex)); + } + } + } + if (shouldResetPreviousCursorCols) { + this[kPreviousCursorCols] = -1; + } + } + + /** + * Creates an `AsyncIterator` object that iterates through + * each line in the input stream as a string. + * @returns {AsyncIterableIterator} + */ + [SymbolAsyncIterator]() { + if (this[kLineObjectStream] === undefined) { + kFirstEventParam ??= Symbol.for("nodejs.kFirstEventParam"); + this[kLineObjectStream] = EventEmitter.on(this, "line", { + close: ["close"], + highWaterMark: 1024, + [kFirstEventParam]: true, + }); + } + return this[kLineObjectStream]; + } +} +Interface.prototype[SymbolDispose] = ObjectDefineProperty( + function () { + this.close(); + }, + "name", + { __proto__: null, configurable: true, value: "[Symbol.dispose]" }, +); + +__node_module__.exports = { + Interface, + InterfaceConstructor, + kAddHistory, + kDecoder, + kDeleteLeft, + kDeleteLineLeft, + kDeleteLineRight, + kDeleteRight, + kDeleteWordLeft, + kDeleteWordRight, + kGetDisplayPos, + kHistoryNext, + kHistoryPrev, + kInsertString, + kIsMultiline, + kLine, + kLine_buffer, + kMoveCursor, + kNormalWrite, + kOldPrompt, + kOnLine, + kSetLine, + kPreviousKey, + kPrompt, + kQuestion, + kQuestionCallback, + kQuestionCancel, + kQuestionReject, + kRefreshLine, + kSawKeyPress, + kSawReturnAt, + kSetRawMode, + kTabComplete, + kTabCompleter, + kTtyWrite, + kWordLeft, + kWordRight, + kWriteToOutput, + kMultilinePrompt, + kRestorePreviousState, + kAddNewLineOnTTY, + kLastCommandErrored, +}; + +export default __node_module__.exports; diff --git a/src/js/internal/readline/promises.js b/src/js/internal/readline/promises.js new file mode 100644 index 000000000000..3b40501245e8 --- /dev/null +++ b/src/js/internal/readline/promises.js @@ -0,0 +1,138 @@ +// Ported from Node.js v26.3.0 lib/internal/readline/promises.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 { ArrayPrototypeJoin, ArrayPrototypePush, Promise } = primordials; + +const { CSI } = require("internal/readline/utils"); +const { validateBoolean, validateInteger } = require("internal/validators"); +const { + codes: { ERR_INVALID_ARG_TYPE }, +} = require("internal/repl/node-errors"); +// Inlined from node-shims (which eagerly loads node:{util,module,path,vm}). +const isWritable = stream => typeof stream?.write === "function"; + +const { kClearToLineBeginning, kClearToLineEnd, kClearLine, kClearScreenDown } = CSI; + +class Readline { + #autoCommit = false; + #stream; + #todo = []; + + constructor(stream, options = undefined) { + if (!isWritable(stream)) throw new ERR_INVALID_ARG_TYPE("stream", "Writable", stream); + this.#stream = stream; + if (options?.autoCommit != null) { + validateBoolean(options.autoCommit, "options.autoCommit"); + this.#autoCommit = options.autoCommit; + } + } + + /** + * Moves the cursor to the x and y coordinate on the given stream. + * @param {integer} x + * @param {integer} [y] + * @returns {Readline} this + */ + cursorTo(x, y = undefined) { + validateInteger(x, "x"); + if (y != null) validateInteger(y, "y"); + + const data = y == null ? CSI`${x + 1}G` : CSI`${y + 1};${x + 1}H`; + if (this.#autoCommit) process.nextTick(() => this.#stream.write(data)); + else ArrayPrototypePush(this.#todo, data); + + return this; + } + + /** + * Moves the cursor relative to its current location. + * @param {integer} dx + * @param {integer} dy + * @returns {Readline} this + */ + moveCursor(dx, dy) { + if (dx || dy) { + validateInteger(dx, "dx"); + validateInteger(dy, "dy"); + + 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`; + } + if (this.#autoCommit) process.nextTick(() => this.#stream.write(data)); + else ArrayPrototypePush(this.#todo, data); + } + return this; + } + + /** + * Clears the current line the cursor is on. + * @param {-1|0|1} dir Direction to clear: + * -1 for left of the cursor + * +1 for right of the cursor + * 0 for the entire line + * @returns {Readline} this + */ + clearLine(dir) { + validateInteger(dir, "dir", -1, 1); + + const data = dir < 0 ? kClearToLineBeginning : dir > 0 ? kClearToLineEnd : kClearLine; + if (this.#autoCommit) process.nextTick(() => this.#stream.write(data)); + else ArrayPrototypePush(this.#todo, data); + return this; + } + + /** + * Clears the screen from the current position of the cursor down. + * @returns {Readline} this + */ + clearScreenDown() { + if (this.#autoCommit) { + process.nextTick(() => this.#stream.write(kClearScreenDown)); + } else { + ArrayPrototypePush(this.#todo, kClearScreenDown); + } + return this; + } + + /** + * Sends all the pending actions to the associated `stream` and clears the + * internal list of pending actions. + * @returns {Promise} Resolves when all pending actions have been + * flushed to the associated `stream`. + */ + commit() { + return new Promise(resolve => { + this.#stream.write(ArrayPrototypeJoin(this.#todo, ""), resolve); + this.#todo = []; + }); + } + + /** + * Clears the internal list of pending actions without sending it to the + * associated `stream`. + * @returns {Readline} this + */ + rollback() { + this.#todo = []; + return this; + } +} + +__node_module__.exports = { + Readline, +}; + +export default __node_module__.exports; diff --git a/src/js/internal/readline/utils.js b/src/js/internal/readline/utils.js new file mode 100644 index 000000000000..55771299c8d5 --- /dev/null +++ b/src/js/internal/readline/utils.js @@ -0,0 +1,595 @@ +// Ported from Node.js v26.3.0 lib/internal/readline/utils.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 { + ArrayPrototypeToSorted, + RegExpPrototypeExec, + StringFromCharCode, + StringPrototypeCharCodeAt, + StringPrototypeCodePointAt, + StringPrototypeSlice, + StringPrototypeSplit, + StringPrototypeToLowerCase, + Symbol, +} = primordials; + +const kUTF16SurrogateThreshold = 0x10000; // 2 ** 16 +const kEscape = "\x1b"; +const kSubstringSearch = Symbol("kSubstringSearch"); + +function CSI(strings, ...args) { + let ret = `${kEscape}[`; + for (let n = 0; n < strings.length; n++) { + ret += strings[n]; + if (n < args.length) ret += args[n]; + } + return ret; +} + +CSI.kEscape = kEscape; +CSI.kClearToLineBeginning = CSI`1K`; +CSI.kClearToLineEnd = CSI`0K`; +CSI.kClearLine = CSI`2K`; +CSI.kClearScreenDown = CSI`0J`; + +// upstream-todo(BridgeAR): Treat combined characters as single character, i.e, +// 'a\u0301' and '\u0301a' (both have the same visual output). +// Check Canonical_Combining_Class in +// http://userguide.icu-project.org/strings/properties +function charLengthLeft(str, i) { + if (i <= 0) return 0; + if ( + (i > 1 && StringPrototypeCodePointAt(str, i - 2) >= kUTF16SurrogateThreshold) || + StringPrototypeCodePointAt(str, i - 1) >= kUTF16SurrogateThreshold + ) { + return 2; + } + return 1; +} + +function charLengthAt(str, i) { + if (str.length <= i) { + // Pretend to move to the right. This is necessary to autocomplete while + // moving to the right. + return 1; + } + return StringPrototypeCodePointAt(str, i) >= kUTF16SurrogateThreshold ? 2 : 1; +} + +/* + Some patterns seen in terminal key escape codes, derived from combos seen + at http://www.midnight-commander.org/browser/lib/tty/key.c + + ESC letter + ESC [ letter + ESC [ modifier letter + ESC [ 1 ; modifier letter + ESC [ num char + ESC [ num ; modifier char + ESC O letter + ESC O modifier letter + ESC O 1 ; modifier letter + ESC N letter + ESC [ [ num ; modifier char + ESC [ [ 1 ; modifier letter + ESC ESC [ num char + ESC ESC O letter + + - char is usually ~ but $ and ^ also happen with rxvt + - modifier is 1 + + (shift * 1) + + (left_alt * 2) + + (ctrl * 4) + + (right_alt * 8) + - two leading ESCs apparently mean the same as one leading ESC +*/ +function* emitKeys(stream) { + while (true) { + let ch = yield; + let s = ch; + let escaped = false; + const key = { + sequence: null, + name: undefined, + ctrl: false, + meta: false, + shift: false, + }; + + if (ch === kEscape) { + escaped = true; + s += ch = yield; + + if (ch === kEscape) { + s += ch = yield; + } + } + + if (escaped && (ch === "O" || ch === "[")) { + // ANSI escape sequence + let code = ch; + let modifier = 0; + + if (ch === "O") { + // ESC O letter + // ESC O modifier letter + s += ch = yield; + + if (ch >= "0" && ch <= "9") { + modifier = (ch >> 0) - 1; + s += ch = yield; + } + + code += ch; + } else if (ch === "[") { + // ESC [ letter + // ESC [ modifier letter + // ESC [ [ modifier letter + // ESC [ [ num char + s += ch = yield; + + if (ch === "[") { + // \x1b[[A + // ^--- escape codes might have a second bracket + code += ch; + s += ch = yield; + } + + /* + * Here and later we try to buffer just enough data to get + * a complete ascii sequence. + * + * We have basically two classes of ascii characters to process: + * + * + * 1. `\x1b[24;5~` should be parsed as { code: '[24~', modifier: 5 } + * + * This particular example is featuring Ctrl+F12 in xterm. + * + * - `;5` part is optional, e.g. it could be `\x1b[24~` + * - first part can contain one or two digits + * - there is also special case when there can be 3 digits + * but without modifier. They are the case of paste bracket mode + * + * So the generic regexp is like /^(?:\d\d?(;\d)?[~^$]|\d{3}~)$/ + * + * + * 2. `\x1b[1;5H` should be parsed as { code: '[H', modifier: 5 } + * + * This particular example is featuring Ctrl+Home in xterm. + * + * - `1;5` part is optional, e.g. it could be `\x1b[H` + * - `1;` part is optional, e.g. it could be `\x1b[5H` + * + * So the generic regexp is like /^((\d;)?\d)?[A-Za-z]$/ + * + */ + const cmdStart = s.length - 1; + + // Skip one or two leading digits + if (ch >= "0" && ch <= "9") { + s += ch = yield; + + if (ch >= "0" && ch <= "9") { + s += ch = yield; + + if (ch >= "0" && ch <= "9") { + s += ch = yield; + } + } + } + + // skip modifier + if (ch === ";") { + s += ch = yield; + + if (ch >= "0" && ch <= "9") { + s += yield; + } + } + + /* + * We buffered enough data, now trying to extract code + * and modifier from it + */ + const cmd = StringPrototypeSlice(s, cmdStart); + let match; + + if ((match = RegExpPrototypeExec(/^(?:(\d\d?)(?:;(\d))?([~^$])|(\d{3}~))$/, cmd))) { + if (match[4]) { + code += match[4]; + } else { + code += match[1] + match[3]; + modifier = (match[2] || 1) - 1; + } + } else if ((match = RegExpPrototypeExec(/^((\d;)?(\d))?([A-Za-z])$/, cmd))) { + code += match[4]; + modifier = (match[3] || 1) - 1; + } else { + code += cmd; + } + } + + // Parse the key modifier + key.ctrl = !!(modifier & 4); + key.meta = !!(modifier & 10); + key.shift = !!(modifier & 1); + key.code = code; + + // Parse the key itself + switch (code) { + /* xterm/gnome ESC [ letter (with modifier) */ + case "[P": + key.name = "f1"; + break; + case "[Q": + key.name = "f2"; + break; + case "[R": + key.name = "f3"; + break; + case "[S": + key.name = "f4"; + break; + + /* xterm/gnome ESC O letter (without modifier) */ + case "OP": + key.name = "f1"; + break; + case "OQ": + key.name = "f2"; + break; + case "OR": + key.name = "f3"; + break; + case "OS": + key.name = "f4"; + break; + + /* xterm/rxvt ESC [ number ~ */ + case "[11~": + key.name = "f1"; + break; + case "[12~": + key.name = "f2"; + break; + case "[13~": + key.name = "f3"; + break; + case "[14~": + key.name = "f4"; + break; + + /* paste bracket mode */ + case "[200~": + key.name = "paste-start"; + break; + case "[201~": + key.name = "paste-end"; + break; + + /* from Cygwin and used in libuv */ + case "[[A": + key.name = "f1"; + break; + case "[[B": + key.name = "f2"; + break; + case "[[C": + key.name = "f3"; + break; + case "[[D": + key.name = "f4"; + break; + case "[[E": + key.name = "f5"; + break; + + /* common */ + case "[15~": + key.name = "f5"; + break; + case "[17~": + key.name = "f6"; + break; + case "[18~": + key.name = "f7"; + break; + case "[19~": + key.name = "f8"; + break; + case "[20~": + key.name = "f9"; + break; + case "[21~": + key.name = "f10"; + break; + case "[23~": + key.name = "f11"; + break; + case "[24~": + key.name = "f12"; + break; + + /* xterm ESC [ letter */ + case "[A": + key.name = "up"; + break; + case "[B": + key.name = "down"; + break; + case "[C": + key.name = "right"; + break; + case "[D": + key.name = "left"; + break; + case "[E": + key.name = "clear"; + break; + case "[F": + key.name = "end"; + break; + case "[H": + key.name = "home"; + break; + + /* xterm/gnome ESC O letter */ + case "OA": + key.name = "up"; + break; + case "OB": + key.name = "down"; + break; + case "OC": + key.name = "right"; + break; + case "OD": + key.name = "left"; + break; + case "OE": + key.name = "clear"; + break; + case "OF": + key.name = "end"; + break; + case "OH": + key.name = "home"; + break; + + /* xterm/rxvt ESC [ number ~ */ + case "[1~": + key.name = "home"; + break; + case "[2~": + key.name = "insert"; + break; + case "[3~": + key.name = "delete"; + break; + case "[4~": + key.name = "end"; + break; + case "[5~": + key.name = "pageup"; + break; + case "[6~": + key.name = "pagedown"; + break; + + /* putty */ + case "[[5~": + key.name = "pageup"; + break; + case "[[6~": + key.name = "pagedown"; + break; + + /* rxvt */ + case "[7~": + key.name = "home"; + break; + case "[8~": + key.name = "end"; + break; + + /* rxvt keys with modifiers */ + case "[a": + key.name = "up"; + key.shift = true; + break; + case "[b": + key.name = "down"; + key.shift = true; + break; + case "[c": + key.name = "right"; + key.shift = true; + break; + case "[d": + key.name = "left"; + key.shift = true; + break; + case "[e": + key.name = "clear"; + key.shift = true; + break; + + case "[2$": + key.name = "insert"; + key.shift = true; + break; + case "[3$": + key.name = "delete"; + key.shift = true; + break; + case "[5$": + key.name = "pageup"; + key.shift = true; + break; + case "[6$": + key.name = "pagedown"; + key.shift = true; + break; + case "[7$": + key.name = "home"; + key.shift = true; + break; + case "[8$": + key.name = "end"; + key.shift = true; + break; + + case "Oa": + key.name = "up"; + key.ctrl = true; + break; + case "Ob": + key.name = "down"; + key.ctrl = true; + break; + case "Oc": + key.name = "right"; + key.ctrl = true; + break; + case "Od": + key.name = "left"; + key.ctrl = true; + break; + case "Oe": + key.name = "clear"; + key.ctrl = true; + break; + + case "[2^": + key.name = "insert"; + key.ctrl = true; + break; + case "[3^": + key.name = "delete"; + key.ctrl = true; + break; + case "[5^": + key.name = "pageup"; + key.ctrl = true; + break; + case "[6^": + key.name = "pagedown"; + key.ctrl = true; + break; + case "[7^": + key.name = "home"; + key.ctrl = true; + break; + case "[8^": + key.name = "end"; + key.ctrl = true; + break; + + /* misc. */ + case "[Z": + key.name = "tab"; + key.shift = true; + break; + default: + key.name = "undefined"; + break; + } + } else if (ch === "\r") { + // carriage return + key.name = "return"; + key.meta = escaped; + } else if (ch === "\n") { + // Enter, should have been called linefeed + key.name = "enter"; + key.meta = escaped; + } else if (ch === "\t") { + // tab + key.name = "tab"; + key.meta = escaped; + } else if (ch === "\b" || ch === "\x7f") { + // backspace or ctrl+h + key.name = "backspace"; + key.meta = escaped; + } else if (ch === kEscape) { + // escape key + key.name = "escape"; + key.meta = escaped; + } else if (ch === " ") { + key.name = "space"; + key.meta = escaped; + } else if (!escaped && ch <= "\x1a") { + // ctrl+letter + key.name = StringFromCharCode(StringPrototypeCharCodeAt(ch) + StringPrototypeCharCodeAt("a") - 1); + key.ctrl = true; + } else if (RegExpPrototypeExec(/^[0-9A-Za-z]$/, ch) !== null) { + // Letter, number, shift+letter + key.name = StringPrototypeToLowerCase(ch); + key.shift = RegExpPrototypeExec(/^[A-Z]$/, ch) !== null; + key.meta = escaped; + } else if (escaped) { + // Escape sequence timeout + key.name = ch.length ? undefined : "escape"; + key.meta = true; + } + + key.sequence = s; + + if (s.length !== 0 && (key.name !== undefined || escaped)) { + /* Named character or sequence */ + stream.emit("keypress", escaped ? undefined : s, key); + } else if (charLengthAt(s, 0) === s.length) { + /* Single unnamed character, e.g. "." */ + stream.emit("keypress", s, key); + } + /* Unrecognized or broken escape sequence, don't emit anything */ + } +} + +// This runs in O(n log n). +function commonPrefix(strings) { + if (strings.length === 0) { + return ""; + } + if (strings.length === 1) { + return strings[0]; + } + const sorted = ArrayPrototypeToSorted(strings); + const min = sorted[0]; + const max = sorted[sorted.length - 1]; + for (let i = 0; i < min.length; i++) { + if (min[i] !== max[i]) { + return StringPrototypeSlice(min, 0, i); + } + } + return min; +} + +function reverseString(line, from = "\r", to = "\r") { + const parts = StringPrototypeSplit(line, from); + + // This implementation should be faster than + // ArrayPrototypeJoin(ArrayPrototypeReverse(StringPrototypeSplit(line, from)), to); + let result = ""; + for (let i = parts.length - 1; i > 0; i--) { + result += parts[i] + to; + } + result += parts[0]; + + return result; +} + +__node_module__.exports = { + charLengthAt, + charLengthLeft, + commonPrefix, + emitKeys, + reverseString, + kSubstringSearch, + CSI, +}; + +export default __node_module__.exports; diff --git a/src/js/internal/repl.js b/src/js/internal/repl.js new file mode 100644 index 000000000000..782cb30b98fc --- /dev/null +++ b/src/js/internal/repl.js @@ -0,0 +1,62 @@ +// Ported from Node.js v26.3.0 lib/internal/repl.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 { Number, NumberIsNaN, NumberParseInt } = primordials; + +const REPL = require("node:repl"); +const { kStandaloneREPL } = require("internal/repl/utils"); + +__node_module__.exports = { __proto__: REPL }; +__node_module__.exports.createInternalRepl = createRepl; + +function createRepl(env, opts, cb) { + if (typeof opts === "function") { + cb = opts; + opts = null; + } + opts = { + [kStandaloneREPL]: true, + ignoreUndefined: false, + useGlobal: true, + breakEvalOnSigint: true, + ...opts, + }; + + if (NumberParseInt(env.NODE_NO_READLINE)) { + opts.terminal = false; + } + + if (env.NODE_REPL_MODE) { + opts.replMode = { + "strict": REPL.REPL_MODE_STRICT, + "sloppy": REPL.REPL_MODE_SLOPPY, + }[env.NODE_REPL_MODE.toLowerCase().trim()]; + } + + if (opts.replMode === undefined) { + opts.replMode = REPL.REPL_MODE_SLOPPY; + } + + const size = Number(env.NODE_REPL_HISTORY_SIZE); + if (!NumberIsNaN(size) && size > 0) { + opts.size = size; + } else { + opts.size = 1000; + } + + const term = "terminal" in opts ? opts.terminal : process.stdout.isTTY; + opts.filePath = term ? env.NODE_REPL_HISTORY : ""; + + const repl = REPL.start(opts); + + repl.setupHistory({ + filePath: opts.filePath, + size: opts.size, + onHistoryFileLoaded: cb, + }); +} + +export default __node_module__.exports; diff --git a/src/js/internal/repl/acorn-walk.js b/src/js/internal/repl/acorn-walk.js new file mode 100644 index 000000000000..376a057da0c9 --- /dev/null +++ b/src/js/internal/repl/acorn-walk.js @@ -0,0 +1,33 @@ +// Vendored from Node.js v26.3.0 deps (acorn-walk.js), minified with esbuild +// to keep the embedded-source literal within compiler limits. +// +// acorn-walk — Copyright (C) 2012-2022 by various contributors (see AUTHORS) +// MIT License. https://github.com/acornjs/acorn +// +// See internal/repl/acorn.js for the vm.Script rationale. Lazy-loaded. +// prettier-ignore +let _walk; +function load() { + if (_walk) return _walk; + const exportsObj = {}; + const moduleObj = { exports: exportsObj }; + new (require("node:vm").Script)( + '(function(exports, module){(function(c,v){typeof exports=="object"&&typeof module<"u"?v(exports):typeof define=="function"&&define.amd?define(["exports"],v):(c=typeof globalThis<"u"?globalThis:c||self,v((c.acorn=c.acorn||{},c.acorn.walk={})))})(this,function(c){"use strict";function v(t,n,e,r,a){e||(e=i),function f(o,u,p){var s=p||o.type;m(e,s,o,u,f),n[s]&&n[s](o,u)}(t,r,a)}function P(t,n,e,r,a){var f=[];e||(e=i),function o(u,p,s){var l=s||u.type,h=u!==f[f.length-1];h&&f.push(u),m(e,l,u,p,o),n[l]&&n[l](u,p||f,f),h&&f.pop()}(t,r,a)}function w(t,n,e,r,a){var f=e?g(e,r||void 0):r;(function o(u,p,s){f[s||u.type](u,p,o)})(t,n,a)}function y(t){return typeof t=="string"?function(n){return n===t}:t||function(){return!0}}var E=function(n,e){this.node=n,this.state=e};function b(t,n,e,r,a){e||(e=i);var f;(function o(u,p,s){var l=s||u.type;m(e,l,u,p,o),f!==u&&(n(u,p,l),f=u)})(t,r,a)}function D(t,n,e,r){e||(e=i);var a=[],f;(function o(u,p,s){var l=s||u.type,h=u!==a[a.length-1];h&&a.push(u),m(e,l,u,p,o),f!==u&&(n(u,p||a,a,l),f=u),h&&a.pop()})(t,r)}function A(t,n,e,r,a,f){a||(a=i),r=y(r);try{(function o(u,p,s){var l=s||u.type;if((n==null||u.start<=n)&&(e==null||u.end>=e)&&m(a,l,u,p,o),(n==null||u.start===n)&&(e==null||u.end===e)&&r(l,u))throw new E(u,p)})(t,f)}catch(o){if(o instanceof E)return o;throw o}}function N(t,n,e,r,a){e=y(e),r||(r=i);try{(function f(o,u,p){var s=p||o.type;if(!(o.start>n||o.end=n&&e(s,o))throw new E(o,u);m(r,s,o,u,f)}})(t,a)}catch(f){if(f instanceof E)return f;throw f}}function F(t,n,e,r,a){e=y(e),r||(r=i);var f;return function o(u,p,s){if(!(u.start>n)){var l=s||u.type;u.end<=n&&(!f||f.node.ende)return!1;if(i+=t[s+1],i>=e)return!0}return!1}function L(e,t){return e<65?e===36:e<91?!0:e<97?e===95:e<123?!0:e<=65535?e>=170&&bt.test(String.fromCharCode(e)):t===!1?!1:ge(e,Pe)}function O(e,t){return e<48?e===36:e<58?!0:e<65?!1:e<91?!0:e<97?e===95:e<123?!0:e<=65535?e>=170&&yt.test(String.fromCharCode(e)):t===!1?!1:ge(e,Pe)||ge(e,Z)}var v=function(t,i){i===void 0&&(i={}),this.label=t,this.keyword=i.keyword,this.beforeExpr=!!i.beforeExpr,this.startsExpr=!!i.startsExpr,this.isLoop=!!i.isLoop,this.isAssign=!!i.isAssign,this.prefix=!!i.prefix,this.postfix=!!i.postfix,this.binop=i.binop||null,this.updateContext=null};function E(e,t){return new v(e,{beforeExpr:!0,binop:t})}var I={beforeExpr:!0},A={startsExpr:!0},ae={};function m(e,t){return t===void 0&&(t={}),t.keyword=e,ae[e]=new v(e,t)}var a={num:new v("num",A),regexp:new v("regexp",A),string:new v("string",A),name:new v("name",A),privateId:new v("privateId",A),eof:new v("eof"),bracketL:new v("[",{beforeExpr:!0,startsExpr:!0}),bracketR:new v("]"),braceL:new v("{",{beforeExpr:!0,startsExpr:!0}),braceR:new v("}"),parenL:new v("(",{beforeExpr:!0,startsExpr:!0}),parenR:new v(")"),comma:new v(",",I),semi:new v(";",I),colon:new v(":",I),dot:new v("."),question:new v("?",I),questionDot:new v("?."),arrow:new v("=>",I),template:new v("template"),invalidTemplate:new v("invalidTemplate"),ellipsis:new v("...",I),backQuote:new v("`",A),dollarBraceL:new v("${",{beforeExpr:!0,startsExpr:!0}),eq:new v("=",{beforeExpr:!0,isAssign:!0}),assign:new v("_=",{beforeExpr:!0,isAssign:!0}),incDec:new v("++/--",{prefix:!0,postfix:!0,startsExpr:!0}),prefix:new v("!/~",{beforeExpr:!0,prefix:!0,startsExpr:!0}),logicalOR:E("||",1),logicalAND:E("&&",2),bitwiseOR:E("|",3),bitwiseXOR:E("^",4),bitwiseAND:E("&",5),equality:E("==/!=/===/!==",6),relational:E("/<=/>=",7),bitShift:E("<>/>>>",8),plusMin:new v("+/-",{beforeExpr:!0,binop:9,prefix:!0,startsExpr:!0}),modulo:E("%",10),star:E("*",10),slash:E("/",10),starstar:new v("**",{beforeExpr:!0}),coalesce:E("??",1),_break:m("break"),_case:m("case",I),_catch:m("catch"),_continue:m("continue"),_debugger:m("debugger"),_default:m("default",I),_do:m("do",{isLoop:!0,beforeExpr:!0}),_else:m("else",I),_finally:m("finally"),_for:m("for",{isLoop:!0}),_function:m("function",A),_if:m("if"),_return:m("return",I),_switch:m("switch"),_throw:m("throw",I),_try:m("try"),_var:m("var"),_const:m("const"),_while:m("while",{isLoop:!0}),_with:m("with"),_new:m("new",{beforeExpr:!0,startsExpr:!0}),_this:m("this",A),_super:m("super",A),_class:m("class",A),_extends:m("extends",I),_export:m("export"),_import:m("import",A),_null:m("null",A),_true:m("true",A),_false:m("false",A),_in:m("in",{beforeExpr:!0,binop:7}),_instanceof:m("instanceof",{beforeExpr:!0,binop:7}),_typeof:m("typeof",{beforeExpr:!0,prefix:!0,startsExpr:!0}),_void:m("void",{beforeExpr:!0,prefix:!0,startsExpr:!0}),_delete:m("delete",{beforeExpr:!0,prefix:!0,startsExpr:!0})},k=/\\r\\n?|\\n|\\u2028|\\u2029/,Te=new RegExp(k.source,"g");function q(e){return e===10||e===13||e===8232||e===8233}function Le(e,t,i){i===void 0&&(i=e.length);for(var s=t;s>10)+55296,(e&1023)+56320))}var St=/(?:[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF])/,z=function(t,i){this.line=t,this.column=i};z.prototype.offset=function(t){return new z(this.line,this.column+t)};var J=function(t,i,s){this.start=i,this.end=s,t.sourceFile!==null&&(this.source=t.sourceFile)};function ye(e,t){for(var i=1,s=0;;){var r=Le(e,s,t);if(r<0)return new z(i,t-s);++i,s=r}}var re={ecmaVersion:null,sourceType:"script",onInsertedSemicolon:null,onTrailingComma:null,allowReserved:null,allowReturnOutsideFunction:!1,allowImportExportEverywhere:!1,allowAwaitOutsideFunction:null,allowSuperOutsideMethod:null,allowHashBang:!1,checkPrivateFields:!0,locations:!1,onToken:null,onComment:null,ranges:!1,program:null,sourceFile:null,directSourceFile:null,preserveParens:!1},De=!1;function kt(e){var t={};for(var i in re)t[i]=e&&W(e,i)?e[i]:re[i];if(t.ecmaVersion==="latest"?t.ecmaVersion=1e8:t.ecmaVersion==null?(!De&&typeof console=="object"&&console.warn&&(De=!0,console.warn(`Since Acorn 8.0.0, options.ecmaVersion is required.\nDefaulting to 2020, but this will stop working in the future.`)),t.ecmaVersion=11):t.ecmaVersion>=2015&&(t.ecmaVersion-=2009),t.allowReserved==null&&(t.allowReserved=t.ecmaVersion<5),(!e||e.allowHashBang==null)&&(t.allowHashBang=t.ecmaVersion>=14),Oe(t.onToken)){var s=t.onToken;t.onToken=function(r){return s.push(r)}}if(Oe(t.onComment)&&(t.onComment=wt(t,t.onComment)),t.sourceType==="commonjs"&&t.allowAwaitOutsideFunction)throw new Error("Cannot use allowAwaitOutsideFunction with sourceType: commonjs");return t}function wt(e,t){return function(i,s,r,n,u,o){var h={type:i?"Block":"Line",value:s,start:r,end:n};e.locations&&(h.loc=new J(this,u,o)),e.ranges&&(h.range=[r,n]),t.push(h)}}var j=1,G=2,Ce=4,Fe=8,_e=16,Me=32,ne=64,Ue=128,H=256,$=512,qe=1024,ue=j|G|H;function Se(e,t){return G|(e?Ce:0)|(t?Fe:0)}var oe=0,ke=1,D=2,je=3,Ge=4,He=5,C=function(t,i,s){this.options=t=kt(t),this.sourceFile=t.sourceFile,this.keywords=M(gt[t.ecmaVersion>=6?6:t.sourceType==="module"?"5module":5]);var r="";t.allowReserved!==!0&&(r=me[t.ecmaVersion>=6?6:t.ecmaVersion===5?5:3],t.sourceType==="module"&&(r+=" await")),this.reservedWords=M(r);var n=(r?r+" ":"")+me.strict;this.reservedWordsStrict=M(n),this.reservedWordsStrictBind=M(n+" "+me.strictBind),this.input=String(i),this.containsEsc=!1,s?(this.pos=s,this.lineStart=this.input.lastIndexOf(`\n`,s-1)+1,this.curLine=this.input.slice(0,this.lineStart).split(k).length):(this.pos=this.lineStart=0,this.curLine=1),this.type=a.eof,this.value=null,this.start=this.end=this.pos,this.startLoc=this.endLoc=this.curPosition(),this.lastTokEndLoc=this.lastTokStartLoc=null,this.lastTokStart=this.lastTokEnd=this.pos,this.context=this.initialContext(),this.exprAllowed=!0,this.inModule=t.sourceType==="module",this.strict=this.inModule||this.strictDirective(this.pos),this.potentialArrowAt=-1,this.potentialArrowInForAwait=!1,this.yieldPos=this.awaitPos=this.awaitIdentPos=0,this.labels=[],this.undefinedExports=Object.create(null),this.pos===0&&t.allowHashBang&&this.input.slice(0,2)==="#!"&&this.skipLineComment(2),this.scopeStack=[],this.enterScope(this.options.sourceType==="commonjs"?G:j),this.regexpState=null,this.privateNameStack=[]},P={inFunction:{configurable:!0},inGenerator:{configurable:!0},inAsync:{configurable:!0},canAwait:{configurable:!0},allowReturn:{configurable:!0},allowSuper:{configurable:!0},allowDirectSuper:{configurable:!0},treatFunctionsAsVar:{configurable:!0},allowNewDotTarget:{configurable:!0},allowUsing:{configurable:!0},inClassStaticBlock:{configurable:!0}};C.prototype.parse=function(){var t=this.options.program||this.startNode();return this.nextToken(),this.parseTopLevel(t)},P.inFunction.get=function(){return(this.currentVarScope().flags&G)>0},P.inGenerator.get=function(){return(this.currentVarScope().flags&Fe)>0},P.inAsync.get=function(){return(this.currentVarScope().flags&Ce)>0},P.canAwait.get=function(){for(var e=this.scopeStack.length-1;e>=0;e--){var t=this.scopeStack[e],i=t.flags;if(i&(H|$))return!1;if(i&G)return(i&Ce)>0}return this.inModule&&this.options.ecmaVersion>=13||this.options.allowAwaitOutsideFunction},P.allowReturn.get=function(){return!!(this.inFunction||this.options.allowReturnOutsideFunction&&this.currentVarScope().flags&j)},P.allowSuper.get=function(){var e=this.currentThisScope(),t=e.flags;return(t&ne)>0||this.options.allowSuperOutsideMethod},P.allowDirectSuper.get=function(){return(this.currentThisScope().flags&Ue)>0},P.treatFunctionsAsVar.get=function(){return this.treatFunctionsAsVarInScope(this.currentScope())},P.allowNewDotTarget.get=function(){for(var e=this.scopeStack.length-1;e>=0;e--){var t=this.scopeStack[e],i=t.flags;if(i&(H|$)||i&G&&!(i&_e))return!0}return!1},P.allowUsing.get=function(){var e=this.currentScope(),t=e.flags;return!(t&qe||!this.inModule&&t&j)},P.inClassStaticBlock.get=function(){return(this.currentVarScope().flags&H)>0},C.extend=function(){for(var t=[],i=arguments.length;i--;)t[i]=arguments[i];for(var s=this,r=0;r=,?^&]/.test(r)||r==="!"&&this.input.charAt(s+1)==="=")}e+=t[0].length,_.lastIndex=e,e+=_.exec(this.input)[0].length,this.input[e]===";"&&e++}},w.eat=function(e){return this.type===e?(this.next(),!0):!1},w.isContextual=function(e){return this.type===a.name&&this.value===e&&!this.containsEsc},w.eatContextual=function(e){return this.isContextual(e)?(this.next(),!0):!1},w.expectContextual=function(e){this.eatContextual(e)||this.unexpected()},w.canInsertSemicolon=function(){return this.type===a.eof||this.type===a.braceR||k.test(this.input.slice(this.lastTokEnd,this.start))},w.insertSemicolon=function(){if(this.canInsertSemicolon())return this.options.onInsertedSemicolon&&this.options.onInsertedSemicolon(this.lastTokEnd,this.lastTokEndLoc),!0},w.semicolon=function(){!this.eat(a.semi)&&!this.insertSemicolon()&&this.unexpected()},w.afterTrailingComma=function(e,t){if(this.type===e)return this.options.onTrailingComma&&this.options.onTrailingComma(this.lastTokStart,this.lastTokStartLoc),t||this.next(),!0},w.expect=function(e){this.eat(e)||this.unexpected()},w.unexpected=function(e){this.raise(e??this.start,"Unexpected token")};var he=function(){this.shorthandAssign=this.trailingComma=this.parenthesizedAssign=this.parenthesizedBind=this.doubleProto=-1};w.checkPatternErrors=function(e,t){if(e){e.trailingComma>-1&&this.raiseRecoverable(e.trailingComma,"Comma is not permitted after the rest element");var i=t?e.parenthesizedAssign:e.parenthesizedBind;i>-1&&this.raiseRecoverable(i,t?"Assigning to rvalue":"Parenthesized pattern")}},w.checkExpressionErrors=function(e,t){if(!e)return!1;var i=e.shorthandAssign,s=e.doubleProto;if(!t)return i>=0||s>=0;i>=0&&this.raise(i,"Shorthand property assignments are valid only in destructuring patterns"),s>=0&&this.raiseRecoverable(s,"Redefinition of __proto__ property")},w.checkYieldAwaitInDefaultParams=function(){this.yieldPos&&(!this.awaitPos||this.yieldPos=6&&this.unexpected(),this.parseFunctionStatement(r,!1,!e);case a._class:return e&&this.unexpected(),this.parseClass(r,!0);case a._if:return this.parseIfStatement(r);case a._return:return this.parseReturnStatement(r);case a._switch:return this.parseSwitchStatement(r);case a._throw:return this.parseThrowStatement(r);case a._try:return this.parseTryStatement(r);case a._const:case a._var:return n=n||this.value,e&&n!=="var"&&this.unexpected(),this.parseVarStatement(r,n);case a._while:return this.parseWhileStatement(r);case a._with:return this.parseWithStatement(r);case a.braceL:return this.parseBlock(!0,r);case a.semi:return this.parseEmptyStatement(r);case a._export:case a._import:if(this.options.ecmaVersion>10&&s===a._import){_.lastIndex=this.pos;var u=_.exec(this.input),o=this.pos+u[0].length,h=this.input.charCodeAt(o);if(h===40||h===46)return this.parseExpressionStatement(r,this.parseExpression())}return this.options.allowImportExportEverywhere||(t||this.raise(this.start,"\'import\' and \'export\' may only appear at the top level"),this.inModule||this.raise(this.start,"\'import\' and \'export\' may appear only with \'sourceType: module\'")),s===a._import?this.parseImport(r):this.parseExport(r,i);default:if(this.isAsyncFunction())return e&&this.unexpected(),this.next(),this.parseFunctionStatement(r,!0,!e);var p=this.isAwaitUsing(!1)?"await using":this.isUsing(!1)?"using":null;if(p)return this.allowUsing||this.raise(this.start,"Using declaration cannot appear in the top level when source type is `script` or in the bare case statement"),p==="await using"&&(this.canAwait||this.raise(this.start,"Await using cannot appear outside of async function"),this.next()),this.next(),this.parseVar(r,!1,p),this.semicolon(),this.finishNode(r,"VariableDeclaration");var d=this.value,y=this.parseExpression();return s===a.name&&y.type==="Identifier"&&this.eat(a.colon)?this.parseLabeledStatement(r,d,y,e):this.parseExpressionStatement(r,y)}},l.parseBreakContinueStatement=function(e,t){var i=t==="break";this.next(),this.eat(a.semi)||this.insertSemicolon()?e.label=null:this.type!==a.name?this.unexpected():(e.label=this.parseIdent(),this.semicolon());for(var s=0;s=6?this.eat(a.semi):this.semicolon(),this.finishNode(e,"DoWhileStatement")},l.parseForStatement=function(e){this.next();var t=this.options.ecmaVersion>=9&&this.canAwait&&this.eatContextual("await")?this.lastTokStart:-1;if(this.labels.push(we),this.enterScope(0),this.expect(a.parenL),this.type===a.semi)return t>-1&&this.unexpected(t),this.parseFor(e,null);var i=this.isLet();if(this.type===a._var||this.type===a._const||i){var s=this.startNode(),r=i?"let":this.value;return this.next(),this.parseVar(s,!0,r),this.finishNode(s,"VariableDeclaration"),this.parseForAfterInit(e,s,t)}var n=this.isContextual("let"),u=!1,o=this.isUsing(!0)?"using":this.isAwaitUsing(!0)?"await using":null;if(o){var h=this.startNode();return this.next(),o==="await using"&&(this.canAwait||this.raise(this.start,"Await using cannot appear outside of async function"),this.next()),this.parseVar(h,!0,o),this.finishNode(h,"VariableDeclaration"),this.parseForAfterInit(e,h,t)}var p=this.containsEsc,d=new he,y=this.start,S=t>-1?this.parseExprSubscripts(d,"await"):this.parseExpression(!0,d);return this.type===a._in||(u=this.options.ecmaVersion>=6&&this.isContextual("of"))?(t>-1?(this.type===a._in&&this.unexpected(t),e.await=!0):u&&this.options.ecmaVersion>=8&&(S.start===y&&!p&&S.type==="Identifier"&&S.name==="async"?this.unexpected():this.options.ecmaVersion>=9&&(e.await=!1)),n&&u&&this.raise(S.start,"The left-hand side of a for-of loop may not start with \'let\'."),this.toAssignable(S,!1,d),this.checkLValPattern(S),this.parseForIn(e,S)):(this.checkExpressionErrors(d,!0),t>-1&&this.unexpected(t),this.parseFor(e,S))},l.parseForAfterInit=function(e,t,i){return(this.type===a._in||this.options.ecmaVersion>=6&&this.isContextual("of"))&&t.declarations.length===1?(this.options.ecmaVersion>=9&&(this.type===a._in?i>-1&&this.unexpected(i):e.await=i>-1),this.parseForIn(e,t)):(i>-1&&this.unexpected(i),this.parseFor(e,t))},l.parseFunctionStatement=function(e,t,i){return this.next(),this.parseFunction(e,ee|(i?0:Ae),!1,t)},l.parseIfStatement=function(e){return this.next(),e.test=this.parseParenExpression(),e.consequent=this.parseStatement("if"),e.alternate=this.eat(a._else)?this.parseStatement("if"):null,this.finishNode(e,"IfStatement")},l.parseReturnStatement=function(e){return this.allowReturn||this.raise(this.start,"\'return\' outside of function"),this.next(),this.eat(a.semi)||this.insertSemicolon()?e.argument=null:(e.argument=this.parseExpression(),this.semicolon()),this.finishNode(e,"ReturnStatement")},l.parseSwitchStatement=function(e){this.next(),e.discriminant=this.parseParenExpression(),e.cases=[],this.expect(a.braceL),this.labels.push(Et),this.enterScope(qe);for(var t,i=!1;this.type!==a.braceR;)if(this.type===a._case||this.type===a._default){var s=this.type===a._case;t&&this.finishNode(t,"SwitchCase"),e.cases.push(t=this.startNode()),t.consequent=[],this.next(),s?t.test=this.parseExpression():(i&&this.raiseRecoverable(this.lastTokStart,"Multiple default clauses"),i=!0,t.test=null),this.expect(a.colon)}else t||this.unexpected(),t.consequent.push(this.parseStatement(null));return this.exitScope(),t&&this.finishNode(t,"SwitchCase"),this.next(),this.labels.pop(),this.finishNode(e,"SwitchStatement")},l.parseThrowStatement=function(e){return this.next(),k.test(this.input.slice(this.lastTokEnd,this.start))&&this.raise(this.lastTokEnd,"Illegal newline after throw"),e.argument=this.parseExpression(),this.semicolon(),this.finishNode(e,"ThrowStatement")};var It=[];l.parseCatchClauseParam=function(){var e=this.parseBindingAtom(),t=e.type==="Identifier";return this.enterScope(t?Me:0),this.checkLValPattern(e,t?Ge:D),this.expect(a.parenR),e},l.parseTryStatement=function(e){if(this.next(),e.block=this.parseBlock(),e.handler=null,this.type===a._catch){var t=this.startNode();this.next(),this.eat(a.parenL)?t.param=this.parseCatchClauseParam():(this.options.ecmaVersion<10&&this.unexpected(),t.param=null,this.enterScope(0)),t.body=this.parseBlock(!1),this.exitScope(),e.handler=this.finishNode(t,"CatchClause")}return e.finalizer=this.eat(a._finally)?this.parseBlock():null,!e.handler&&!e.finalizer&&this.raise(e.start,"Missing catch or finally clause"),this.finishNode(e,"TryStatement")},l.parseVarStatement=function(e,t,i){return this.next(),this.parseVar(e,!1,t,i),this.semicolon(),this.finishNode(e,"VariableDeclaration")},l.parseWhileStatement=function(e){return this.next(),e.test=this.parseParenExpression(),this.labels.push(we),e.body=this.parseStatement("while"),this.labels.pop(),this.finishNode(e,"WhileStatement")},l.parseWithStatement=function(e){return this.strict&&this.raise(this.start,"\'with\' in strict mode"),this.next(),e.object=this.parseParenExpression(),e.body=this.parseStatement("with"),this.finishNode(e,"WithStatement")},l.parseEmptyStatement=function(e){return this.next(),this.finishNode(e,"EmptyStatement")},l.parseLabeledStatement=function(e,t,i,s){for(var r=0,n=this.labels;r=0;h--){var p=this.labels[h];if(p.statementStart===e.start)p.statementStart=this.start,p.kind=o;else break}return this.labels.push({name:t,kind:o,statementStart:this.start}),e.body=this.parseStatement(s?s.indexOf("label")===-1?s+"label":s:"label"),this.labels.pop(),e.label=i,this.finishNode(e,"LabeledStatement")},l.parseExpressionStatement=function(e,t){return e.expression=t,this.semicolon(),this.finishNode(e,"ExpressionStatement")},l.parseBlock=function(e,t,i){for(e===void 0&&(e=!0),t===void 0&&(t=this.startNode()),t.body=[],this.expect(a.braceL),e&&this.enterScope(0);this.type!==a.braceR;){var s=this.parseStatement(null);t.body.push(s)}return i&&(this.strict=!1),this.next(),e&&this.exitScope(),this.finishNode(t,"BlockStatement")},l.parseFor=function(e,t){return e.init=t,this.expect(a.semi),e.test=this.type===a.semi?null:this.parseExpression(),this.expect(a.semi),e.update=this.type===a.parenR?null:this.parseExpression(),this.expect(a.parenR),e.body=this.parseStatement("for"),this.exitScope(),this.labels.pop(),this.finishNode(e,"ForStatement")},l.parseForIn=function(e,t){var i=this.type===a._in;return this.next(),t.type==="VariableDeclaration"&&t.declarations[0].init!=null&&(!i||this.options.ecmaVersion<8||this.strict||t.kind!=="var"||t.declarations[0].id.type!=="Identifier")&&this.raise(t.start,(i?"for-in":"for-of")+" loop variable declaration may not have an initializer"),e.left=t,e.right=i?this.parseExpression():this.parseMaybeAssign(),this.expect(a.parenR),e.body=this.parseStatement("for"),this.exitScope(),this.labels.pop(),this.finishNode(e,i?"ForInStatement":"ForOfStatement")},l.parseVar=function(e,t,i,s){for(e.declarations=[],e.kind=i;;){var r=this.startNode();if(this.parseVarId(r,i),this.eat(a.eq)?r.init=this.parseMaybeAssign(t):!s&&i==="const"&&!(this.type===a._in||this.options.ecmaVersion>=6&&this.isContextual("of"))?this.unexpected():!s&&(i==="using"||i==="await using")&&this.options.ecmaVersion>=17&&this.type!==a._in&&!this.isContextual("of")?this.raise(this.lastTokEnd,"Missing initializer in "+i+" declaration"):!s&&r.id.type!=="Identifier"&&!(t&&(this.type===a._in||this.isContextual("of")))?this.raise(this.lastTokEnd,"Complex binding patterns require an initialization value"):r.init=null,e.declarations.push(this.finishNode(r,"VariableDeclarator")),!this.eat(a.comma))break}return e},l.parseVarId=function(e,t){e.id=t==="using"||t==="await using"?this.parseIdent():this.parseBindingAtom(),this.checkLValPattern(e.id,t==="var"?ke:D,!1)};var ee=1,Ae=2,We=4;l.parseFunction=function(e,t,i,s,r){this.initFunction(e),(this.options.ecmaVersion>=9||this.options.ecmaVersion>=6&&!s)&&(this.type===a.star&&t&Ae&&this.unexpected(),e.generator=this.eat(a.star)),this.options.ecmaVersion>=8&&(e.async=!!s),t&ee&&(e.id=t&We&&this.type!==a.name?null:this.parseIdent(),e.id&&!(t&Ae)&&this.checkLValSimple(e.id,this.strict||e.generator||e.async?this.treatFunctionsAsVar?ke:D:je));var n=this.yieldPos,u=this.awaitPos,o=this.awaitIdentPos;return this.yieldPos=0,this.awaitPos=0,this.awaitIdentPos=0,this.enterScope(Se(e.async,e.generator)),t&ee||(e.id=this.type===a.name?this.parseIdent():null),this.parseFunctionParams(e),this.parseFunctionBody(e,i,!1,r),this.yieldPos=n,this.awaitPos=u,this.awaitIdentPos=o,this.finishNode(e,t&ee?"FunctionDeclaration":"FunctionExpression")},l.parseFunctionParams=function(e){this.expect(a.parenL),e.params=this.parseBindingList(a.parenR,!1,this.options.ecmaVersion>=8),this.checkYieldAwaitInDefaultParams()},l.parseClass=function(e,t){this.next();var i=this.strict;this.strict=!0,this.parseClassId(e,t),this.parseClassSuper(e);var s=this.enterClassBody(),r=this.startNode(),n=!1;for(r.body=[],this.expect(a.braceL);this.type!==a.braceR;){var u=this.parseClassElement(e.superClass!==null);u&&(r.body.push(u),u.type==="MethodDefinition"&&u.kind==="constructor"?(n&&this.raiseRecoverable(u.start,"Duplicate constructor in the same class"),n=!0):u.key&&u.key.type==="PrivateIdentifier"&&Pt(s,u)&&this.raiseRecoverable(u.key.start,"Identifier \'#"+u.key.name+"\' has already been declared"))}return this.strict=i,this.next(),e.body=this.finishNode(r,"ClassBody"),this.exitClassBody(),this.finishNode(e,t?"ClassDeclaration":"ClassExpression")},l.parseClassElement=function(e){if(this.eat(a.semi))return null;var t=this.options.ecmaVersion,i=this.startNode(),s="",r=!1,n=!1,u="method",o=!1;if(this.eatContextual("static")){if(t>=13&&this.eat(a.braceL))return this.parseClassStaticBlock(i),i;this.isClassElementNameStart()||this.type===a.star?o=!0:s="static"}if(i.static=o,!s&&t>=8&&this.eatContextual("async")&&((this.isClassElementNameStart()||this.type===a.star)&&!this.canInsertSemicolon()?n=!0:s="async"),!s&&(t>=9||!n)&&this.eat(a.star)&&(r=!0),!s&&!n&&!r){var h=this.value;(this.eatContextual("get")||this.eatContextual("set"))&&(this.isClassElementNameStart()?u=h:s=h)}if(s?(i.computed=!1,i.key=this.startNodeAt(this.lastTokStart,this.lastTokStartLoc),i.key.name=s,this.finishNode(i.key,"Identifier")):this.parseClassElementName(i),t<13||this.type===a.parenL||u!=="method"||r||n){var p=!i.static&&ce(i,"constructor"),d=p&&e;p&&u!=="method"&&this.raise(i.key.start,"Constructor can\'t have get/set modifier"),i.kind=p?"constructor":u,this.parseClassMethod(i,r,n,d)}else this.parseClassField(i);return i},l.isClassElementNameStart=function(){return this.type===a.name||this.type===a.privateId||this.type===a.num||this.type===a.string||this.type===a.bracketL||this.type.keyword},l.parseClassElementName=function(e){this.type===a.privateId?(this.value==="constructor"&&this.raise(this.start,"Classes can\'t have an element named \'#constructor\'"),e.computed=!1,e.key=this.parsePrivateIdent()):this.parsePropertyName(e)},l.parseClassMethod=function(e,t,i,s){var r=e.key;e.kind==="constructor"?(t&&this.raise(r.start,"Constructor can\'t be a generator"),i&&this.raise(r.start,"Constructor can\'t be an async method")):e.static&&ce(e,"prototype")&&this.raise(r.start,"Classes may not have a static property named prototype");var n=e.value=this.parseMethod(t,i,s);return e.kind==="get"&&n.params.length!==0&&this.raiseRecoverable(n.start,"getter should have no params"),e.kind==="set"&&n.params.length!==1&&this.raiseRecoverable(n.start,"setter should have exactly one param"),e.kind==="set"&&n.params[0].type==="RestElement"&&this.raiseRecoverable(n.params[0].start,"Setter cannot use rest params"),this.finishNode(e,"MethodDefinition")},l.parseClassField=function(e){return ce(e,"constructor")?this.raise(e.key.start,"Classes can\'t have a field named \'constructor\'"):e.static&&ce(e,"prototype")&&this.raise(e.key.start,"Classes can\'t have a static field named \'prototype\'"),this.eat(a.eq)?(this.enterScope($|ne),e.value=this.parseMaybeAssign(),this.exitScope()):e.value=null,this.semicolon(),this.finishNode(e,"PropertyDefinition")},l.parseClassStaticBlock=function(e){e.body=[];var t=this.labels;for(this.labels=[],this.enterScope(H|ne);this.type!==a.braceR;){var i=this.parseStatement(null);e.body.push(i)}return this.next(),this.exitScope(),this.labels=t,this.finishNode(e,"StaticBlock")},l.parseClassId=function(e,t){this.type===a.name?(e.id=this.parseIdent(),t&&this.checkLValSimple(e.id,D,!1)):(t===!0&&this.unexpected(),e.id=null)},l.parseClassSuper=function(e){e.superClass=this.eat(a._extends)?this.parseExprSubscripts(null,!1):null},l.enterClassBody=function(){var e={declared:Object.create(null),used:[]};return this.privateNameStack.push(e),e.declared},l.exitClassBody=function(){var e=this.privateNameStack.pop(),t=e.declared,i=e.used;if(this.options.checkPrivateFields)for(var s=this.privateNameStack.length,r=s===0?null:this.privateNameStack[s-1],n=0;n=11&&(this.eatContextual("as")?(e.exported=this.parseModuleExportName(),this.checkExport(t,e.exported,this.lastTokStart)):e.exported=null),this.expectContextual("from"),this.type!==a.string&&this.unexpected(),e.source=this.parseExprAtom(),this.options.ecmaVersion>=16&&(e.attributes=this.parseWithClause()),this.semicolon(),this.finishNode(e,"ExportAllDeclaration")},l.parseExport=function(e,t){if(this.next(),this.eat(a.star))return this.parseExportAllDeclaration(e,t);if(this.eat(a._default))return this.checkExport(t,"default",this.lastTokStart),e.declaration=this.parseExportDefaultDeclaration(),this.finishNode(e,"ExportDefaultDeclaration");if(this.shouldParseExportStatement())e.declaration=this.parseExportDeclaration(e),e.declaration.type==="VariableDeclaration"?this.checkVariableExport(t,e.declaration.declarations):this.checkExport(t,e.declaration.id,e.declaration.id.start),e.specifiers=[],e.source=null,this.options.ecmaVersion>=16&&(e.attributes=[]);else{if(e.declaration=null,e.specifiers=this.parseExportSpecifiers(t),this.eatContextual("from"))this.type!==a.string&&this.unexpected(),e.source=this.parseExprAtom(),this.options.ecmaVersion>=16&&(e.attributes=this.parseWithClause());else{for(var i=0,s=e.specifiers;i=16&&(e.attributes=[])}this.semicolon()}return this.finishNode(e,"ExportNamedDeclaration")},l.parseExportDeclaration=function(e){return this.parseStatement(null)},l.parseExportDefaultDeclaration=function(){var e;if(this.type===a._function||(e=this.isAsyncFunction())){var t=this.startNode();return this.next(),e&&this.next(),this.parseFunction(t,ee|We,!1,e)}else if(this.type===a._class){var i=this.startNode();return this.parseClass(i,"nullableID")}else{var s=this.parseMaybeAssign();return this.semicolon(),s}},l.checkExport=function(e,t,i){e&&(typeof t!="string"&&(t=t.type==="Identifier"?t.name:t.value),W(e,t)&&this.raiseRecoverable(i,"Duplicate export \'"+t+"\'"),e[t]=!0)},l.checkPatternExport=function(e,t){var i=t.type;if(i==="Identifier")this.checkExport(e,t,t.start);else if(i==="ObjectPattern")for(var s=0,r=t.properties;s=16&&(e.attributes=this.parseWithClause()),this.semicolon(),this.finishNode(e,"ImportDeclaration")},l.parseImportSpecifier=function(){var e=this.startNode();return e.imported=this.parseModuleExportName(),this.eatContextual("as")?e.local=this.parseIdent():(this.checkUnreserved(e.imported),e.local=e.imported),this.checkLValSimple(e.local,D),this.finishNode(e,"ImportSpecifier")},l.parseImportDefaultSpecifier=function(){var e=this.startNode();return e.local=this.parseIdent(),this.checkLValSimple(e.local,D),this.finishNode(e,"ImportDefaultSpecifier")},l.parseImportNamespaceSpecifier=function(){var e=this.startNode();return this.next(),this.expectContextual("as"),e.local=this.parseIdent(),this.checkLValSimple(e.local,D),this.finishNode(e,"ImportNamespaceSpecifier")},l.parseImportSpecifiers=function(){var e=[],t=!0;if(this.type===a.name&&(e.push(this.parseImportDefaultSpecifier()),!this.eat(a.comma)))return e;if(this.type===a.star)return e.push(this.parseImportNamespaceSpecifier()),e;for(this.expect(a.braceL);!this.eat(a.braceR);){if(t)t=!1;else if(this.expect(a.comma),this.afterTrailingComma(a.braceR))break;e.push(this.parseImportSpecifier())}return e},l.parseWithClause=function(){var e=[];if(!this.eat(a._with))return e;this.expect(a.braceL);for(var t={},i=!0;!this.eat(a.braceR);){if(i)i=!1;else if(this.expect(a.comma),this.afterTrailingComma(a.braceR))break;var s=this.parseImportAttribute(),r=s.key.type==="Identifier"?s.key.name:s.key.value;W(t,r)&&this.raiseRecoverable(s.key.start,"Duplicate attribute key \'"+r+"\'"),t[r]=!0,e.push(s)}return e},l.parseImportAttribute=function(){var e=this.startNode();return e.key=this.type===a.string?this.parseExprAtom():this.parseIdent(this.options.allowReserved!=="never"),this.expect(a.colon),this.type!==a.string&&this.unexpected(),e.value=this.parseExprAtom(),this.finishNode(e,"ImportAttribute")},l.parseModuleExportName=function(){if(this.options.ecmaVersion>=13&&this.type===a.string){var e=this.parseLiteral(this.value);return St.test(e.value)&&this.raise(e.start,"An export name cannot include a lone surrogate."),e}return this.parseIdent(!0)},l.adaptDirectivePrologue=function(e){for(var t=0;t=5&&e.type==="ExpressionStatement"&&e.expression.type==="Literal"&&typeof e.expression.value=="string"&&(this.input[e.start]===\'"\'||this.input[e.start]==="\'")};var N=C.prototype;N.toAssignable=function(e,t,i){if(this.options.ecmaVersion>=6&&e)switch(e.type){case"Identifier":this.inAsync&&e.name==="await"&&this.raise(e.start,"Cannot use \'await\' as identifier inside an async function");break;case"ObjectPattern":case"ArrayPattern":case"AssignmentPattern":case"RestElement":break;case"ObjectExpression":e.type="ObjectPattern",i&&this.checkPatternErrors(i,!0);for(var s=0,r=e.properties;s=6)switch(this.type){case a.bracketL:var e=this.startNode();return this.next(),e.elements=this.parseBindingList(a.bracketR,!0,!0),this.finishNode(e,"ArrayPattern");case a.braceL:return this.parseObj(!0)}return this.parseIdent()},N.parseBindingList=function(e,t,i,s){for(var r=[],n=!0;!this.eat(e);)if(n?n=!1:this.expect(a.comma),t&&this.type===a.comma)r.push(null);else{if(i&&this.afterTrailingComma(e))break;if(this.type===a.ellipsis){var u=this.parseRestBinding();this.parseBindingListItem(u),r.push(u),this.type===a.comma&&this.raiseRecoverable(this.start,"Comma is not permitted after the rest element"),this.expect(e);break}else r.push(this.parseAssignableListItem(s))}return r},N.parseAssignableListItem=function(e){var t=this.parseMaybeDefault(this.start,this.startLoc);return this.parseBindingListItem(t),t},N.parseBindingListItem=function(e){return e},N.parseMaybeDefault=function(e,t,i){if(i=i||this.parseBindingAtom(),this.options.ecmaVersion<6||!this.eat(a.eq))return i;var s=this.startNodeAt(e,t);return s.left=i,s.right=this.parseMaybeAssign(),this.finishNode(s,"AssignmentPattern")},N.checkLValSimple=function(e,t,i){t===void 0&&(t=oe);var s=t!==oe;switch(e.type){case"Identifier":this.strict&&this.reservedWordsStrictBind.test(e.name)&&this.raiseRecoverable(e.start,(s?"Binding ":"Assigning to ")+e.name+" in strict mode"),s&&(t===D&&e.name==="let"&&this.raiseRecoverable(e.start,"let is disallowed as a lexically bound name"),i&&(W(i,e.name)&&this.raiseRecoverable(e.start,"Argument name clash"),i[e.name]=!0),t!==He&&this.declareName(e.name,t,e.start));break;case"ChainExpression":this.raiseRecoverable(e.start,"Optional chaining cannot appear in left-hand side");break;case"MemberExpression":s&&this.raiseRecoverable(e.start,"Binding member expression");break;case"ParenthesizedExpression":return s&&this.raiseRecoverable(e.start,"Binding parenthesized expression"),this.checkLValSimple(e.expression,t,i);default:this.raise(e.start,(s?"Binding":"Assigning to")+" rvalue")}},N.checkLValPattern=function(e,t,i){switch(t===void 0&&(t=oe),e.type){case"ObjectPattern":for(var s=0,r=e.properties;s=1;e--){var t=this.context[e];if(t.token==="function")return t.generator}return!1},K.updateContext=function(e){var t,i=this.type;i.keyword&&e===a.dot?this.exprAllowed=!1:(t=i.updateContext)?t.call(this,e):this.exprAllowed=i.beforeExpr},K.overrideContext=function(e){this.curContext()!==e&&(this.context[this.context.length-1]=e)},a.parenR.updateContext=a.braceR.updateContext=function(){if(this.context.length===1){this.exprAllowed=!0;return}var e=this.context.pop();e===b.b_stat&&this.curContext().token==="function"&&(e=this.context.pop()),this.exprAllowed=!e.isExpr},a.braceL.updateContext=function(e){this.context.push(this.braceIsBlock(e)?b.b_stat:b.b_expr),this.exprAllowed=!0},a.dollarBraceL.updateContext=function(){this.context.push(b.b_tmpl),this.exprAllowed=!0},a.parenL.updateContext=function(e){var t=e===a._if||e===a._for||e===a._with||e===a._while;this.context.push(t?b.p_stat:b.p_expr),this.exprAllowed=!0},a.incDec.updateContext=function(){},a._function.updateContext=a._class.updateContext=function(e){e.beforeExpr&&e!==a._else&&!(e===a.semi&&this.curContext()!==b.p_stat)&&!(e===a._return&&k.test(this.input.slice(this.lastTokEnd,this.start)))&&!((e===a.colon||e===a.braceL)&&this.curContext()===b.b_stat)?this.context.push(b.f_expr):this.context.push(b.f_stat),this.exprAllowed=!1},a.colon.updateContext=function(){this.curContext().token==="function"&&this.context.pop(),this.exprAllowed=!0},a.backQuote.updateContext=function(){this.curContext()===b.q_tmpl?this.context.pop():this.context.push(b.q_tmpl),this.exprAllowed=!1},a.star.updateContext=function(e){if(e===a._function){var t=this.context.length-1;this.context[t]===b.f_expr?this.context[t]=b.f_expr_gen:this.context[t]=b.f_gen}this.exprAllowed=!0},a.name.updateContext=function(e){var t=!1;this.options.ecmaVersion>=6&&e!==a.dot&&(this.value==="of"&&!this.exprAllowed||this.value==="yield"&&this.inGeneratorContext())&&(t=!0),this.exprAllowed=t};var f=C.prototype;f.checkPropClash=function(e,t,i){if(!(this.options.ecmaVersion>=9&&e.type==="SpreadElement")&&!(this.options.ecmaVersion>=6&&(e.computed||e.method||e.shorthand))){var s=e.key,r;switch(s.type){case"Identifier":r=s.name;break;case"Literal":r=String(s.value);break;default:return}var n=e.kind;if(this.options.ecmaVersion>=6){r==="__proto__"&&n==="init"&&(t.proto&&(i?i.doubleProto<0&&(i.doubleProto=s.start):this.raiseRecoverable(s.start,"Redefinition of __proto__ property")),t.proto=!0);return}r="$"+r;var u=t[r];if(u){var o;n==="init"?o=this.strict&&u.init||u.get||u.set:o=u.init||u[n],o&&this.raiseRecoverable(s.start,"Redefinition of property")}else u=t[r]={init:!1,get:!1,set:!1};u[n]=!0}},f.parseExpression=function(e,t){var i=this.start,s=this.startLoc,r=this.parseMaybeAssign(e,t);if(this.type===a.comma){var n=this.startNodeAt(i,s);for(n.expressions=[r];this.eat(a.comma);)n.expressions.push(this.parseMaybeAssign(e,t));return this.finishNode(n,"SequenceExpression")}return r},f.parseMaybeAssign=function(e,t,i){if(this.isContextual("yield")){if(this.inGenerator)return this.parseYield(e);this.exprAllowed=!1}var s=!1,r=-1,n=-1,u=-1;t?(r=t.parenthesizedAssign,n=t.trailingComma,u=t.doubleProto,t.parenthesizedAssign=t.trailingComma=-1):(t=new he,s=!0);var o=this.start,h=this.startLoc;(this.type===a.parenL||this.type===a.name)&&(this.potentialArrowAt=this.start,this.potentialArrowInForAwait=e==="await");var p=this.parseMaybeConditional(e,t);if(i&&(p=i.call(this,p,o,h)),this.type.isAssign){var d=this.startNodeAt(o,h);return d.operator=this.value,this.type===a.eq&&(p=this.toAssignable(p,!1,t)),s||(t.parenthesizedAssign=t.trailingComma=t.doubleProto=-1),t.shorthandAssign>=p.start&&(t.shorthandAssign=-1),this.type===a.eq?this.checkLValPattern(p):this.checkLValSimple(p),d.left=p,this.next(),d.right=this.parseMaybeAssign(e),u>-1&&(t.doubleProto=u),this.finishNode(d,"AssignmentExpression")}else s&&this.checkExpressionErrors(t,!0);return r>-1&&(t.parenthesizedAssign=r),n>-1&&(t.trailingComma=n),p},f.parseMaybeConditional=function(e,t){var i=this.start,s=this.startLoc,r=this.parseExprOps(e,t);if(this.checkExpressionErrors(t))return r;if(this.eat(a.question)){var n=this.startNodeAt(i,s);return n.test=r,n.consequent=this.parseMaybeAssign(),this.expect(a.colon),n.alternate=this.parseMaybeAssign(e),this.finishNode(n,"ConditionalExpression")}return r},f.parseExprOps=function(e,t){var i=this.start,s=this.startLoc,r=this.parseMaybeUnary(t,!1,!1,e);return this.checkExpressionErrors(t)||r.start===i&&r.type==="ArrowFunctionExpression"?r:this.parseExprOp(r,i,s,-1,e)},f.parseExprOp=function(e,t,i,s,r){var n=this.type.binop;if(n!=null&&(!r||this.type!==a._in)&&n>s){var u=this.type===a.logicalOR||this.type===a.logicalAND,o=this.type===a.coalesce;o&&(n=a.logicalAND.binop);var h=this.value;this.next();var p=this.start,d=this.startLoc,y=this.parseExprOp(this.parseMaybeUnary(null,!1,!1,r),p,d,n,r),S=this.buildBinary(t,i,e,y,h,u||o);return(u&&this.type===a.coalesce||o&&(this.type===a.logicalOR||this.type===a.logicalAND))&&this.raiseRecoverable(this.start,"Logical expressions and coalesce expressions cannot be mixed. Wrap either by parentheses"),this.parseExprOp(S,t,i,s,r)}return e},f.buildBinary=function(e,t,i,s,r,n){s.type==="PrivateIdentifier"&&this.raise(s.start,"Private identifier can only be left side of binary expression");var u=this.startNodeAt(e,t);return u.left=i,u.operator=r,u.right=s,this.finishNode(u,n?"LogicalExpression":"BinaryExpression")},f.parseMaybeUnary=function(e,t,i,s){var r=this.start,n=this.startLoc,u;if(this.isContextual("await")&&this.canAwait)u=this.parseAwait(s),t=!0;else if(this.type.prefix){var o=this.startNode(),h=this.type===a.incDec;o.operator=this.value,o.prefix=!0,this.next(),o.argument=this.parseMaybeUnary(null,!0,h,s),this.checkExpressionErrors(e,!0),h?this.checkLValSimple(o.argument):this.strict&&o.operator==="delete"&&ze(o.argument)?this.raiseRecoverable(o.start,"Deleting local variable in strict mode"):o.operator==="delete"&&Ee(o.argument)?this.raiseRecoverable(o.start,"Private fields can not be deleted"):t=!0,u=this.finishNode(o,h?"UpdateExpression":"UnaryExpression")}else if(!t&&this.type===a.privateId)(s||this.privateNameStack.length===0)&&this.options.checkPrivateFields&&this.unexpected(),u=this.parsePrivateIdent(),this.type!==a._in&&this.unexpected();else{if(u=this.parseExprSubscripts(e,s),this.checkExpressionErrors(e))return u;for(;this.type.postfix&&!this.canInsertSemicolon();){var p=this.startNodeAt(r,n);p.operator=this.value,p.prefix=!1,p.argument=u,this.checkLValSimple(u),this.next(),u=this.finishNode(p,"UpdateExpression")}}if(!i&&this.eat(a.starstar))if(t)this.unexpected(this.lastTokStart);else return this.buildBinary(r,n,u,this.parseMaybeUnary(null,!1,!1,s),"**",!1);else return u};function ze(e){return e.type==="Identifier"||e.type==="ParenthesizedExpression"&&ze(e.expression)}function Ee(e){return e.type==="MemberExpression"&&e.property.type==="PrivateIdentifier"||e.type==="ChainExpression"&&Ee(e.expression)||e.type==="ParenthesizedExpression"&&Ee(e.expression)}f.parseExprSubscripts=function(e,t){var i=this.start,s=this.startLoc,r=this.parseExprAtom(e,t);if(r.type==="ArrowFunctionExpression"&&this.input.slice(this.lastTokStart,this.lastTokEnd)!==")")return r;var n=this.parseSubscripts(r,i,s,!1,t);return e&&n.type==="MemberExpression"&&(e.parenthesizedAssign>=n.start&&(e.parenthesizedAssign=-1),e.parenthesizedBind>=n.start&&(e.parenthesizedBind=-1),e.trailingComma>=n.start&&(e.trailingComma=-1)),n},f.parseSubscripts=function(e,t,i,s,r){for(var n=this.options.ecmaVersion>=8&&e.type==="Identifier"&&e.name==="async"&&this.lastTokEnd===e.end&&!this.canInsertSemicolon()&&e.end-e.start===5&&this.potentialArrowAt===e.start,u=!1;;){var o=this.parseSubscript(e,t,i,s,n,u,r);if(o.optional&&(u=!0),o===e||o.type==="ArrowFunctionExpression"){if(u){var h=this.startNodeAt(t,i);h.expression=o,o=this.finishNode(h,"ChainExpression")}return o}e=o}},f.shouldParseAsyncArrow=function(){return!this.canInsertSemicolon()&&this.eat(a.arrow)},f.parseSubscriptAsyncArrow=function(e,t,i,s){return this.parseArrowExpression(this.startNodeAt(e,t),i,!0,s)},f.parseSubscript=function(e,t,i,s,r,n,u){var o=this.options.ecmaVersion>=11,h=o&&this.eat(a.questionDot);s&&h&&this.raise(this.lastTokStart,"Optional chaining cannot appear in the callee of new expressions");var p=this.eat(a.bracketL);if(p||h&&this.type!==a.parenL&&this.type!==a.backQuote||this.eat(a.dot)){var d=this.startNodeAt(t,i);d.object=e,p?(d.property=this.parseExpression(),this.expect(a.bracketR)):this.type===a.privateId&&e.type!=="Super"?d.property=this.parsePrivateIdent():d.property=this.parseIdent(this.options.allowReserved!=="never"),d.computed=!!p,o&&(d.optional=h),e=this.finishNode(d,"MemberExpression")}else if(!s&&this.eat(a.parenL)){var y=new he,S=this.yieldPos,se=this.awaitPos,Q=this.awaitIdentPos;this.yieldPos=0,this.awaitPos=0,this.awaitIdentPos=0;var xe=this.parseExprList(a.parenR,this.options.ecmaVersion>=8,!1,y);if(r&&!h&&this.shouldParseAsyncArrow())return this.checkPatternErrors(y,!1),this.checkYieldAwaitInDefaultParams(),this.awaitIdentPos>0&&this.raise(this.awaitIdentPos,"Cannot use \'await\' as identifier inside an async function"),this.yieldPos=S,this.awaitPos=se,this.awaitIdentPos=Q,this.parseSubscriptAsyncArrow(t,i,xe,u);this.checkExpressionErrors(y,!0),this.yieldPos=S||this.yieldPos,this.awaitPos=se||this.awaitPos,this.awaitIdentPos=Q||this.awaitIdentPos;var Y=this.startNodeAt(t,i);Y.callee=e,Y.arguments=xe,o&&(Y.optional=h),e=this.finishNode(Y,"CallExpression")}else if(this.type===a.backQuote){(h||n)&&this.raise(this.start,"Optional chaining cannot appear in the tag of tagged template expressions");var X=this.startNodeAt(t,i);X.tag=e,X.quasi=this.parseTemplate({isTagged:!0}),e=this.finishNode(X,"TaggedTemplateExpression")}return e},f.parseExprAtom=function(e,t,i){this.type===a.slash&&this.readRegexp();var s,r=this.potentialArrowAt===this.start;switch(this.type){case a._super:return this.allowSuper||this.raise(this.start,"\'super\' keyword outside a method"),s=this.startNode(),this.next(),this.type===a.parenL&&!this.allowDirectSuper&&this.raise(s.start,"super() call outside constructor of a subclass"),this.type!==a.dot&&this.type!==a.bracketL&&this.type!==a.parenL&&this.unexpected(),this.finishNode(s,"Super");case a._this:return s=this.startNode(),this.next(),this.finishNode(s,"ThisExpression");case a.name:var n=this.start,u=this.startLoc,o=this.containsEsc,h=this.parseIdent(!1);if(this.options.ecmaVersion>=8&&!o&&h.name==="async"&&!this.canInsertSemicolon()&&this.eat(a._function))return this.overrideContext(b.f_expr),this.parseFunction(this.startNodeAt(n,u),0,!1,!0,t);if(r&&!this.canInsertSemicolon()){if(this.eat(a.arrow))return this.parseArrowExpression(this.startNodeAt(n,u),[h],!1,t);if(this.options.ecmaVersion>=8&&h.name==="async"&&this.type===a.name&&!o&&(!this.potentialArrowInForAwait||this.value!=="of"||this.containsEsc))return h=this.parseIdent(!1),(this.canInsertSemicolon()||!this.eat(a.arrow))&&this.unexpected(),this.parseArrowExpression(this.startNodeAt(n,u),[h],!0,t)}return h;case a.regexp:var p=this.value;return s=this.parseLiteral(p.value),s.regex={pattern:p.pattern,flags:p.flags},s;case a.num:case a.string:return this.parseLiteral(this.value);case a._null:case a._true:case a._false:return s=this.startNode(),s.value=this.type===a._null?null:this.type===a._true,s.raw=this.type.keyword,this.next(),this.finishNode(s,"Literal");case a.parenL:var d=this.start,y=this.parseParenAndDistinguishExpression(r,t);return e&&(e.parenthesizedAssign<0&&!this.isSimpleAssignTarget(y)&&(e.parenthesizedAssign=d),e.parenthesizedBind<0&&(e.parenthesizedBind=d)),y;case a.bracketL:return s=this.startNode(),this.next(),s.elements=this.parseExprList(a.bracketR,!0,!0,e),this.finishNode(s,"ArrayExpression");case a.braceL:return this.overrideContext(b.b_expr),this.parseObj(!1,e);case a._function:return s=this.startNode(),this.next(),this.parseFunction(s,0);case a._class:return this.parseClass(this.startNode(),!1);case a._new:return this.parseNew();case a.backQuote:return this.parseTemplate();case a._import:return this.options.ecmaVersion>=11?this.parseExprImport(i):this.unexpected();default:return this.parseExprAtomDefault()}},f.parseExprAtomDefault=function(){this.unexpected()},f.parseExprImport=function(e){var t=this.startNode();if(this.containsEsc&&this.raiseRecoverable(this.start,"Escape sequence in keyword import"),this.next(),this.type===a.parenL&&!e)return this.parseDynamicImport(t);if(this.type===a.dot){var i=this.startNodeAt(t.start,t.loc&&t.loc.start);return i.name="import",t.meta=this.finishNode(i,"Identifier"),this.parseImportMeta(t)}else this.unexpected()},f.parseDynamicImport=function(e){if(this.next(),e.source=this.parseMaybeAssign(),this.options.ecmaVersion>=16)this.eat(a.parenR)?e.options=null:(this.expect(a.comma),this.afterTrailingComma(a.parenR)?e.options=null:(e.options=this.parseMaybeAssign(),this.eat(a.parenR)||(this.expect(a.comma),this.afterTrailingComma(a.parenR)||this.unexpected())));else if(!this.eat(a.parenR)){var t=this.start;this.eat(a.comma)&&this.eat(a.parenR)?this.raiseRecoverable(t,"Trailing comma is not allowed in import()"):this.unexpected(t)}return this.finishNode(e,"ImportExpression")},f.parseImportMeta=function(e){this.next();var t=this.containsEsc;return e.property=this.parseIdent(!0),e.property.name!=="meta"&&this.raiseRecoverable(e.property.start,"The only valid meta property for import is \'import.meta\'"),t&&this.raiseRecoverable(e.start,"\'import.meta\' must not contain escaped characters"),this.options.sourceType!=="module"&&!this.options.allowImportExportEverywhere&&this.raiseRecoverable(e.start,"Cannot use \'import.meta\' outside a module"),this.finishNode(e,"MetaProperty")},f.parseLiteral=function(e){var t=this.startNode();return t.value=e,t.raw=this.input.slice(this.start,this.end),t.raw.charCodeAt(t.raw.length-1)===110&&(t.bigint=t.value!=null?t.value.toString():t.raw.slice(0,-1).replace(/_/g,"")),this.next(),this.finishNode(t,"Literal")},f.parseParenExpression=function(){this.expect(a.parenL);var e=this.parseExpression();return this.expect(a.parenR),e},f.shouldParseArrow=function(e){return!this.canInsertSemicolon()},f.parseParenAndDistinguishExpression=function(e,t){var i=this.start,s=this.startLoc,r,n=this.options.ecmaVersion>=8;if(this.options.ecmaVersion>=6){this.next();var u=this.start,o=this.startLoc,h=[],p=!0,d=!1,y=new he,S=this.yieldPos,se=this.awaitPos,Q;for(this.yieldPos=0,this.awaitPos=0;this.type!==a.parenR;)if(p?p=!1:this.expect(a.comma),n&&this.afterTrailingComma(a.parenR,!0)){d=!0;break}else if(this.type===a.ellipsis){Q=this.start,h.push(this.parseParenItem(this.parseRestBinding())),this.type===a.comma&&this.raiseRecoverable(this.start,"Comma is not permitted after the rest element");break}else h.push(this.parseMaybeAssign(!1,y,this.parseParenItem));var xe=this.lastTokEnd,Y=this.lastTokEndLoc;if(this.expect(a.parenR),e&&this.shouldParseArrow(h)&&this.eat(a.arrow))return this.checkPatternErrors(y,!1),this.checkYieldAwaitInDefaultParams(),this.yieldPos=S,this.awaitPos=se,this.parseParenArrowList(i,s,h,t);(!h.length||d)&&this.unexpected(this.lastTokStart),Q&&this.unexpected(Q),this.checkExpressionErrors(y,!0),this.yieldPos=S||this.yieldPos,this.awaitPos=se||this.awaitPos,h.length>1?(r=this.startNodeAt(u,o),r.expressions=h,this.finishNodeAt(r,"SequenceExpression",xe,Y)):r=h[0]}else r=this.parseParenExpression();if(this.options.preserveParens){var X=this.startNodeAt(i,s);return X.expression=r,this.finishNode(X,"ParenthesizedExpression")}else return r},f.parseParenItem=function(e){return e},f.parseParenArrowList=function(e,t,i,s){return this.parseArrowExpression(this.startNodeAt(e,t),i,!1,s)};var Nt=[];f.parseNew=function(){this.containsEsc&&this.raiseRecoverable(this.start,"Escape sequence in keyword new");var e=this.startNode();if(this.next(),this.options.ecmaVersion>=6&&this.type===a.dot){var t=this.startNodeAt(e.start,e.loc&&e.loc.start);t.name="new",e.meta=this.finishNode(t,"Identifier"),this.next();var i=this.containsEsc;return e.property=this.parseIdent(!0),e.property.name!=="target"&&this.raiseRecoverable(e.property.start,"The only valid meta property for new is \'new.target\'"),i&&this.raiseRecoverable(e.start,"\'new.target\' must not contain escaped characters"),this.allowNewDotTarget||this.raiseRecoverable(e.start,"\'new.target\' can only be used in functions and class static block"),this.finishNode(e,"MetaProperty")}var s=this.start,r=this.startLoc;return e.callee=this.parseSubscripts(this.parseExprAtom(null,!1,!0),s,r,!0,!1),this.eat(a.parenL)?e.arguments=this.parseExprList(a.parenR,this.options.ecmaVersion>=8,!1):e.arguments=Nt,this.finishNode(e,"NewExpression")},f.parseTemplateElement=function(e){var t=e.isTagged,i=this.startNode();return this.type===a.invalidTemplate?(t||this.raiseRecoverable(this.start,"Bad escape sequence in untagged template literal"),i.value={raw:this.value.replace(/\\r\\n?/g,`\n`),cooked:null}):i.value={raw:this.input.slice(this.start,this.end).replace(/\\r\\n?/g,`\n`),cooked:this.value},this.next(),i.tail=this.type===a.backQuote,this.finishNode(i,"TemplateElement")},f.parseTemplate=function(e){e===void 0&&(e={});var t=e.isTagged;t===void 0&&(t=!1);var i=this.startNode();this.next(),i.expressions=[];var s=this.parseTemplateElement({isTagged:t});for(i.quasis=[s];!s.tail;)this.type===a.eof&&this.raise(this.pos,"Unterminated template literal"),this.expect(a.dollarBraceL),i.expressions.push(this.parseExpression()),this.expect(a.braceR),i.quasis.push(s=this.parseTemplateElement({isTagged:t}));return this.next(),this.finishNode(i,"TemplateLiteral")},f.isAsyncProp=function(e){return!e.computed&&e.key.type==="Identifier"&&e.key.name==="async"&&(this.type===a.name||this.type===a.num||this.type===a.string||this.type===a.bracketL||this.type.keyword||this.options.ecmaVersion>=9&&this.type===a.star)&&!k.test(this.input.slice(this.lastTokEnd,this.start))},f.parseObj=function(e,t){var i=this.startNode(),s=!0,r={};for(i.properties=[],this.next();!this.eat(a.braceR);){if(s)s=!1;else if(this.expect(a.comma),this.options.ecmaVersion>=5&&this.afterTrailingComma(a.braceR))break;var n=this.parseProperty(e,t);e||this.checkPropClash(n,r,t),i.properties.push(n)}return this.finishNode(i,e?"ObjectPattern":"ObjectExpression")},f.parseProperty=function(e,t){var i=this.startNode(),s,r,n,u;if(this.options.ecmaVersion>=9&&this.eat(a.ellipsis))return e?(i.argument=this.parseIdent(!1),this.type===a.comma&&this.raiseRecoverable(this.start,"Comma is not permitted after the rest element"),this.finishNode(i,"RestElement")):(i.argument=this.parseMaybeAssign(!1,t),this.type===a.comma&&t&&t.trailingComma<0&&(t.trailingComma=this.start),this.finishNode(i,"SpreadElement"));this.options.ecmaVersion>=6&&(i.method=!1,i.shorthand=!1,(e||t)&&(n=this.start,u=this.startLoc),e||(s=this.eat(a.star)));var o=this.containsEsc;return this.parsePropertyName(i),!e&&!o&&this.options.ecmaVersion>=8&&!s&&this.isAsyncProp(i)?(r=!0,s=this.options.ecmaVersion>=9&&this.eat(a.star),this.parsePropertyName(i)):r=!1,this.parsePropertyValue(i,e,s,r,n,u,t,o),this.finishNode(i,"Property")},f.parseGetterSetter=function(e){var t=e.key.name;this.parsePropertyName(e),e.value=this.parseMethod(!1),e.kind=t;var i=e.kind==="get"?0:1;if(e.value.params.length!==i){var s=e.value.start;e.kind==="get"?this.raiseRecoverable(s,"getter should have no params"):this.raiseRecoverable(s,"setter should have exactly one param")}else e.kind==="set"&&e.value.params[0].type==="RestElement"&&this.raiseRecoverable(e.value.params[0].start,"Setter cannot use rest params")},f.parsePropertyValue=function(e,t,i,s,r,n,u,o){(i||s)&&this.type===a.colon&&this.unexpected(),this.eat(a.colon)?(e.value=t?this.parseMaybeDefault(this.start,this.startLoc):this.parseMaybeAssign(!1,u),e.kind="init"):this.options.ecmaVersion>=6&&this.type===a.parenL?(t&&this.unexpected(),e.method=!0,e.value=this.parseMethod(i,s),e.kind="init"):!t&&!o&&this.options.ecmaVersion>=5&&!e.computed&&e.key.type==="Identifier"&&(e.key.name==="get"||e.key.name==="set")&&this.type!==a.comma&&this.type!==a.braceR&&this.type!==a.eq?((i||s)&&this.unexpected(),this.parseGetterSetter(e)):this.options.ecmaVersion>=6&&!e.computed&&e.key.type==="Identifier"?((i||s)&&this.unexpected(),this.checkUnreserved(e.key),e.key.name==="await"&&!this.awaitIdentPos&&(this.awaitIdentPos=r),t?e.value=this.parseMaybeDefault(r,n,this.copyNode(e.key)):this.type===a.eq&&u?(u.shorthandAssign<0&&(u.shorthandAssign=this.start),e.value=this.parseMaybeDefault(r,n,this.copyNode(e.key))):e.value=this.copyNode(e.key),e.kind="init",e.shorthand=!0):this.unexpected()},f.parsePropertyName=function(e){if(this.options.ecmaVersion>=6){if(this.eat(a.bracketL))return e.computed=!0,e.key=this.parseMaybeAssign(),this.expect(a.bracketR),e.key;e.computed=!1}return e.key=this.type===a.num||this.type===a.string?this.parseExprAtom():this.parseIdent(this.options.allowReserved!=="never")},f.initFunction=function(e){e.id=null,this.options.ecmaVersion>=6&&(e.generator=e.expression=!1),this.options.ecmaVersion>=8&&(e.async=!1)},f.parseMethod=function(e,t,i){var s=this.startNode(),r=this.yieldPos,n=this.awaitPos,u=this.awaitIdentPos;return this.initFunction(s),this.options.ecmaVersion>=6&&(s.generator=e),this.options.ecmaVersion>=8&&(s.async=!!t),this.yieldPos=0,this.awaitPos=0,this.awaitIdentPos=0,this.enterScope(Se(t,s.generator)|ne|(i?Ue:0)),this.expect(a.parenL),s.params=this.parseBindingList(a.parenR,!1,this.options.ecmaVersion>=8),this.checkYieldAwaitInDefaultParams(),this.parseFunctionBody(s,!1,!0,!1),this.yieldPos=r,this.awaitPos=n,this.awaitIdentPos=u,this.finishNode(s,"FunctionExpression")},f.parseArrowExpression=function(e,t,i,s){var r=this.yieldPos,n=this.awaitPos,u=this.awaitIdentPos;return this.enterScope(Se(i,!1)|_e),this.initFunction(e),this.options.ecmaVersion>=8&&(e.async=!!i),this.yieldPos=0,this.awaitPos=0,this.awaitIdentPos=0,e.params=this.toAssignableList(t,!0),this.parseFunctionBody(e,!0,!1,s),this.yieldPos=r,this.awaitPos=n,this.awaitIdentPos=u,this.finishNode(e,"ArrowFunctionExpression")},f.parseFunctionBody=function(e,t,i,s){var r=t&&this.type!==a.braceL,n=this.strict,u=!1;if(r)e.body=this.parseMaybeAssign(s),e.expression=!0,this.checkParams(e,!1);else{var o=this.options.ecmaVersion>=7&&!this.isSimpleParamList(e.params);(!n||o)&&(u=this.strictDirective(this.end),u&&o&&this.raiseRecoverable(e.start,"Illegal \'use strict\' directive in function with non-simple parameter list"));var h=this.labels;this.labels=[],u&&(this.strict=!0),this.checkParams(e,!n&&!u&&!t&&!i&&this.isSimpleParamList(e.params)),this.strict&&e.id&&this.checkLValSimple(e.id,He),e.body=this.parseBlock(!1,void 0,u&&!n),e.expression=!1,this.adaptDirectivePrologue(e.body.body),this.labels=h}this.exitScope()},f.isSimpleParamList=function(e){for(var t=0,i=e;t-1||r.functions.indexOf(e)>-1||r.var.indexOf(e)>-1,r.lexical.push(e),this.inModule&&r.flags&j&&delete this.undefinedExports[e]}else if(t===Ge){var n=this.currentScope();n.lexical.push(e)}else if(t===je){var u=this.currentScope();this.treatFunctionsAsVar?s=u.lexical.indexOf(e)>-1:s=u.lexical.indexOf(e)>-1||u.var.indexOf(e)>-1,u.functions.push(e)}else for(var o=this.scopeStack.length-1;o>=0;--o){var h=this.scopeStack[o];if(h.lexical.indexOf(e)>-1&&!(h.flags&Me&&h.lexical[0]===e)||!this.treatFunctionsAsVarInScope(h)&&h.functions.indexOf(e)>-1){s=!0;break}if(h.var.push(e),this.inModule&&h.flags&j&&delete this.undefinedExports[e],h.flags&ue)break}s&&this.raiseRecoverable(i,"Identifier \'"+e+"\' has already been declared")},U.checkLocalExport=function(e){this.scopeStack[0].lexical.indexOf(e.name)===-1&&this.scopeStack[0].var.indexOf(e.name)===-1&&(this.undefinedExports[e.name]=e)},U.currentScope=function(){return this.scopeStack[this.scopeStack.length-1]},U.currentVarScope=function(){for(var e=this.scopeStack.length-1;;e--){var t=this.scopeStack[e];if(t.flags&(ue|$|H))return t}},U.currentThisScope=function(){for(var e=this.scopeStack.length-1;;e--){var t=this.scopeStack[e];if(t.flags&(ue|$|H)&&!(t.flags&_e))return t}};var te=function(t,i,s){this.type="",this.start=i,this.end=0,t.options.locations&&(this.loc=new J(t,s)),t.options.directSourceFile&&(this.sourceFile=t.options.directSourceFile),t.options.ranges&&(this.range=[i,0])},ie=C.prototype;ie.startNode=function(){return new te(this,this.start,this.startLoc)},ie.startNodeAt=function(e,t){return new te(this,e,t)};function Ke(e,t,i,s){return e.type=t,e.end=i,this.options.locations&&(e.loc.end=s),this.options.ranges&&(e.range[1]=i),e}ie.finishNode=function(e,t){return Ke.call(this,e,t,this.lastTokEnd,this.lastTokEndLoc)},ie.finishNodeAt=function(e,t,i,s){return Ke.call(this,e,t,i,s)},ie.copyNode=function(e){var t=new te(this,e.start,this.startLoc);for(var i in e)t[i]=e[i];return t};var Tt="Berf Beria_Erfe Gara Garay Gukh Gurung_Khema Hrkt Katakana_Or_Hiragana Kawi Kirat_Rai Krai Nag_Mundari Nagm Ol_Onal Onao Sidetic Sidt Sunu Sunuwar Tai_Yo Tayo Todhri Todr Tolong_Siki Tols Tulu_Tigalari Tutg Unknown Zzzz",Qe="ASCII ASCII_Hex_Digit AHex Alphabetic Alpha Any Assigned Bidi_Control Bidi_C Bidi_Mirrored Bidi_M Case_Ignorable CI Cased Changes_When_Casefolded CWCF Changes_When_Casemapped CWCM Changes_When_Lowercased CWL Changes_When_NFKC_Casefolded CWKCF Changes_When_Titlecased CWT Changes_When_Uppercased CWU Dash Default_Ignorable_Code_Point DI Deprecated Dep Diacritic Dia Emoji Emoji_Component Emoji_Modifier Emoji_Modifier_Base Emoji_Presentation Extender Ext Grapheme_Base Gr_Base Grapheme_Extend Gr_Ext Hex_Digit Hex IDS_Binary_Operator IDSB IDS_Trinary_Operator IDST ID_Continue IDC ID_Start IDS Ideographic Ideo Join_Control Join_C Logical_Order_Exception LOE Lowercase Lower Math Noncharacter_Code_Point NChar Pattern_Syntax Pat_Syn Pattern_White_Space Pat_WS Quotation_Mark QMark Radical Regional_Indicator RI Sentence_Terminal STerm Soft_Dotted SD Terminal_Punctuation Term Unified_Ideograph UIdeo Uppercase Upper Variation_Selector VS White_Space space XID_Continue XIDC XID_Start XIDS",Ye=Qe+" Extended_Pictographic",Xe=Ye,Ze=Xe+" EBase EComp EMod EPres ExtPict",Je=Ze,Lt=Je,Rt={9:Qe,10:Ye,11:Xe,12:Ze,13:Je,14:Lt},Ot="Basic_Emoji Emoji_Keycap_Sequence RGI_Emoji_Modifier_Sequence RGI_Emoji_Flag_Sequence RGI_Emoji_Tag_Sequence RGI_Emoji_ZWJ_Sequence RGI_Emoji",Bt={9:"",10:"",11:"",12:"",13:"",14:Ot},$e="Cased_Letter LC Close_Punctuation Pe Connector_Punctuation Pc Control Cc cntrl Currency_Symbol Sc Dash_Punctuation Pd Decimal_Number Nd digit Enclosing_Mark Me Final_Punctuation Pf Format Cf Initial_Punctuation Pi Letter L Letter_Number Nl Line_Separator Zl Lowercase_Letter Ll Mark M Combining_Mark Math_Symbol Sm Modifier_Letter Lm Modifier_Symbol Sk Nonspacing_Mark Mn Number N Open_Punctuation Ps Other C Other_Letter Lo Other_Number No Other_Punctuation Po Other_Symbol So Paragraph_Separator Zp Private_Use Co Punctuation P punct Separator Z Space_Separator Zs Spacing_Mark Mc Surrogate Cs Symbol S Titlecase_Letter Lt Unassigned Cn Uppercase_Letter Lu",et="Adlam Adlm Ahom Anatolian_Hieroglyphs Hluw Arabic Arab Armenian Armn Avestan Avst Balinese Bali Bamum Bamu Bassa_Vah Bass Batak Batk Bengali Beng Bhaiksuki Bhks Bopomofo Bopo Brahmi Brah Braille Brai Buginese Bugi Buhid Buhd Canadian_Aboriginal Cans Carian Cari Caucasian_Albanian Aghb Chakma Cakm Cham Cham Cherokee Cher Common Zyyy Coptic Copt Qaac Cuneiform Xsux Cypriot Cprt Cyrillic Cyrl Deseret Dsrt Devanagari Deva Duployan Dupl Egyptian_Hieroglyphs Egyp Elbasan Elba Ethiopic Ethi Georgian Geor Glagolitic Glag Gothic Goth Grantha Gran Greek Grek Gujarati Gujr Gurmukhi Guru Han Hani Hangul Hang Hanunoo Hano Hatran Hatr Hebrew Hebr Hiragana Hira Imperial_Aramaic Armi Inherited Zinh Qaai Inscriptional_Pahlavi Phli Inscriptional_Parthian Prti Javanese Java Kaithi Kthi Kannada Knda Katakana Kana Kayah_Li Kali Kharoshthi Khar Khmer Khmr Khojki Khoj Khudawadi Sind Lao Laoo Latin Latn Lepcha Lepc Limbu Limb Linear_A Lina Linear_B Linb Lisu Lisu Lycian Lyci Lydian Lydi Mahajani Mahj Malayalam Mlym Mandaic Mand Manichaean Mani Marchen Marc Masaram_Gondi Gonm Meetei_Mayek Mtei Mende_Kikakui Mend Meroitic_Cursive Merc Meroitic_Hieroglyphs Mero Miao Plrd Modi Mongolian Mong Mro Mroo Multani Mult Myanmar Mymr Nabataean Nbat New_Tai_Lue Talu Newa Newa Nko Nkoo Nushu Nshu Ogham Ogam Ol_Chiki Olck Old_Hungarian Hung Old_Italic Ital Old_North_Arabian Narb Old_Permic Perm Old_Persian Xpeo Old_South_Arabian Sarb Old_Turkic Orkh Oriya Orya Osage Osge Osmanya Osma Pahawh_Hmong Hmng Palmyrene Palm Pau_Cin_Hau Pauc Phags_Pa Phag Phoenician Phnx Psalter_Pahlavi Phlp Rejang Rjng Runic Runr Samaritan Samr Saurashtra Saur Sharada Shrd Shavian Shaw Siddham Sidd SignWriting Sgnw Sinhala Sinh Sora_Sompeng Sora Soyombo Soyo Sundanese Sund Syloti_Nagri Sylo Syriac Syrc Tagalog Tglg Tagbanwa Tagb Tai_Le Tale Tai_Tham Lana Tai_Viet Tavt Takri Takr Tamil Taml Tangut Tang Telugu Telu Thaana Thaa Thai Thai Tibetan Tibt Tifinagh Tfng Tirhuta Tirh Ugaritic Ugar Vai Vaii Warang_Citi Wara Yi Yiii Zanabazar_Square Zanb",tt=et+" Dogra Dogr Gunjala_Gondi Gong Hanifi_Rohingya Rohg Makasar Maka Medefaidrin Medf Old_Sogdian Sogo Sogdian Sogd",it=tt+" Elymaic Elym Nandinagari Nand Nyiakeng_Puachue_Hmong Hmnp Wancho Wcho",st=it+" Chorasmian Chrs Diak Dives_Akuru Khitan_Small_Script Kits Yezi Yezidi",at=st+" Cypro_Minoan Cpmn Old_Uyghur Ougr Tangsa Tnsa Toto Vithkuqi Vith",Dt=at+" "+Tt,Ft={9:et,10:tt,11:it,12:st,13:at,14:Dt},rt={};function Mt(e){var t=rt[e]={binary:M(Rt[e]+" "+$e),binaryOfStrings:M(Bt[e]),nonBinary:{General_Category:M($e),Script:M(Ft[e])}};t.nonBinary.Script_Extensions=t.nonBinary.Script,t.nonBinary.gc=t.nonBinary.General_Category,t.nonBinary.sc=t.nonBinary.Script,t.nonBinary.scx=t.nonBinary.Script_Extensions}for(var Ie=0,nt=[9,10,11,12,13,14];Ie=6?"uy":"")+(t.options.ecmaVersion>=9?"s":"")+(t.options.ecmaVersion>=13?"d":"")+(t.options.ecmaVersion>=15?"v":""),this.unicodeProperties=rt[t.options.ecmaVersion>=14?14:t.options.ecmaVersion],this.source="",this.flags="",this.start=0,this.switchU=!1,this.switchV=!1,this.switchN=!1,this.pos=0,this.lastIntValue=0,this.lastStringValue="",this.lastAssertionIsQuantifiable=!1,this.numCapturingParens=0,this.maxBackReference=0,this.groupNames=Object.create(null),this.backReferenceNames=[],this.branchID=null};R.prototype.reset=function(t,i,s){var r=s.indexOf("v")!==-1,n=s.indexOf("u")!==-1;this.start=t|0,this.source=i+"",this.flags=s,r&&this.parser.options.ecmaVersion>=15?(this.switchU=!0,this.switchV=!0,this.switchN=!0):(this.switchU=n&&this.parser.options.ecmaVersion>=6,this.switchV=!1,this.switchN=n&&this.parser.options.ecmaVersion>=9)},R.prototype.raise=function(t){this.parser.raiseRecoverable(this.start,"Invalid regular expression: /"+this.source+"/: "+t)},R.prototype.at=function(t,i){i===void 0&&(i=!1);var s=this.source,r=s.length;if(t>=r)return-1;var n=s.charCodeAt(t);if(!(i||this.switchU)||n<=55295||n>=57344||t+1>=r)return n;var u=s.charCodeAt(t+1);return u>=56320&&u<=57343?(n<<10)+u-56613888:n},R.prototype.nextIndex=function(t,i){i===void 0&&(i=!1);var s=this.source,r=s.length;if(t>=r)return r;var n=s.charCodeAt(t),u;return!(i||this.switchU)||n<=55295||n>=57344||t+1>=r||(u=s.charCodeAt(t+1))<56320||u>57343?t+1:t+2},R.prototype.current=function(t){return t===void 0&&(t=!1),this.at(this.pos,t)},R.prototype.lookahead=function(t){return t===void 0&&(t=!1),this.at(this.nextIndex(this.pos,t),t)},R.prototype.advance=function(t){t===void 0&&(t=!1),this.pos=this.nextIndex(this.pos,t)},R.prototype.eat=function(t,i){return i===void 0&&(i=!1),this.current(i)===t?(this.advance(i),!0):!1},R.prototype.eatChars=function(t,i){i===void 0&&(i=!1);for(var s=this.pos,r=0,n=t;r-1&&this.raise(e.start,"Duplicate regular expression flag"),u==="u"&&(s=!0),u==="v"&&(r=!0)}this.options.ecmaVersion>=15&&s&&r&&this.raise(e.start,"Invalid regular expression flag")};function qt(e){for(var t in e)return!0;return!1}c.validateRegExpPattern=function(e){this.regexp_pattern(e),!e.switchN&&this.options.ecmaVersion>=9&&qt(e.groupNames)&&(e.switchN=!0,this.regexp_pattern(e))},c.regexp_pattern=function(e){e.pos=0,e.lastIntValue=0,e.lastStringValue="",e.lastAssertionIsQuantifiable=!1,e.numCapturingParens=0,e.maxBackReference=0,e.groupNames=Object.create(null),e.backReferenceNames.length=0,e.branchID=null,this.regexp_disjunction(e),e.pos!==e.source.length&&(e.eat(41)&&e.raise("Unmatched \')\'"),(e.eat(93)||e.eat(125))&&e.raise("Lone quantifier brackets")),e.maxBackReference>e.numCapturingParens&&e.raise("Invalid escape");for(var t=0,i=e.backReferenceNames;t=16;for(t&&(e.branchID=new pe(e.branchID,null)),this.regexp_alternative(e);e.eat(124);)t&&(e.branchID=e.branchID.sibling()),this.regexp_alternative(e);t&&(e.branchID=e.branchID.parent),this.regexp_eatQuantifier(e,!0)&&e.raise("Nothing to repeat"),e.eat(123)&&e.raise("Lone quantifier brackets")},c.regexp_alternative=function(e){for(;e.pos=9&&(i=e.eat(60)),e.eat(61)||e.eat(33))return this.regexp_disjunction(e),e.eat(41)||e.raise("Unterminated group"),e.lastAssertionIsQuantifiable=!i,!0}return e.pos=t,!1},c.regexp_eatQuantifier=function(e,t){return t===void 0&&(t=!1),this.regexp_eatQuantifierPrefix(e,t)?(e.eat(63),!0):!1},c.regexp_eatQuantifierPrefix=function(e,t){return e.eat(42)||e.eat(43)||e.eat(63)||this.regexp_eatBracedQuantifier(e,t)},c.regexp_eatBracedQuantifier=function(e,t){var i=e.pos;if(e.eat(123)){var s=0,r=-1;if(this.regexp_eatDecimalDigits(e)&&(s=e.lastIntValue,e.eat(44)&&this.regexp_eatDecimalDigits(e)&&(r=e.lastIntValue),e.eat(125)))return r!==-1&&r=16){var i=this.regexp_eatModifiers(e),s=e.eat(45);if(i||s){for(var r=0;r-1&&e.raise("Duplicate regular expression modifiers")}if(s){var u=this.regexp_eatModifiers(e);!i&&!u&&e.current()===58&&e.raise("Invalid regular expression modifiers");for(var o=0;o-1||i.indexOf(h)>-1)&&e.raise("Duplicate regular expression modifiers")}}}}if(e.eat(58)){if(this.regexp_disjunction(e),e.eat(41))return!0;e.raise("Unterminated group")}}e.pos=t}return!1},c.regexp_eatCapturingGroup=function(e){if(e.eat(40)){if(this.options.ecmaVersion>=9?this.regexp_groupSpecifier(e):e.current()===63&&e.raise("Invalid group"),this.regexp_disjunction(e),e.eat(41))return e.numCapturingParens+=1,!0;e.raise("Unterminated group")}return!1},c.regexp_eatModifiers=function(e){for(var t="",i=0;(i=e.current())!==-1&&jt(i);)t+=B(i),e.advance();return t};function jt(e){return e===105||e===109||e===115}c.regexp_eatExtendedAtom=function(e){return e.eat(46)||this.regexp_eatReverseSolidusAtomEscape(e)||this.regexp_eatCharacterClass(e)||this.regexp_eatUncapturingGroup(e)||this.regexp_eatCapturingGroup(e)||this.regexp_eatInvalidBracedQuantifier(e)||this.regexp_eatExtendedPatternCharacter(e)},c.regexp_eatInvalidBracedQuantifier=function(e){return this.regexp_eatBracedQuantifier(e,!0)&&e.raise("Nothing to repeat"),!1},c.regexp_eatSyntaxCharacter=function(e){var t=e.current();return ut(t)?(e.lastIntValue=t,e.advance(),!0):!1};function ut(e){return e===36||e>=40&&e<=43||e===46||e===63||e>=91&&e<=94||e>=123&&e<=125}c.regexp_eatPatternCharacters=function(e){for(var t=e.pos,i=0;(i=e.current())!==-1&&!ut(i);)e.advance();return e.pos!==t},c.regexp_eatExtendedPatternCharacter=function(e){var t=e.current();return t!==-1&&t!==36&&!(t>=40&&t<=43)&&t!==46&&t!==63&&t!==91&&t!==94&&t!==124?(e.advance(),!0):!1},c.regexp_groupSpecifier=function(e){if(e.eat(63)){this.regexp_eatGroupName(e)||e.raise("Invalid group");var t=this.options.ecmaVersion>=16,i=e.groupNames[e.lastStringValue];if(i)if(t)for(var s=0,r=i;s=11,s=e.current(i);return e.advance(i),s===92&&this.regexp_eatRegExpUnicodeEscapeSequence(e,i)&&(s=e.lastIntValue),Gt(s)?(e.lastIntValue=s,!0):(e.pos=t,!1)};function Gt(e){return L(e,!0)||e===36||e===95}c.regexp_eatRegExpIdentifierPart=function(e){var t=e.pos,i=this.options.ecmaVersion>=11,s=e.current(i);return e.advance(i),s===92&&this.regexp_eatRegExpUnicodeEscapeSequence(e,i)&&(s=e.lastIntValue),Ht(s)?(e.lastIntValue=s,!0):(e.pos=t,!1)};function Ht(e){return O(e,!0)||e===36||e===95||e===8204||e===8205}c.regexp_eatAtomEscape=function(e){return this.regexp_eatBackReference(e)||this.regexp_eatCharacterClassEscape(e)||this.regexp_eatCharacterEscape(e)||e.switchN&&this.regexp_eatKGroupName(e)?!0:(e.switchU&&(e.current()===99&&e.raise("Invalid unicode escape"),e.raise("Invalid escape")),!1)},c.regexp_eatBackReference=function(e){var t=e.pos;if(this.regexp_eatDecimalEscape(e)){var i=e.lastIntValue;if(e.switchU)return i>e.maxBackReference&&(e.maxBackReference=i),!0;if(i<=e.numCapturingParens)return!0;e.pos=t}return!1},c.regexp_eatKGroupName=function(e){if(e.eat(107)){if(this.regexp_eatGroupName(e))return e.backReferenceNames.push(e.lastStringValue),!0;e.raise("Invalid named reference")}return!1},c.regexp_eatCharacterEscape=function(e){return this.regexp_eatControlEscape(e)||this.regexp_eatCControlLetter(e)||this.regexp_eatZero(e)||this.regexp_eatHexEscapeSequence(e)||this.regexp_eatRegExpUnicodeEscapeSequence(e,!1)||!e.switchU&&this.regexp_eatLegacyOctalEscapeSequence(e)||this.regexp_eatIdentityEscape(e)},c.regexp_eatCControlLetter=function(e){var t=e.pos;if(e.eat(99)){if(this.regexp_eatControlLetter(e))return!0;e.pos=t}return!1},c.regexp_eatZero=function(e){return e.current()===48&&!fe(e.lookahead())?(e.lastIntValue=0,e.advance(),!0):!1},c.regexp_eatControlEscape=function(e){var t=e.current();return t===116?(e.lastIntValue=9,e.advance(),!0):t===110?(e.lastIntValue=10,e.advance(),!0):t===118?(e.lastIntValue=11,e.advance(),!0):t===102?(e.lastIntValue=12,e.advance(),!0):t===114?(e.lastIntValue=13,e.advance(),!0):!1},c.regexp_eatControlLetter=function(e){var t=e.current();return ot(t)?(e.lastIntValue=t%32,e.advance(),!0):!1};function ot(e){return e>=65&&e<=90||e>=97&&e<=122}c.regexp_eatRegExpUnicodeEscapeSequence=function(e,t){t===void 0&&(t=!1);var i=e.pos,s=t||e.switchU;if(e.eat(117)){if(this.regexp_eatFixedHexDigits(e,4)){var r=e.lastIntValue;if(s&&r>=55296&&r<=56319){var n=e.pos;if(e.eat(92)&&e.eat(117)&&this.regexp_eatFixedHexDigits(e,4)){var u=e.lastIntValue;if(u>=56320&&u<=57343)return e.lastIntValue=(r-55296)*1024+(u-56320)+65536,!0}e.pos=n,e.lastIntValue=r}return!0}if(s&&e.eat(123)&&this.regexp_eatHexDigits(e)&&e.eat(125)&&Wt(e.lastIntValue))return!0;s&&e.raise("Invalid unicode escape"),e.pos=i}return!1};function Wt(e){return e>=0&&e<=1114111}c.regexp_eatIdentityEscape=function(e){if(e.switchU)return this.regexp_eatSyntaxCharacter(e)?!0:e.eat(47)?(e.lastIntValue=47,!0):!1;var t=e.current();return t!==99&&(!e.switchN||t!==107)?(e.lastIntValue=t,e.advance(),!0):!1},c.regexp_eatDecimalEscape=function(e){e.lastIntValue=0;var t=e.current();if(t>=49&&t<=57){do e.lastIntValue=10*e.lastIntValue+(t-48),e.advance();while((t=e.current())>=48&&t<=57);return!0}return!1};var ht=0,F=1,T=2;c.regexp_eatCharacterClassEscape=function(e){var t=e.current();if(zt(t))return e.lastIntValue=-1,e.advance(),F;var i=!1;if(e.switchU&&this.options.ecmaVersion>=9&&((i=t===80)||t===112)){e.lastIntValue=-1,e.advance();var s;if(e.eat(123)&&(s=this.regexp_eatUnicodePropertyValueExpression(e))&&e.eat(125))return i&&s===T&&e.raise("Invalid property name"),s;e.raise("Invalid property name")}return ht};function zt(e){return e===100||e===68||e===115||e===83||e===119||e===87}c.regexp_eatUnicodePropertyValueExpression=function(e){var t=e.pos;if(this.regexp_eatUnicodePropertyName(e)&&e.eat(61)){var i=e.lastStringValue;if(this.regexp_eatUnicodePropertyValue(e)){var s=e.lastStringValue;return this.regexp_validateUnicodePropertyNameAndValue(e,i,s),F}}if(e.pos=t,this.regexp_eatLoneUnicodePropertyNameOrValue(e)){var r=e.lastStringValue;return this.regexp_validateUnicodePropertyNameOrValue(e,r)}return ht},c.regexp_validateUnicodePropertyNameAndValue=function(e,t,i){W(e.unicodeProperties.nonBinary,t)||e.raise("Invalid property name"),e.unicodeProperties.nonBinary[t].test(i)||e.raise("Invalid property value")},c.regexp_validateUnicodePropertyNameOrValue=function(e,t){if(e.unicodeProperties.binary.test(t))return F;if(e.switchV&&e.unicodeProperties.binaryOfStrings.test(t))return T;e.raise("Invalid property name")},c.regexp_eatUnicodePropertyName=function(e){var t=0;for(e.lastStringValue="";ct(t=e.current());)e.lastStringValue+=B(t),e.advance();return e.lastStringValue!==""};function ct(e){return ot(e)||e===95}c.regexp_eatUnicodePropertyValue=function(e){var t=0;for(e.lastStringValue="";Kt(t=e.current());)e.lastStringValue+=B(t),e.advance();return e.lastStringValue!==""};function Kt(e){return ct(e)||fe(e)}c.regexp_eatLoneUnicodePropertyNameOrValue=function(e){return this.regexp_eatUnicodePropertyValue(e)},c.regexp_eatCharacterClass=function(e){if(e.eat(91)){var t=e.eat(94),i=this.regexp_classContents(e);return e.eat(93)||e.raise("Unterminated character class"),t&&i===T&&e.raise("Negated character class may contain strings"),!0}return!1},c.regexp_classContents=function(e){return e.current()===93?F:e.switchV?this.regexp_classSetExpression(e):(this.regexp_nonEmptyClassRanges(e),F)},c.regexp_nonEmptyClassRanges=function(e){for(;this.regexp_eatClassAtom(e);){var t=e.lastIntValue;if(e.eat(45)&&this.regexp_eatClassAtom(e)){var i=e.lastIntValue;e.switchU&&(t===-1||i===-1)&&e.raise("Invalid character class"),t!==-1&&i!==-1&&t>i&&e.raise("Range out of order in character class")}}},c.regexp_eatClassAtom=function(e){var t=e.pos;if(e.eat(92)){if(this.regexp_eatClassEscape(e))return!0;if(e.switchU){var i=e.current();(i===99||ft(i))&&e.raise("Invalid class escape"),e.raise("Invalid escape")}e.pos=t}var s=e.current();return s!==93?(e.lastIntValue=s,e.advance(),!0):!1},c.regexp_eatClassEscape=function(e){var t=e.pos;if(e.eat(98))return e.lastIntValue=8,!0;if(e.switchU&&e.eat(45))return e.lastIntValue=45,!0;if(!e.switchU&&e.eat(99)){if(this.regexp_eatClassControlLetter(e))return!0;e.pos=t}return this.regexp_eatCharacterClassEscape(e)||this.regexp_eatCharacterEscape(e)},c.regexp_classSetExpression=function(e){var t=F,i;if(!this.regexp_eatClassSetRange(e))if(i=this.regexp_eatClassSetOperand(e)){i===T&&(t=T);for(var s=e.pos;e.eatChars([38,38]);){if(e.current()!==38&&(i=this.regexp_eatClassSetOperand(e))){i!==T&&(t=F);continue}e.raise("Invalid character in character class")}if(s!==e.pos)return t;for(;e.eatChars([45,45]);)this.regexp_eatClassSetOperand(e)||e.raise("Invalid character in character class");if(s!==e.pos)return t}else e.raise("Invalid character in character class");for(;;)if(!this.regexp_eatClassSetRange(e)){if(i=this.regexp_eatClassSetOperand(e),!i)return t;i===T&&(t=T)}},c.regexp_eatClassSetRange=function(e){var t=e.pos;if(this.regexp_eatClassSetCharacter(e)){var i=e.lastIntValue;if(e.eat(45)&&this.regexp_eatClassSetCharacter(e)){var s=e.lastIntValue;return i!==-1&&s!==-1&&i>s&&e.raise("Range out of order in character class"),!0}e.pos=t}return!1},c.regexp_eatClassSetOperand=function(e){return this.regexp_eatClassSetCharacter(e)?F:this.regexp_eatClassStringDisjunction(e)||this.regexp_eatNestedClass(e)},c.regexp_eatNestedClass=function(e){var t=e.pos;if(e.eat(91)){var i=e.eat(94),s=this.regexp_classContents(e);if(e.eat(93))return i&&s===T&&e.raise("Negated character class may contain strings"),s;e.pos=t}if(e.eat(92)){var r=this.regexp_eatCharacterClassEscape(e);if(r)return r;e.pos=t}return null},c.regexp_eatClassStringDisjunction=function(e){var t=e.pos;if(e.eatChars([92,113])){if(e.eat(123)){var i=this.regexp_classStringDisjunctionContents(e);if(e.eat(125))return i}else e.raise("Invalid escape");e.pos=t}return null},c.regexp_classStringDisjunctionContents=function(e){for(var t=this.regexp_classString(e);e.eat(124);)this.regexp_classString(e)===T&&(t=T);return t},c.regexp_classString=function(e){for(var t=0;this.regexp_eatClassSetCharacter(e);)t++;return t===1?F:T},c.regexp_eatClassSetCharacter=function(e){var t=e.pos;if(e.eat(92))return this.regexp_eatCharacterEscape(e)||this.regexp_eatClassSetReservedPunctuator(e)?!0:e.eat(98)?(e.lastIntValue=8,!0):(e.pos=t,!1);var i=e.current();return i<0||i===e.lookahead()&&Qt(i)||Yt(i)?!1:(e.advance(),e.lastIntValue=i,!0)};function Qt(e){return e===33||e>=35&&e<=38||e>=42&&e<=44||e===46||e>=58&&e<=64||e===94||e===96||e===126}function Yt(e){return e===40||e===41||e===45||e===47||e>=91&&e<=93||e>=123&&e<=125}c.regexp_eatClassSetReservedPunctuator=function(e){var t=e.current();return Xt(t)?(e.lastIntValue=t,e.advance(),!0):!1};function Xt(e){return e===33||e===35||e===37||e===38||e===44||e===45||e>=58&&e<=62||e===64||e===96||e===126}c.regexp_eatClassControlLetter=function(e){var t=e.current();return fe(t)||t===95?(e.lastIntValue=t%32,e.advance(),!0):!1},c.regexp_eatHexEscapeSequence=function(e){var t=e.pos;if(e.eat(120)){if(this.regexp_eatFixedHexDigits(e,2))return!0;e.switchU&&e.raise("Invalid escape"),e.pos=t}return!1},c.regexp_eatDecimalDigits=function(e){var t=e.pos,i=0;for(e.lastIntValue=0;fe(i=e.current());)e.lastIntValue=10*e.lastIntValue+(i-48),e.advance();return e.pos!==t};function fe(e){return e>=48&&e<=57}c.regexp_eatHexDigits=function(e){var t=e.pos,i=0;for(e.lastIntValue=0;lt(i=e.current());)e.lastIntValue=16*e.lastIntValue+pt(i),e.advance();return e.pos!==t};function lt(e){return e>=48&&e<=57||e>=65&&e<=70||e>=97&&e<=102}function pt(e){return e>=65&&e<=70?10+(e-65):e>=97&&e<=102?10+(e-97):e-48}c.regexp_eatLegacyOctalEscapeSequence=function(e){if(this.regexp_eatOctalDigit(e)){var t=e.lastIntValue;if(this.regexp_eatOctalDigit(e)){var i=e.lastIntValue;t<=3&&this.regexp_eatOctalDigit(e)?e.lastIntValue=t*64+i*8+e.lastIntValue:e.lastIntValue=t*8+i}else e.lastIntValue=t;return!0}return!1},c.regexp_eatOctalDigit=function(e){var t=e.current();return ft(t)?(e.lastIntValue=t-48,e.advance(),!0):(e.lastIntValue=0,!1)};function ft(e){return e>=48&&e<=55}c.regexp_eatFixedHexDigits=function(e,t){var i=e.pos;e.lastIntValue=0;for(var s=0;s=this.input.length)return this.finishToken(a.eof);if(e.override)return e.override(this);this.readToken(this.fullCharCodeAtPos())},x.readToken=function(e){return L(e,this.options.ecmaVersion>=6)||e===92?this.readWord():this.getTokenFromCode(e)},x.fullCharCodeAt=function(e){var t=this.input.charCodeAt(e);if(t<=55295||t>=56320)return t;var i=this.input.charCodeAt(e+1);return i<=56319||i>=57344?t:(t<<10)+i-56613888},x.fullCharCodeAtPos=function(){return this.fullCharCodeAt(this.pos)},x.skipBlockComment=function(){var e=this.options.onComment&&this.curPosition(),t=this.pos,i=this.input.indexOf("*/",this.pos+=2);if(i===-1&&this.raise(this.pos-2,"Unterminated comment"),this.pos=i+2,this.options.locations)for(var s=void 0,r=t;(s=Le(this.input,r,this.pos))>-1;)++this.curLine,r=this.lineStart=s;this.options.onComment&&this.options.onComment(!0,this.input.slice(t+2,i),t,this.pos,e,this.curPosition())},x.skipLineComment=function(e){for(var t=this.pos,i=this.options.onComment&&this.curPosition(),s=this.input.charCodeAt(this.pos+=e);this.pos8&&e<14||e>=5760&&be.test(String.fromCharCode(e)))++this.pos;else break e}}},x.finishToken=function(e,t){this.end=this.pos,this.options.locations&&(this.endLoc=this.curPosition());var i=this.type;this.type=e,this.value=t,this.updateContext(i)},x.readToken_dot=function(){var e=this.input.charCodeAt(this.pos+1);if(e>=48&&e<=57)return this.readNumber(!0);var t=this.input.charCodeAt(this.pos+2);return this.options.ecmaVersion>=6&&e===46&&t===46?(this.pos+=3,this.finishToken(a.ellipsis)):(++this.pos,this.finishToken(a.dot))},x.readToken_slash=function(){var e=this.input.charCodeAt(this.pos+1);return this.exprAllowed?(++this.pos,this.readRegexp()):e===61?this.finishOp(a.assign,2):this.finishOp(a.slash,1)},x.readToken_mult_modulo_exp=function(e){var t=this.input.charCodeAt(this.pos+1),i=1,s=e===42?a.star:a.modulo;return this.options.ecmaVersion>=7&&e===42&&t===42&&(++i,s=a.starstar,t=this.input.charCodeAt(this.pos+2)),t===61?this.finishOp(a.assign,i+1):this.finishOp(s,i)},x.readToken_pipe_amp=function(e){var t=this.input.charCodeAt(this.pos+1);if(t===e){if(this.options.ecmaVersion>=12){var i=this.input.charCodeAt(this.pos+2);if(i===61)return this.finishOp(a.assign,3)}return this.finishOp(e===124?a.logicalOR:a.logicalAND,2)}return t===61?this.finishOp(a.assign,2):this.finishOp(e===124?a.bitwiseOR:a.bitwiseAND,1)},x.readToken_caret=function(){var e=this.input.charCodeAt(this.pos+1);return e===61?this.finishOp(a.assign,2):this.finishOp(a.bitwiseXOR,1)},x.readToken_plus_min=function(e){var t=this.input.charCodeAt(this.pos+1);return t===e?t===45&&!this.inModule&&this.input.charCodeAt(this.pos+2)===62&&(this.lastTokEnd===0||k.test(this.input.slice(this.lastTokEnd,this.pos)))?(this.skipLineComment(3),this.skipSpace(),this.nextToken()):this.finishOp(a.incDec,2):t===61?this.finishOp(a.assign,2):this.finishOp(a.plusMin,1)},x.readToken_lt_gt=function(e){var t=this.input.charCodeAt(this.pos+1),i=1;return t===e?(i=e===62&&this.input.charCodeAt(this.pos+2)===62?3:2,this.input.charCodeAt(this.pos+i)===61?this.finishOp(a.assign,i+1):this.finishOp(a.bitShift,i)):t===33&&e===60&&!this.inModule&&this.input.charCodeAt(this.pos+2)===45&&this.input.charCodeAt(this.pos+3)===45?(this.skipLineComment(4),this.skipSpace(),this.nextToken()):(t===61&&(i=2),this.finishOp(a.relational,i))},x.readToken_eq_excl=function(e){var t=this.input.charCodeAt(this.pos+1);return t===61?this.finishOp(a.equality,this.input.charCodeAt(this.pos+2)===61?3:2):e===61&&t===62&&this.options.ecmaVersion>=6?(this.pos+=2,this.finishToken(a.arrow)):this.finishOp(e===61?a.eq:a.prefix,1)},x.readToken_question=function(){var e=this.options.ecmaVersion;if(e>=11){var t=this.input.charCodeAt(this.pos+1);if(t===46){var i=this.input.charCodeAt(this.pos+2);if(i<48||i>57)return this.finishOp(a.questionDot,2)}if(t===63){if(e>=12){var s=this.input.charCodeAt(this.pos+2);if(s===61)return this.finishOp(a.assign,3)}return this.finishOp(a.coalesce,2)}}return this.finishOp(a.question,1)},x.readToken_numberSign=function(){var e=this.options.ecmaVersion,t=35;if(e>=13&&(++this.pos,t=this.fullCharCodeAtPos(),L(t,!0)||t===92))return this.finishToken(a.privateId,this.readWord1());this.raise(this.pos,"Unexpected character \'"+B(t)+"\'")},x.getTokenFromCode=function(e){switch(e){case 46:return this.readToken_dot();case 40:return++this.pos,this.finishToken(a.parenL);case 41:return++this.pos,this.finishToken(a.parenR);case 59:return++this.pos,this.finishToken(a.semi);case 44:return++this.pos,this.finishToken(a.comma);case 91:return++this.pos,this.finishToken(a.bracketL);case 93:return++this.pos,this.finishToken(a.bracketR);case 123:return++this.pos,this.finishToken(a.braceL);case 125:return++this.pos,this.finishToken(a.braceR);case 58:return++this.pos,this.finishToken(a.colon);case 96:if(this.options.ecmaVersion<6)break;return++this.pos,this.finishToken(a.backQuote);case 48:var t=this.input.charCodeAt(this.pos+1);if(t===120||t===88)return this.readRadixNumber(16);if(this.options.ecmaVersion>=6){if(t===111||t===79)return this.readRadixNumber(8);if(t===98||t===66)return this.readRadixNumber(2)}case 49:case 50:case 51:case 52:case 53:case 54:case 55:case 56:case 57:return this.readNumber(!1);case 34:case 39:return this.readString(e);case 47:return this.readToken_slash();case 37:case 42:return this.readToken_mult_modulo_exp(e);case 124:case 38:return this.readToken_pipe_amp(e);case 94:return this.readToken_caret();case 43:case 45:return this.readToken_plus_min(e);case 60:case 62:return this.readToken_lt_gt(e);case 61:case 33:return this.readToken_eq_excl(e);case 63:return this.readToken_question();case 126:return this.finishOp(a.prefix,1);case 35:return this.readToken_numberSign()}this.raise(this.pos,"Unexpected character \'"+B(e)+"\'")},x.finishOp=function(e,t){var i=this.input.slice(this.pos,this.pos+t);return this.pos+=t,this.finishToken(e,i)},x.readRegexp=function(){for(var e,t,i=this.pos;;){this.pos>=this.input.length&&this.raise(i,"Unterminated regular expression");var s=this.input.charAt(this.pos);if(k.test(s)&&this.raise(i,"Unterminated regular expression"),e)e=!1;else{if(s==="[")t=!0;else if(s==="]"&&t)t=!1;else if(s==="/"&&!t)break;e=s==="\\\\"}++this.pos}var r=this.input.slice(i,this.pos);++this.pos;var n=this.pos,u=this.readWord1();this.containsEsc&&this.unexpected(n);var o=this.regexpState||(this.regexpState=new R(this));o.reset(i,r,u),this.validateRegExpFlags(o),this.validateRegExpPattern(o);var h=null;try{h=new RegExp(r,u)}catch{}return this.finishToken(a.regexp,{pattern:r,flags:u,value:h})},x.readInt=function(e,t,i){for(var s=this.options.ecmaVersion>=12&&t===void 0,r=i&&this.input.charCodeAt(this.pos)===48,n=this.pos,u=0,o=0,h=0,p=t??1/0;h=97?y=d-97+10:d>=65?y=d-65+10:d>=48&&d<=57?y=d-48:y=1/0,y>=e)break;o=d,u=u*e+y}return s&&o===95&&this.raiseRecoverable(this.pos-1,"Numeric separator is not allowed at the last of digits"),this.pos===n||t!=null&&this.pos-n!==t?null:u};function Zt(e,t){return t?parseInt(e,8):parseFloat(e.replace(/_/g,""))}function dt(e){return typeof BigInt!="function"?null:BigInt(e.replace(/_/g,""))}x.readRadixNumber=function(e){var t=this.pos;this.pos+=2;var i=this.readInt(e);return i==null&&this.raise(this.start+2,"Expected number in radix "+e),this.options.ecmaVersion>=11&&this.input.charCodeAt(this.pos)===110?(i=dt(this.input.slice(t,this.pos)),++this.pos):L(this.fullCharCodeAtPos())&&this.raise(this.pos,"Identifier directly after number"),this.finishToken(a.num,i)},x.readNumber=function(e){var t=this.pos;!e&&this.readInt(10,void 0,!0)===null&&this.raise(t,"Invalid number");var i=this.pos-t>=2&&this.input.charCodeAt(t)===48;i&&this.strict&&this.raise(t,"Invalid number");var s=this.input.charCodeAt(this.pos);if(!i&&!e&&this.options.ecmaVersion>=11&&s===110){var r=dt(this.input.slice(t,this.pos));return++this.pos,L(this.fullCharCodeAtPos())&&this.raise(this.pos,"Identifier directly after number"),this.finishToken(a.num,r)}i&&/[89]/.test(this.input.slice(t,this.pos))&&(i=!1),s===46&&!i&&(++this.pos,this.readInt(10),s=this.input.charCodeAt(this.pos)),(s===69||s===101)&&!i&&(s=this.input.charCodeAt(++this.pos),(s===43||s===45)&&++this.pos,this.readInt(10)===null&&this.raise(t,"Invalid number")),L(this.fullCharCodeAtPos())&&this.raise(this.pos,"Identifier directly after number");var n=Zt(this.input.slice(t,this.pos),i);return this.finishToken(a.num,n)},x.readCodePoint=function(){var e=this.input.charCodeAt(this.pos),t;if(e===123){this.options.ecmaVersion<6&&this.unexpected();var i=++this.pos;t=this.readHexChar(this.input.indexOf("}",this.pos)-this.pos),++this.pos,t>1114111&&this.invalidStringToken(i,"Code point out of bounds")}else t=this.readHexChar(4);return t},x.readString=function(e){for(var t="",i=++this.pos;;){this.pos>=this.input.length&&this.raise(this.start,"Unterminated string constant");var s=this.input.charCodeAt(this.pos);if(s===e)break;s===92?(t+=this.input.slice(i,this.pos),t+=this.readEscapedChar(!1),i=this.pos):s===8232||s===8233?(this.options.ecmaVersion<10&&this.raise(this.start,"Unterminated string constant"),++this.pos,this.options.locations&&(this.curLine++,this.lineStart=this.pos)):(q(s)&&this.raise(this.start,"Unterminated string constant"),++this.pos)}return t+=this.input.slice(i,this.pos++),this.finishToken(a.string,t)};var xt={};x.tryReadTemplateToken=function(){this.inTemplateElement=!0;try{this.readTmplToken()}catch(e){if(e===xt)this.readInvalidTemplateToken();else throw e}this.inTemplateElement=!1},x.invalidStringToken=function(e,t){if(this.inTemplateElement&&this.options.ecmaVersion>=9)throw xt;this.raise(e,t)},x.readTmplToken=function(){for(var e="",t=this.pos;;){this.pos>=this.input.length&&this.raise(this.start,"Unterminated template");var i=this.input.charCodeAt(this.pos);if(i===96||i===36&&this.input.charCodeAt(this.pos+1)===123)return this.pos===this.start&&(this.type===a.template||this.type===a.invalidTemplate)?i===36?(this.pos+=2,this.finishToken(a.dollarBraceL)):(++this.pos,this.finishToken(a.backQuote)):(e+=this.input.slice(t,this.pos),this.finishToken(a.template,e));if(i===92)e+=this.input.slice(t,this.pos),e+=this.readEscapedChar(!0),t=this.pos;else if(q(i)){switch(e+=this.input.slice(t,this.pos),++this.pos,i){case 13:this.input.charCodeAt(this.pos)===10&&++this.pos;case 10:e+=`\n`;break;default:e+=String.fromCharCode(i);break}this.options.locations&&(++this.curLine,this.lineStart=this.pos),t=this.pos}else++this.pos}},x.readInvalidTemplateToken=function(){for(;this.pos=48&&t<=55){var s=this.input.substr(this.pos-1,3).match(/^[0-7]+/)[0],r=parseInt(s,8);return r>255&&(s=s.slice(0,-1),r=parseInt(s,8)),this.pos+=s.length-1,t=this.input.charCodeAt(this.pos),(s!=="0"||t===56||t===57)&&(this.strict||e)&&this.invalidStringToken(this.pos-1-s.length,e?"Octal literal in template string":"Octal literal in strict mode"),String.fromCharCode(r)}return q(t)?(this.options.locations&&(this.lineStart=this.pos,++this.curLine),""):String.fromCharCode(t)}},x.readHexChar=function(e){var t=this.pos,i=this.readInt(16,e);return i===null&&this.invalidStringToken(t,"Bad character escape sequence"),i},x.readWord1=function(){this.containsEsc=!1;for(var e="",t=!0,i=this.pos,s=this.options.ecmaVersion>=6;this.pos {}; +const visitorsWithoutAncestors = { + ClassDeclaration(node, state, c) { + if (isTopLevelDeclaration(state)) { + state.prepend(node, `${node.id.name}=`); + state.hoistedDeclarationStatements.push(`let ${node.id.name}; `); + } + + walk.base.ClassDeclaration(node, state, c); + }, + ForOfStatement(node, state, c) { + if (node.await === true) { + state.containsAwait = true; + } + walk.base.ForOfStatement(node, state, c); + }, + FunctionDeclaration(node, state, c) { + state.prepend(node, `this.${node.id.name} = ${node.id.name}; `); + state.hoistedDeclarationStatements.push(`var ${node.id.name}; `); + }, + FunctionExpression: noop, + ArrowFunctionExpression: noop, + MethodDefinition: noop, + AwaitExpression(node, state, c) { + state.containsAwait = true; + walk.base.AwaitExpression(node, state, c); + }, + ReturnStatement(node, state, c) { + state.containsReturn = true; + walk.base.ReturnStatement(node, state, c); + }, + VariableDeclaration(node, state, c) { + const variableKind = node.kind; + const isIterableForDeclaration = ["ForOfStatement", "ForInStatement"].includes( + state.ancestors[state.ancestors.length - 2].type, + ); + + if (variableKind === "var" || isTopLevelDeclaration(state)) { + state.replace( + node.start, + node.start + variableKind.length + (isIterableForDeclaration ? 1 : 0), + variableKind === "var" && isIterableForDeclaration ? "" : "void" + (node.declarations.length === 1 ? "" : " ("), + ); + + if (!isIterableForDeclaration) { + node.declarations.forEach(decl => { + state.prepend(decl, "("); + state.append(decl, decl.init ? ")" : "=undefined)"); + }); + + if (node.declarations.length !== 1) { + state.append(node.declarations[node.declarations.length - 1], ")"); + } + } + + const variableIdentifiersToHoist = [ + ["var", []], + ["let", []], + ]; + function registerVariableDeclarationIdentifiers(node) { + switch (node.type) { + case "Identifier": + variableIdentifiersToHoist[variableKind === "var" ? 0 : 1][1].push(node.name); + break; + case "ObjectPattern": + node.properties.forEach(property => { + registerVariableDeclarationIdentifiers(property.value || property.argument); + }); + break; + case "ArrayPattern": + node.elements.forEach(element => { + registerVariableDeclarationIdentifiers(element); + }); + break; + } + } + + node.declarations.forEach(decl => { + registerVariableDeclarationIdentifiers(decl.id); + }); + + variableIdentifiersToHoist.forEach(({ 0: kind, 1: identifiers }) => { + if (identifiers.length > 0) { + state.hoistedDeclarationStatements.push(`${kind} ${identifiers.join(", ")}; `); + } + }); + } + + walk.base.VariableDeclaration(node, state, c); + }, +}; + +const visitors = {}; +for (const nodeType of Object.keys(walk.base)) { + const callback = visitorsWithoutAncestors[nodeType] || walk.base[nodeType]; + visitors[nodeType] = (node, state, c) => { + const isNew = node !== state.ancestors[state.ancestors.length - 1]; + if (isNew) { + state.ancestors.push(node); + } + callback(node, state, c); + if (isNew) { + state.ancestors.pop(); + } + }; +} + +// Hoisted from Node's inline literal: builtin-parser.ts only recognises `/` +// as regex-start after `[(,=;:{]|return|=>`, so an inline regex here (after +// `+` in the upstream `.replace(...)` chain) would silently truncate the +// bundled module. bundle-modules.ts asserts on that truncation now. +const kParenMessageRe = / \([^)]+\)/; + +function processTopLevelAwait(src) { + const wrapPrefix = "(async () => { "; + const wrapped = `${wrapPrefix}${src} })()`; + const wrappedArray = wrapped.split(""); + let root; + try { + root = parser.parse(wrapped, { ecmaVersion: "latest" }); + } catch (e) { + if (e.message.startsWith("Unterminated ")) throw new Recoverable(e); + // If the parse error is before the first "await", then use the execution + // error. Otherwise we must emit this parse error, making it look like a + // proper syntax error. + const awaitPos = src.indexOf("await"); + const errPos = e.pos - wrapPrefix.length; + if (awaitPos > errPos) return null; + // Convert keyword parse errors on await into their original errors when + // possible. + if (errPos === awaitPos + 6 && e.message.includes("Expecting Unicode escape sequence")) return null; + if (errPos === awaitPos + 7 && e.message.includes("Unexpected token")) return null; + const line = e.loc.line; + const column = line === 1 ? e.loc.column - wrapPrefix.length : e.loc.column; + let message = + "\n" + + src.split("\n", line)[line - 1] + + "\n" + + " ".repeat(column) + + "^\n\n" + + kParenMessageRe[Symbol.replace](e.message, ""); + // V8 unexpected token errors include the token string. + if (message.endsWith("Unexpected token")) + message += + " '" + + // Wrapper end may cause acorn to report error position after the source + (src[e.pos - wrapPrefix.length] ?? src[src.length - 1]) + + "'"; + throw new SyntaxError(message); + } + const body = root.body[0].expression.callee.body; + const state = { + body, + ancestors: [], + hoistedDeclarationStatements: [], + replace(from, to, str) { + for (let i = from; i < to; i++) { + wrappedArray[i] = ""; + } + if (from === to) str += wrappedArray[from]; + wrappedArray[from] = str; + }, + prepend(node, str) { + wrappedArray[node.start] = str + wrappedArray[node.start]; + }, + append(node, str) { + wrappedArray[node.end - 1] += str; + }, + containsAwait: false, + containsReturn: false, + }; + + walk.recursive(body, state, visitors); + + // Do not transform if + // 1. False alarm: there isn't actually an await expression. + // 2. There is a top-level return, which is not allowed. + if (!state.containsAwait || state.containsReturn) { + return null; + } + + for (let i = body.body.length - 1; i >= 0; i--) { + const node = body.body[i]; + if (node.type === "EmptyStatement") continue; + if (node.type === "ExpressionStatement") { + // For an expression statement of the form + // ( expr ) ; + // ^^^^^^^^^^ // node + // ^^^^ // node.expression + // + // We do not want the left parenthesis before the `return` keyword; + // therefore we prepend the `return (` to `node`. + // + // On the other hand, we do not want the right parenthesis after the + // semicolon. Since there can only be more right parentheses between + // node.expression.end and the semicolon, appending one more to + // node.expression should be fine. + // + // We also create a wrapper object around the result of the expression. + // Consider an expression of the form `(await x).y`. If we just return + // this expression from an async function, the caller will await `y`, too, + // if it evaluates to a Promise. Instead, we return + // `{ value: ((await x).y) }`, which allows the caller to retrieve the + // awaited value correctly. + state.prepend(node.expression, "{ value: ("); + state.prepend(node, "return "); + state.append(node.expression, ") }"); + } + break; + } + + return state.hoistedDeclarationStatements.join("") + wrappedArray.join(""); +} + +export default { + processTopLevelAwait, +}; diff --git a/src/js/internal/repl/completion.js b/src/js/internal/repl/completion.js new file mode 100644 index 000000000000..b987f39ef8b1 --- /dev/null +++ b/src/js/internal/repl/completion.js @@ -0,0 +1,839 @@ +// Ported from Node.js v26.3.0 lib/internal/repl/completion.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 { + ArrayPrototypeFilter, + ArrayPrototypeForEach, + ArrayPrototypeIncludes, + ArrayPrototypeJoin, + ArrayPrototypeMap, + ArrayPrototypePop, + ArrayPrototypePush, + ArrayPrototypePushApply, + ArrayPrototypeShift, + ArrayPrototypeSlice, + ArrayPrototypeSome, + ArrayPrototypeSort, + ArrayPrototypeUnshift, + ObjectGetOwnPropertyDescriptor, + ObjectGetPrototypeOf, + ObjectKeys, + ReflectApply, + RegExpPrototypeExec, + SafeSet, + StringPrototypeCodePointAt, + StringPrototypeEndsWith, + StringPrototypeIncludes, + StringPrototypeSlice, + StringPrototypeSplit, + StringPrototypeStartsWith, + StringPrototypeToLocaleLowerCase, + StringPrototypeTrimStart, +} = primordials; + +const { + kContextId, + getREPLResourceName, + globalBuiltins, + getReplBuiltinLibs, + fixReplRequire, +} = require("internal/repl/utils"); + +const { sendInspectorCommand } = require("internal/repl/node-shims"); + +const { isProxy } = require("internal/repl/node-shims"); + +const CJSModule = require("internal/repl/node-shims").Module; + +const { extensionFormatMap } = require("internal/repl/node-shims"); + +const path = require("node:path"); +const fs = require("node:fs"); + +const { + constants: { ALL_PROPERTIES, SKIP_SYMBOLS }, + getOwnNonIndexProperties, +} = require("internal/repl/node-shims"); + +// Lazy: don't destructure — see internal/repl/acorn.js. +const acorn = require("internal/repl/acorn"); +const acornWalk = require("internal/repl/acorn-walk"); + +const importRE = /\bimport\s*\(\s*['"`](([\w@./:-]+\/)?(?:[\w@./:-]*))(?![^'"`])$/; +const requireRE = /\brequire\s*\(\s*['"`](([\w@./:-]+\/)?(?:[\w@./:-]*))(?![^'"`])$/; +const fsAutoCompleteRE = /fs(?:\.promises)?\.\s*[a-z][a-zA-Z]+\(\s*["'](.*)/; +const versionedFileNamesRe = /-\d+\.\d+/; + +fixReplRequire(__node_module__); + +const { BuiltinModule } = require("internal/repl/node-shims"); + +const nodeSchemeBuiltinLibs = ArrayPrototypeMap(getReplBuiltinLibs(), lib => `node:${lib}`); +ArrayPrototypeForEach(BuiltinModule.getSchemeOnlyModuleNames(), lib => + ArrayPrototypePush(nodeSchemeBuiltinLibs, `node:${lib}`), +); + +function isIdentifier(str) { + if (str === "") { + return false; + } + const first = StringPrototypeCodePointAt(str, 0); + if (!acorn.isIdentifierStart(first)) { + return false; + } + const firstLen = first > 0xffff ? 2 : 1; + for (let i = firstLen; i < str.length; i += 1) { + const cp = StringPrototypeCodePointAt(str, i); + if (!acorn.isIdentifierChar(cp)) { + return false; + } + if (cp > 0xffff) { + i += 1; + } + } + return true; +} + +function isNotLegacyObjectPrototypeMethod(str) { + return ( + isIdentifier(str) && + str !== "__defineGetter__" && + str !== "__defineSetter__" && + str !== "__lookupGetter__" && + str !== "__lookupSetter__" + ); +} + +function getGlobalLexicalScopeNames(contextId) { + return sendInspectorCommand( + session => { + let names = []; + session.post( + "Runtime.globalLexicalScopeNames", + { + executionContextId: contextId, + }, + (error, result) => { + if (!error) names = result.names; + }, + ); + return names; + }, + () => [], + ); +} + +function filteredOwnPropertyNames(obj) { + if (!obj) return []; + // `Object.prototype` is the only non-contrived object that fulfills + // `Object.getPrototypeOf(X) === null && + // Object.getPrototypeOf(Object.getPrototypeOf(X.constructor)) === X`. + let isObjectPrototype = false; + if (ObjectGetPrototypeOf(obj) === null) { + const ctorDescriptor = ObjectGetOwnPropertyDescriptor(obj, "constructor"); + if (ctorDescriptor?.value) { + const ctorProto = ObjectGetPrototypeOf(ctorDescriptor.value); + isObjectPrototype = ctorProto && ObjectGetPrototypeOf(ctorProto) === obj; + } + } + const filter = ALL_PROPERTIES | SKIP_SYMBOLS; + return ArrayPrototypeFilter( + getOwnNonIndexProperties(obj, filter), + isObjectPrototype ? isNotLegacyObjectPrototypeMethod : isIdentifier, + ); +} + +function addCommonWords(completionGroups) { + // Only words which do not yet exist as global property should be added to + // this list. + ArrayPrototypePush(completionGroups, [ + "async", + "await", + "break", + "case", + "catch", + "const", + "continue", + "debugger", + "default", + "delete", + "do", + "else", + "export", + "false", + "finally", + "for", + "function", + "if", + "import", + "in", + "instanceof", + "let", + "new", + "null", + "return", + "switch", + "this", + "throw", + "true", + "try", + "typeof", + "var", + "void", + "while", + "with", + "yield", + ]); +} + +function gracefulReaddir(...args) { + try { + return ReflectApply(fs.readdirSync, null, args); + } catch { + // Continue regardless of error. + } +} + +function completeFSFunctions(match) { + let baseName = ""; + let filePath = match[1]; + let fileList = gracefulReaddir(filePath, { withFileTypes: true }); + + if (!fileList) { + baseName = path.basename(filePath); + filePath = path.dirname(filePath); + fileList = gracefulReaddir(filePath, { withFileTypes: true }) || []; + } + + const completions = ArrayPrototypeMap( + ArrayPrototypeFilter(fileList, dirent => StringPrototypeStartsWith(dirent.name, baseName)), + d => d.name, + ); + + return [[completions], baseName]; +} + +// Provide a list of completions for the given leading text. This is +// given to the readline interface for handling tab completion. +// +// Example: +// complete('let foo = util.') +// -> [['util.print', 'util.debug', 'util.log', 'util.inspect'], +// 'util.' ] +// +// Warning: This evals code like "foo.bar.baz", so it could run property +// getter code. To avoid potential triggering side-effects with getters the completion +// logic is skipped when getters or proxies are involved in the expression. +// (see: https://github.com/nodejs/node/issues/57829). +function complete(line, callback) { + // List of completion lists, one for each inheritance "level" + let completionGroups = []; + let completeOn, group; + + // Ignore right whitespace. It could change the outcome. + line = StringPrototypeTrimStart(line); + + let filter = ""; + + let match; + // REPL commands (e.g. ".break"). + if ((match = RegExpPrototypeExec(/^\s*\.(\w*)$/, line)) !== null) { + ArrayPrototypePush(completionGroups, ObjectKeys(this.commands)); + completeOn = match[1]; + if (completeOn.length) { + filter = completeOn; + } + } else if ((match = RegExpPrototypeExec(requireRE, line)) !== null) { + // require("...") + completeOn = match[1]; + filter = completeOn; + if (this.allowBlockingCompletions) { + const subdir = match[2] || ""; + const extensions = ObjectKeys(CJSModule._extensions); + const indexes = ArrayPrototypeMap(extensions, extension => `index${extension}`); + ArrayPrototypePush(indexes, "package.json", "index"); + + group = []; + let paths = []; + + if (completeOn === ".") { + group = ["./", "../"]; + } else if (completeOn === "..") { + group = ["../"]; + } else if (RegExpPrototypeExec(/^\.\.?\//, completeOn) !== null) { + paths = [process.cwd()]; + } else { + paths = []; + ArrayPrototypePushApply(paths, __node_module__.paths); + ArrayPrototypePushApply(paths, CJSModule.globalPaths); + } + + ArrayPrototypeForEach(paths, dir => { + dir = path.resolve(dir, subdir); + const dirents = gracefulReaddir(dir, { withFileTypes: true }) || []; + ArrayPrototypeForEach(dirents, dirent => { + if (RegExpPrototypeExec(versionedFileNamesRe, dirent.name) !== null || dirent.name === ".npm") { + // Exclude versioned names that 'npm' installs. + return; + } + const extension = path.extname(dirent.name); + const base = StringPrototypeSlice(dirent.name, 0, -extension.length); + if (!dirent.isDirectory()) { + if (StringPrototypeIncludes(extensions, extension) && (!subdir || base !== "index")) { + ArrayPrototypePush(group, `${subdir}${base}`); + } + return; + } + ArrayPrototypePush(group, `${subdir}${dirent.name}/`); + const absolute = path.resolve(dir, dirent.name); + if ( + ArrayPrototypeSome(gracefulReaddir(absolute) || [], subfile => ArrayPrototypeIncludes(indexes, subfile)) + ) { + ArrayPrototypePush(group, `${subdir}${dirent.name}`); + } + }); + }); + if (group.length) { + ArrayPrototypePush(completionGroups, group); + } + } + + ArrayPrototypePush(completionGroups, getReplBuiltinLibs(), nodeSchemeBuiltinLibs); + } else if ((match = RegExpPrototypeExec(importRE, line)) !== null) { + // import('...') + completeOn = match[1]; + filter = completeOn; + if (this.allowBlockingCompletions) { + const subdir = match[2] || ""; + // File extensions that can be imported: + const extensions = ObjectKeys(extensionFormatMap); + + // Only used when loading bare module specifiers from `node_modules`: + const indexes = ArrayPrototypeMap(extensions, ext => `index${ext}`); + ArrayPrototypePush(indexes, "package.json"); + + group = []; + let paths = []; + if (completeOn === ".") { + group = ["./", "../"]; + } else if (completeOn === "..") { + group = ["../"]; + } else if (RegExpPrototypeExec(/^\.\.?\//, completeOn) !== null) { + paths = [process.cwd()]; + } else { + paths = ArrayPrototypeSlice(__node_module__.paths); + } + + ArrayPrototypeForEach(paths, dir => { + dir = path.resolve(dir, subdir); + const isInNodeModules = path.basename(dir) === "node_modules"; + const dirents = gracefulReaddir(dir, { withFileTypes: true }) || []; + ArrayPrototypeForEach(dirents, dirent => { + const { name } = dirent; + if (RegExpPrototypeExec(versionedFileNamesRe, name) !== null || name === ".npm") { + // Exclude versioned names that 'npm' installs. + return; + } + + if (!dirent.isDirectory()) { + const extension = path.extname(name); + if (StringPrototypeIncludes(extensions, extension)) { + ArrayPrototypePush(group, `${subdir}${name}`); + } + return; + } + + ArrayPrototypePush(group, `${subdir}${name}/`); + if (!subdir && isInNodeModules) { + const absolute = path.resolve(dir, name); + const subfiles = gracefulReaddir(absolute) || []; + if ( + ArrayPrototypeSome(subfiles, subfile => { + return ArrayPrototypeIncludes(indexes, subfile); + }) + ) { + ArrayPrototypePush(group, `${subdir}${name}`); + } + } + }); + }); + + if (group.length) { + ArrayPrototypePush(completionGroups, group); + } + } + + ArrayPrototypePush(completionGroups, getReplBuiltinLibs(), nodeSchemeBuiltinLibs); + } else if ((match = RegExpPrototypeExec(fsAutoCompleteRE, line)) !== null && this.allowBlockingCompletions) { + ({ 0: completionGroups, 1: completeOn } = completeFSFunctions(match)); + } else if (line.length === 0 || RegExpPrototypeExec(/\w|\.|\$/, line[line.length - 1]) !== null) { + const completeTarget = line.length === 0 ? line : findExpressionCompleteTarget(line); + + if (line.length !== 0 && !completeTarget) { + completionGroupsLoaded(); + return; + } + let expr = ""; + completeOn = completeTarget; + if (StringPrototypeEndsWith(line, ".")) { + expr = StringPrototypeSlice(completeTarget, 0, -1); + } else if (line.length !== 0) { + const bits = StringPrototypeSplit(completeTarget, "."); + filter = ArrayPrototypePop(bits); + expr = ArrayPrototypeJoin(bits, "."); + } + + // Resolve expr and get its completions. + if (!expr) { + // Get global vars synchronously + ArrayPrototypePush(completionGroups, getGlobalLexicalScopeNames(this[kContextId])); + let contextProto = this.context; + while ((contextProto = ObjectGetPrototypeOf(contextProto)) !== null) { + ArrayPrototypePush(completionGroups, filteredOwnPropertyNames(contextProto)); + } + const contextOwnNames = filteredOwnPropertyNames(this.context); + if (!this.useGlobal) { + // When the context is not `global`, builtins are not own + // properties of it. + // `globalBuiltins` is a `SafeSet`, not an Array-like. + ArrayPrototypePush(contextOwnNames, ...globalBuiltins); + } + ArrayPrototypePush(completionGroups, contextOwnNames); + if (filter !== "") addCommonWords(completionGroups); + completionGroupsLoaded(); + return; + } + + // If the target ends with a dot (e.g. `obj.foo.`) such code won't be valid for AST parsing + // so in order to make it correct we add an identifier to its end (e.g. `obj.foo.x`) + const parsableCompleteTarget = completeTarget.endsWith(".") ? `${completeTarget}x` : completeTarget; + + let completeTargetAst; + try { + completeTargetAst = acorn.parse(parsableCompleteTarget, { + __proto__: null, + sourceType: "module", + ecmaVersion: "latest", + }); + } catch { + /* No need to specifically handle parse errors */ + } + + if (!completeTargetAst) { + return completionGroupsLoaded(); + } + + // Destructuring keeps the "eval" property name out of member-access + // position: JSC's assertion-enabled builtin parser rejects `x.eval` / + // `x["eval"]` inside builtin sources, and minify-syntax would fold a + // bracket access back into dot form. + const { eval: evalFn } = this; + + return includesProxiesOrGetters( + completeTargetAst.body[0].expression, + parsableCompleteTarget, + evalFn, + this.context, + includes => { + if (includes) { + // The expression involves proxies or getters, meaning that it + // can trigger side-effectful behaviors, so bail out + return completionGroupsLoaded(); + } + + let chaining = "."; + if (StringPrototypeEndsWith(expr, "?")) { + expr = StringPrototypeSlice(expr, 0, -1); + chaining = "?."; + } + + const memberGroups = []; + const evalExpr = `try { ${expr} } catch {}`; + // ReflectApply keeps `this` bound like `this.eval(...)` would. + ReflectApply(evalFn, this, [ + evalExpr, + this.context, + getREPLResourceName(), + (e, obj) => { + try { + let p; + if ((typeof obj === "object" && obj !== null) || typeof obj === "function") { + ArrayPrototypePush(memberGroups, filteredOwnPropertyNames(obj)); + p = ObjectGetPrototypeOf(obj); + } else { + p = obj.constructor ? obj.constructor.prototype : null; + } + // Circular refs possible? Let's guard against that. + let sentinel = 5; + while (p !== null && sentinel-- !== 0) { + ArrayPrototypePush(memberGroups, filteredOwnPropertyNames(p)); + p = ObjectGetPrototypeOf(p); + } + } catch { + // Maybe a Proxy object without `getOwnPropertyNames` trap. + // We simply ignore it here, as we don't want to break the + // autocompletion. Fixes the bug + // https://github.com/nodejs/node/issues/2119 + } + + if (memberGroups.length) { + expr += chaining; + ArrayPrototypeForEach(memberGroups, group => { + ArrayPrototypePush( + completionGroups, + ArrayPrototypeMap(group, member => `${expr}${member}`), + ); + }); + filter &&= `${expr}${filter}`; + } + + completionGroupsLoaded(); + }, + ]); + }, + ); + } + + return completionGroupsLoaded(); + + // Will be called when all completionGroups are in place + // Useful for async autocompletion + function completionGroupsLoaded() { + // Filter, sort (within each group), uniq and merge the completion groups. + if (completionGroups.length && filter) { + const newCompletionGroups = []; + const lowerCaseFilter = StringPrototypeToLocaleLowerCase(filter); + ArrayPrototypeForEach(completionGroups, group => { + const filteredGroup = ArrayPrototypeFilter(group, str => { + // Filter is always case-insensitive following chromium autocomplete + // behavior. + return StringPrototypeStartsWith(StringPrototypeToLocaleLowerCase(str), lowerCaseFilter); + }); + if (filteredGroup.length) { + ArrayPrototypePush(newCompletionGroups, filteredGroup); + } + }); + completionGroups = newCompletionGroups; + } + + const completions = []; + // Unique completions across all groups. + const uniqueSet = new SafeSet(); + uniqueSet.add(""); + // Completion group 0 is the "closest" (least far up the inheritance + // chain) so we put its completions last: to be closest in the REPL. + ArrayPrototypeForEach(completionGroups, group => { + ArrayPrototypeSort(group, (a, b) => (b > a ? 1 : -1)); + const setSize = uniqueSet.size; + ArrayPrototypeForEach(group, entry => { + if (!uniqueSet.has(entry)) { + ArrayPrototypeUnshift(completions, entry); + uniqueSet.add(entry); + } + }); + // Add a separator between groups. + if (uniqueSet.size !== setSize) { + ArrayPrototypeUnshift(completions, ""); + } + }); + + // Remove obsolete group entry, if present. + if (completions[0] === "") { + ArrayPrototypeShift(completions); + } + + callback(null, [completions, completeOn]); + } +} + +/** + * This function tries to extract a target for tab completion from code representing an expression. + * + * Such target is basically the last piece of the expression that can be evaluated for the potential + * tab completion. + * + * Some examples: + * - The complete target for `const a = obj.b` is `obj.b` + * (because tab completion will evaluate and check the `obj.b` object) + * - The complete target for `tru` is `tru` + * (since we'd ideally want to complete that to `true`) + * - The complete target for `{ a: tru` is `tru` + * (like the last example, we'd ideally want that to complete to true) + * - There is no complete target for `{ a: true }` + * (there is nothing to complete) + * @param {string} code the code representing the expression to analyze + * @returns {string|null} a substring of the code representing the complete target is there was one, `null` otherwise + */ +function findExpressionCompleteTarget(code) { + if (!code) { + return null; + } + + if (code.at(-1) === ".") { + if (code.at(-2) === "?") { + // The code ends with the optional chaining operator (`?.`), + // such code can't generate a valid AST so we need to strip + // the suffix, run this function's logic and add back the + // optional chaining operator to the result if present + const result = findExpressionCompleteTarget(code.slice(0, -2)); + return !result ? result : `${result}?.`; + } + + // The code ends with a dot, such code can't generate a valid AST + // so we need to strip the suffix, run this function's logic and + // add back the dot to the result if present + const result = findExpressionCompleteTarget(code.slice(0, -1)); + return !result ? result : `${result}.`; + } + + let ast; + try { + ast = acorn.parse(code, { __proto__: null, sourceType: "module", ecmaVersion: "latest" }); + } catch { + const keywords = code.split(" "); + + if (keywords.length > 1) { + // Something went wrong with the parsing, however this can be due to incomplete code + // (that is for example missing a closing bracket, as for example `{ a: obj.te`), in + // this case we take the last code keyword and try again + // upstream-todo(dario-piotrowicz): make this more robust, right now we only split by spaces + // but that's not always enough, for example it doesn't handle + // this code: `{ a: obj['hello world'].te` + return findExpressionCompleteTarget(keywords.at(-1)); + } + + // The ast parsing has legitimately failed so we return null + return null; + } + + const lastBodyStatement = ast.body[ast.body.length - 1]; + + if (!lastBodyStatement) { + return null; + } + + // If the last statement is a block we know there is not going to be a potential + // completion target (e.g. in `{ a: true }` there is no completion to be done) + if (lastBodyStatement.type === "BlockStatement") { + return null; + } + + // If the last statement is an expression and it has a right side, that's what we + // want to potentially complete on, so let's re-run the function's logic on that + if (lastBodyStatement.type === "ExpressionStatement" && lastBodyStatement.expression.right) { + const exprRight = lastBodyStatement.expression.right; + const exprRightCode = code.slice(exprRight.start, exprRight.end); + return findExpressionCompleteTarget(exprRightCode); + } + + // If the last statement is a variable declaration statement the last declaration is + // what we can potentially complete on, so let's re-run the function's logic on that + if (lastBodyStatement.type === "VariableDeclaration") { + const lastDeclarationInit = lastBodyStatement.declarations.at(-1).init; + if (!lastDeclarationInit) { + // If there is no initialization we can simply return + return null; + } + const lastDeclarationInitCode = code.slice(lastDeclarationInit.start, lastDeclarationInit.end); + return findExpressionCompleteTarget(lastDeclarationInitCode); + } + + // If the last statement is an expression statement with a unary operator (delete, typeof, etc.) + // we want to extract the argument for completion (e.g. for `delete obj.prop` we want `obj.prop`) + if ( + lastBodyStatement.type === "ExpressionStatement" && + lastBodyStatement.expression.type === "UnaryExpression" && + lastBodyStatement.expression.argument + ) { + const argument = lastBodyStatement.expression.argument; + const argumentCode = code.slice(argument.start, argument.end); + return findExpressionCompleteTarget(argumentCode); + } + + // If the last statement is an expression statement with "new" syntax + // we want to extract the callee for completion (e.g. for `new Sample` we want `Sample`) + if ( + lastBodyStatement.type === "ExpressionStatement" && + lastBodyStatement.expression.type === "NewExpression" && + lastBodyStatement.expression.callee + ) { + const callee = lastBodyStatement.expression.callee; + const calleeCode = code.slice(callee.start, callee.end); + return findExpressionCompleteTarget(calleeCode); + } + + // Walk the AST for the current block of code, and check whether it contains any + // statement or expression type that would potentially have side effects if evaluated. + let isAllowed = true; + const disallow = () => (isAllowed = false); + acornWalk.simple(lastBodyStatement, { + ForInStatement: disallow, + ForOfStatement: disallow, + CallExpression: disallow, + AssignmentExpression: disallow, + UpdateExpression: disallow, + }); + if (!isAllowed) { + return null; + } + + // If any of the above early returns haven't activated then it means that + // the potential complete target is the full code (e.g. the code represents + // a simple partial identifier, a member expression, etc...) + return code.slice(lastBodyStatement.start, lastBodyStatement.end); +} + +/** + * Utility used to determine if an expression includes object getters or proxies. + * + * Example: given `obj.foo`, the function lets you know if `foo` has a getter function + * associated to it, or if `obj` is a proxy + * @param {any} expr The expression, in AST format to analyze + * @param {string} exprStr The string representation of the expression + * @param {(str: string, ctx: any, resourceName: string, cb: (error, evaled) => void) => void} evalFn + * Eval function to use + * @param {any} ctx The context to use for any code evaluation + * @param {(includes: boolean) => void} callback Callback that will be called with the result of the operation + * @returns {void} + */ +function includesProxiesOrGetters(expr, exprStr, evalFn, ctx, callback) { + if (expr?.type !== "MemberExpression") { + // If the expression is not a member one for obvious reasons no getters are involved + return callback(false); + } + + if (expr.object.type === "MemberExpression") { + // The object itself is a member expression, so we need to recurse (e.g. the expression is `obj.foo.bar`) + return includesProxiesOrGetters( + expr.object, + exprStr.slice(0, expr.object.end), + evalFn, + ctx, + (includes, lastEvaledObj) => { + if (includes) { + // If the recurred call found a getter we can also terminate + return callback(includes); + } + + if (isProxy(lastEvaledObj)) { + return callback(true); + } + + // If a getter/proxy hasn't been found by the recursion call we need to check if maybe a getter/proxy + // is present here (e.g. in `obj.foo.bar` we found that `obj.foo` doesn't involve any getters so we now + // need to check if `bar` on `obj.foo` (i.e. `lastEvaledObj`) has a getter or if `obj.foo.bar` is a proxy) + return hasGetterOrIsProxy(lastEvaledObj, expr.property, doesHaveGetterOrIsProxy => { + return callback(doesHaveGetterOrIsProxy); + }); + }, + ); + } + + // This is the base of the recursion we have an identifier for the object and an identifier or literal + // for the property (e.g. we have `obj.foo` or `obj['foo']`, `obj` is the object identifier and `foo` + // is the property identifier/literal) + if (expr.object.type === "Identifier") { + return evalFn(`try { ${expr.object.name} } catch {}`, ctx, getREPLResourceName(), (err, obj) => { + if (err) { + return callback(false); + } + + if (isProxy(obj)) { + return callback(true); + } + + return hasGetterOrIsProxy(obj, expr.property, doesHaveGetterOrIsProxy => { + if (doesHaveGetterOrIsProxy) { + return callback(true); + } + + return evalFn(`try { ${exprStr} } catch {} `, ctx, getREPLResourceName(), (err, obj) => { + if (err) { + return callback(false); + } + return callback(false, obj); + }); + }); + }); + } + + /** + * Utility to see if a property has a getter associated to it or if + * the property itself is a proxy object. + * @returns {void} + */ + function hasGetterOrIsProxy(obj, astProp, cb) { + if (!obj || !astProp) { + return cb(false); + } + + if (astProp.type === "Literal") { + // We have something like `obj['foo'].x` where `x` is the literal + return propHasGetterOrIsProxy(obj, astProp.value, cb); + } + + if (astProp.type === "Identifier" && exprStr.at(astProp.start - 1) === ".") { + // We have something like `obj.foo.x` where `foo` is the identifier + return propHasGetterOrIsProxy(obj, astProp.name, cb); + } + + return evalFn( + // Note: this eval runs the property expression, which might be side-effectful, for example + // the user could be running `obj[getKey()].` where `getKey()` has some side effects. + // Arguably this behavior should not be too surprising, but if it turns out that it is, + // then we can revisit this behavior and add logic to analyze the property expression + // and eval it only if we can confidently say that it can't have any side effects + `try { ${exprStr.slice(astProp.start, astProp.end)} } catch {} `, + ctx, + getREPLResourceName(), + (err, evaledProp) => { + if (err) { + return cb(false); + } + + if (typeof evaledProp === "string") { + return propHasGetterOrIsProxy(obj, evaledProp, cb); + } + + return cb(false); + }, + ); + } + + return callback(false); +} + +/** + * Given an object and a property name, checks whether the property has a getter, if not checks whether its + * value is a proxy. + * + * Note: the order is relevant here, we want to check whether the property has a getter _before_ we check + * whether its value is a proxy, to ensure that is the property does have a getter we don't end up + * triggering it when checking its value + * @param {any} obj The target object + * @param {string | number | bigint | boolean | RegExp} prop The target property + * @param {(includes: boolean) => void} cb Callback that will be called with the result of the operation + * @returns {void} + */ +function propHasGetterOrIsProxy(obj, prop, cb) { + const propDescriptor = ObjectGetOwnPropertyDescriptor(obj, prop); + const propHasGetter = typeof propDescriptor?.get === "function"; + if (propHasGetter) { + return cb(true); + } + + if (isProxy(obj[prop])) { + return cb(true); + } + + return cb(false); +} + +__node_module__.exports = { + complete, +}; + +export default __node_module__.exports; diff --git a/src/js/internal/repl/history.js b/src/js/internal/repl/history.js new file mode 100644 index 000000000000..9a857a6257e8 --- /dev/null +++ b/src/js/internal/repl/history.js @@ -0,0 +1,448 @@ +// Ported from Node.js v26.3.0 lib/internal/repl/history.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 { + ArrayPrototypeIndexOf, + ArrayPrototypeJoin, + ArrayPrototypePop, + ArrayPrototypeShift, + ArrayPrototypeSplice, + ArrayPrototypeUnshift, + Boolean, + RegExpPrototypeSymbolSplit, + StringPrototypeStartsWith, + StringPrototypeTrim, + Symbol, +} = primordials; + +const { validateNumber, validateArray } = require("internal/validators"); + +const path = require("node:path"); +const fs = require("node:fs"); +const os = require("node:os"); +let debug = require("internal/repl/node-shims").debuglog("repl", fn => { + debug = fn; +}); +const permission = require("internal/repl/node-shims"); +const { clearTimeout, setTimeout } = require("node:timers"); +const { reverseString } = require("internal/readline/utils"); + +// The debounce is to guard against code pasted into the REPL. +const kDebounceHistoryMS = 15; +const kHistorySize = 30; + +// Class fields +const kTimer = Symbol("_kTimer"); +const kWriting = Symbol("_kWriting"); +const kPending = Symbol("_kPending"); +const kRemoveHistoryDuplicates = Symbol("_kRemoveHistoryDuplicates"); +const kHistoryHandle = Symbol("_kHistoryHandle"); +const kHistoryPath = Symbol("_kHistoryPath"); +const kContext = Symbol("_kContext"); +const kIsFlushing = Symbol("_kIsFlushing"); +const kHistory = Symbol("_kHistory"); +const kSize = Symbol("_kSize"); +const kIndex = Symbol("_kIndex"); + +// Class methods +const kNormalizeLineEndings = Symbol("_kNormalizeLineEndings"); +const kWriteToOutput = Symbol("_kWriteToOutput"); +const kOnLine = Symbol("_kOnLine"); +const kOnExit = Symbol("_kOnExit"); +const kInitializeHistory = Symbol("_kInitializeHistory"); +const kHandleHistoryInitError = Symbol("_kHandleHistoryInitError"); +const kHasWritePermission = Symbol("_kHasWritePermission"); +const kValidateOptions = Symbol("_kValidateOptions"); +const kResolveHistoryPath = Symbol("_kResolveHistoryPath"); +const kReplHistoryMessage = Symbol("_kReplHistoryMessage"); +const kFlushHistory = Symbol("_kFlushHistory"); +const kGetHistoryPath = Symbol("_kGetHistoryPath"); +const kCloseHandle = Symbol("_kCloseHandle"); + +class ReplHistory { + constructor(context, options) { + this[kValidateOptions](options); + + this[kHistoryPath] = ReplHistory[kGetHistoryPath](options); + this[kContext] = context; + this[kTimer] = null; + this[kWriting] = false; + this[kPending] = false; + this[kRemoveHistoryDuplicates] = options.removeHistoryDuplicates || false; + this[kHistoryHandle] = null; + this[kIsFlushing] = false; + this[kSize] = options.size ?? context.historySize ?? kHistorySize; + this[kHistory] = options.history ?? []; + this[kIndex] = -1; + } + + initialize(onReadyCallback) { + // Empty string disables persistent history + if (this[kHistoryPath] === "") { + // Save a reference to the context's original _historyPrev + this.historyPrev = this[kContext]._historyPrev; + this[kContext]._historyPrev = this[kReplHistoryMessage].bind(this); + return onReadyCallback(null, this[kContext]); + } + + const resolvedPath = this[kResolveHistoryPath](); + if (!resolvedPath) { + ReplHistory[kWriteToOutput]( + this[kContext], + "\nError: Could not get the home directory.\n" + "REPL session history will not be persisted.\n", + ); + + // Save a reference to the context's original _historyPrev + this.historyPrev = this[kContext]._historyPrev; + this[kContext]._historyPrev = this[kReplHistoryMessage].bind(this); + return onReadyCallback(null, this[kContext]); + } + + if (!this[kHasWritePermission]()) { + ReplHistory[kWriteToOutput]( + this[kContext], + "\nAccess to FileSystemWrite is restricted.\n" + "REPL session history will not be persisted.\n", + ); + return onReadyCallback(null, this[kContext]); + } + + this[kContext].pause(); + + this[kInitializeHistory](onReadyCallback).catch(err => { + this[kHandleHistoryInitError](err, onReadyCallback); + }); + } + + addHistory(isMultiline, lastCommandErrored) { + const line = this[kContext].line; + + if (line.length === 0) return ""; + + // If the history is disabled then return the line + if (this[kSize] === 0) return line; + + // If the trimmed line is empty then return the line + if (StringPrototypeTrim(line).length === 0) return line; + + // This is necessary because each line would be saved in the history while creating + // a new multiline, and we don't want that. + if (isMultiline && this[kIndex] === -1) { + ArrayPrototypeShift(this[kHistory]); + } else if (lastCommandErrored) { + // If the last command errored and we are trying to edit the history to fix it + // remove the broken one from the history + ArrayPrototypeShift(this[kHistory]); + } + + const normalizedLine = ReplHistory[kNormalizeLineEndings](line, "\n", "\r"); + + if (this[kHistory].length === 0 || this[kHistory][0] !== normalizedLine) { + if (this[kRemoveHistoryDuplicates]) { + // Remove older history line if identical to new one + const dupIndex = ArrayPrototypeIndexOf(this[kHistory], normalizedLine); + if (dupIndex !== -1) ArrayPrototypeSplice(this[kHistory], dupIndex, 1); + } + + // Add the new line to the history + ArrayPrototypeUnshift(this[kHistory], normalizedLine); + + // Only store so many + if (this[kHistory].length > this[kSize]) ArrayPrototypePop(this[kHistory]); + } + + this[kIndex] = -1; + + const finalLine = isMultiline ? reverseString(this[kHistory][0]) : this[kHistory][0]; + + // The listener could change the history object, possibly + // to remove the last added entry if it is sensitive and should + // not be persisted in the history, like a password + // Emit history event to notify listeners of update + this[kContext].emit("history", this[kHistory]); + + return finalLine; + } + + canNavigateToNext() { + return this[kIndex] > -1 && this[kHistory].length > 0; + } + + navigateToNext(substringSearch) { + if (!this.canNavigateToNext()) { + return null; + } + const search = substringSearch || ""; + let index = this[kIndex] - 1; + + while ( + index >= 0 && + (!StringPrototypeStartsWith(this[kHistory][index], search) || this[kContext].line === this[kHistory][index]) + ) { + index--; + } + + this[kIndex] = index; + + if (index === -1) { + return search; + } + + return ReplHistory[kNormalizeLineEndings](this[kHistory][index], "\r", "\n"); + } + + canNavigateToPrevious() { + return this[kHistory].length !== this[kIndex] && this[kHistory].length > 0; + } + + navigateToPrevious(substringSearch = "") { + if (!this.canNavigateToPrevious()) { + return null; + } + const search = substringSearch || ""; + let index = this[kIndex] + 1; + + while ( + index < this[kHistory].length && + (!StringPrototypeStartsWith(this[kHistory][index], search) || this[kContext].line === this[kHistory][index]) + ) { + index++; + } + + this[kIndex] = index; + + if (index === this[kHistory].length) { + return search; + } + + return ReplHistory[kNormalizeLineEndings](this[kHistory][index], "\r", "\n"); + } + + get size() { + return this[kSize]; + } + get isFlushing() { + return this[kIsFlushing]; + } + get history() { + return this[kHistory]; + } + set history(value) { + this[kHistory] = value; + } + get index() { + return this[kIndex]; + } + set index(value) { + this[kIndex] = value; + } + + // Start private methods + + static [kGetHistoryPath](options) { + let historyPath = options.filePath; + if (typeof historyPath === "string") { + historyPath = StringPrototypeTrim(historyPath); + } + return historyPath; + } + + static [kNormalizeLineEndings](line, from, to) { + // Multiline history entries are saved reversed + // History is structured with the newest entries at the top + // and the oldest at the bottom. Multiline histories, however, only occupy + // one line in the history file. When loading multiline history with + // an old node binary, the history will be saved in the old format. + // This is why we need to reverse the multilines. + // Reversing the multilines is necessary when adding / editing and displaying them + return reverseString(line, from, to); + } + + static [kWriteToOutput](context, message) { + if (typeof context._writeToOutput === "function") { + context._writeToOutput(message); + if (typeof context._refreshLine === "function") { + context._refreshLine(); + } + } + } + + [kResolveHistoryPath]() { + if (!this[kHistoryPath]) { + try { + this[kHistoryPath] = path.join(os.homedir(), ".node_repl_history"); + return this[kHistoryPath]; + } catch (err) { + debug(err.stack); + return null; + } + } + return this[kHistoryPath]; + } + + [kHasWritePermission]() { + return !(permission.isEnabled() && permission.has("fs.write", this[kHistoryPath]) === false); + } + + [kValidateOptions](options) { + if (typeof options.history !== "undefined") { + validateArray(options.history, "history"); + } + if (typeof options.size !== "undefined") { + validateNumber(options.size, "size", 0); + } + } + + async [kInitializeHistory](onReadyCallback) { + try { + // Open and close file first to ensure it exists + // History files are conventionally not readable by others + // 0o0600 = read/write for owner only + const hnd = await fs.promises.open(this[kHistoryPath], "a+", 0o0600); + await hnd.close(); + + let data; + try { + data = await fs.promises.readFile(this[kHistoryPath], "utf8"); + } catch (err) { + return this[kHandleHistoryInitError](err, onReadyCallback); + } + + if (data) { + this[kHistory] = RegExpPrototypeSymbolSplit(/\r?\n+/, data, this[kSize]); + } else { + this[kHistory] = []; + } + + validateArray(this[kHistory], "history"); + + const handle = await fs.promises.open(this[kHistoryPath], "r+"); + this[kHistoryHandle] = handle; + + await handle.truncate(0); + + this[kContext].on("line", this[kOnLine].bind(this)); + this[kContext].once("exit", this[kOnExit].bind(this)); + + this[kContext].once("flushHistory", () => { + if (!this[kContext].closed) { + this[kContext].resume(); + onReadyCallback(null, this[kContext]); + } + }); + + await this[kFlushHistory](); + } catch (err) { + await this[kCloseHandle](); + return this[kHandleHistoryInitError](err, onReadyCallback); + } + } + + [kHandleHistoryInitError](err, onReadyCallback) { + // Cannot open history file. + // Don't crash, just don't persist history. + ReplHistory[kWriteToOutput]( + this[kContext], + "\nError: Could not open history file.\n" + "REPL session history will not be persisted.\n", + ); + debug(err.stack); + + // Save a reference to the context's original _historyPrev + this.historyPrev = this[kContext]._historyPrev; + this[kContext]._historyPrev = this[kReplHistoryMessage].bind(this); + this[kContext].resume(); + return onReadyCallback(null, this[kContext]); + } + + [kOnLine]() { + this[kIsFlushing] = true; + + if (this[kTimer]) { + clearTimeout(this[kTimer]); + } + + this[kTimer] = setTimeout(() => this[kFlushHistory](), kDebounceHistoryMS); + } + + async [kFlushHistory]() { + this[kTimer] = null; + if (this[kWriting]) { + this[kPending] = true; + return; + } + + this[kWriting] = true; + const historyData = ArrayPrototypeJoin(this[kHistory], "\n"); + + try { + await this[kHistoryHandle].write(historyData, 0, "utf8"); + this[kWriting] = false; + + if (this[kPending]) { + this[kPending] = false; + this[kOnLine](); + } else { + this[kIsFlushing] = Boolean(this[kTimer]); + if (!this[kIsFlushing]) { + this[kContext].emit("flushHistory"); + } + } + } catch (err) { + this[kWriting] = false; + debug("Error writing history file:", err); + } + } + + async [kOnExit]() { + if (this[kIsFlushing]) { + this[kContext].once("flushHistory", this[kOnExit].bind(this)); + return; + } + this[kContext].off("line", this[kOnLine].bind(this)); + + await this[kCloseHandle](); + } + + async [kCloseHandle]() { + if (this[kHistoryHandle] !== null) { + const handle = this[kHistoryHandle]; + this[kHistoryHandle] = null; + try { + await handle.close(); + } catch (err) { + debug("Error closing history file:", err); + } + } + } + + /** + * Closes the history file handle. + * @returns {Promise} + */ + closeHandle() { + return this[kCloseHandle](); + } + + [kReplHistoryMessage]() { + if (this[kHistory].length === 0) { + ReplHistory[kWriteToOutput]( + this[kContext], + "\nPersistent history support disabled. " + + "Set the NODE_REPL_HISTORY environment\nvariable to " + + "a valid, user-writable path to enable.\n", + ); + } + // First restore the original method on the context + this[kContext]._historyPrev = this.historyPrev; + // Then call it with the correct context + return this[kContext]._historyPrev(); + } +} + +__node_module__.exports = { + ReplHistory, +}; + +export default __node_module__.exports; diff --git a/src/js/internal/repl/mode.js b/src/js/internal/repl/mode.js new file mode 100644 index 000000000000..db52632817df --- /dev/null +++ b/src/js/internal/repl/mode.js @@ -0,0 +1,6 @@ +// REPL_MODE_* symbols, split out so node:repl can export them without +// evaluating internal/repl/utils (which pulls in readline + shims). +export default { + REPL_MODE_SLOPPY: Symbol("repl-sloppy"), + REPL_MODE_STRICT: Symbol("repl-strict"), +}; diff --git a/src/js/internal/repl/node-errors.js b/src/js/internal/repl/node-errors.js new file mode 100644 index 000000000000..84c2c29eecc6 --- /dev/null +++ b/src/js/internal/repl/node-errors.js @@ -0,0 +1,99 @@ +// Error-code shims for Node.js sources ported into Bun (node:repl stack). +// All codes route to Bun's native $ERR_* constructors (registered in +// ErrorCode.ts), which give the Node-compatible `err.name` (bare "TypeError", +// with `[CODE]` only in toString()). `instanceof ERR_X` is keyed on `.code`. + +function ERR_INVALID_ARG_TYPE(...args) { + return $ERR_INVALID_ARG_TYPE(...args); +} +function ERR_INVALID_ARG_VALUE(...args) { + return $ERR_INVALID_ARG_VALUE(...args); +} +function ERR_MISSING_ARGS(...args) { + return $ERR_MISSING_ARGS(...args); +} +function ERR_USE_AFTER_CLOSE(...args) { + return $ERR_USE_AFTER_CLOSE(...args); +} +function ERR_INVALID_CURSOR_POS(...args) { + return $ERR_INVALID_CURSOR_POS(...args); +} +function ERR_SCRIPT_EXECUTION_INTERRUPTED(...args) { + return $ERR_SCRIPT_EXECUTION_INTERRUPTED(...args); +} +function ERR_INVALID_STATE(...args) { + return $ERR_INVALID_STATE(...args); +} +// Bun's native $ERR_* gives Node-compatible `.name` and `.toString()`, but +// JSC materializes `.stack` from `.name + ": " + msg` at construction, so the +// `[CODE]` (which Node's prepareStackTrace injects) is missing there. The +// vendored REPL tests match on the stack text, so re-head it to `.toString()`. +function decorateNodeErrorStack(e) { + if (typeof e?.stack === "string") { + const nl = e.stack.indexOf("\n"); + e.stack = e.toString() + (nl === -1 ? "" : e.stack.slice(nl)); + } + return e; +} +function ERR_CANNOT_WATCH_SIGINT() { + return decorateNodeErrorStack($ERR_CANNOT_WATCH_SIGINT("Cannot watch for interruptions when running asynchronously")); +} +function ERR_INSPECTOR_NOT_AVAILABLE() { + return decorateNodeErrorStack($ERR_INSPECTOR_NOT_AVAILABLE("Inspector is not available")); +} +function ERR_INVALID_REPL_EVAL_CONFIG() { + return decorateNodeErrorStack( + $ERR_INVALID_REPL_EVAL_CONFIG('Cannot specify both "breakEvalOnSigint" and "eval" for REPL'), + ); +} +function ERR_INVALID_REPL_INPUT(message) { + return decorateNodeErrorStack($ERR_INVALID_REPL_INPUT(message)); +} +function AbortError(message = "The operation was aborted", options = undefined) { + return $makeAbortError(message, options); +} + +// Builtin function declarations have no .prototype, so route `instanceof` +// through Symbol.hasInstance keyed on the error code. +for (const fn of [ + ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, + ERR_MISSING_ARGS, + ERR_USE_AFTER_CLOSE, + ERR_INVALID_CURSOR_POS, + ERR_SCRIPT_EXECUTION_INTERRUPTED, + ERR_INVALID_STATE, + ERR_CANNOT_WATCH_SIGINT, + ERR_INSPECTOR_NOT_AVAILABLE, + ERR_INVALID_REPL_EVAL_CONFIG, + ERR_INVALID_REPL_INPUT, +]) { + Object.defineProperty(fn, Symbol.hasInstance, { + __proto__: null, + value: e => typeof e === "object" && e !== null && e.code === fn.name, + }); +} + +function isErrorStackTraceLimitWritable() { + const desc = Object.getOwnPropertyDescriptor(Error, "stackTraceLimit"); + if (desc === undefined) return Object.isExtensible(Error); + return Object.prototype.hasOwnProperty.$call(desc, "writable") ? desc.writable : desc.set !== undefined; +} + +export default { + isErrorStackTraceLimitWritable, + AbortError, + codes: { + ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, + ERR_MISSING_ARGS, + ERR_USE_AFTER_CLOSE, + ERR_INVALID_CURSOR_POS, + ERR_SCRIPT_EXECUTION_INTERRUPTED, + ERR_INVALID_STATE, + ERR_CANNOT_WATCH_SIGINT, + ERR_INSPECTOR_NOT_AVAILABLE, + ERR_INVALID_REPL_EVAL_CONFIG, + ERR_INVALID_REPL_INPUT, + }, +}; diff --git a/src/js/internal/repl/node-inspect.js b/src/js/internal/repl/node-inspect.js new file mode 100644 index 000000000000..7e7a9c635599 --- /dev/null +++ b/src/js/internal/repl/node-inspect.js @@ -0,0 +1,40 @@ +// Shim for Node's `internal/util/inspect` as consumed by the ported +// node:repl / internal/readline stack. getStringWidth/stripVTControlCharacters +// go straight to the native bindings so `require("node:readline")` does not +// pull in the 99 KB internal/util/inspect; inspect/format load it lazily on +// first access (REPL output / completion rendering). + +const stripANSI = Bun.stripANSI; +const nativeStringWidth = $newCppFunction("stringWidth.cpp", "jsFunctionBunStringWidth", 1); +const StringPrototypeNormalize = String.prototype.normalize; + +// Same wrapper internal/util/inspect exports: strip ANSI (opt-out via second +// arg) then NFC-normalize so combining sequences measure as one cell. +function getStringWidth(str, removeControlChars = true) { + if (removeControlChars) str = stripANSI(str); + return nativeStringWidth(StringPrototypeNormalize.$call(str, "NFC")); +} + +function stripVTControlCharacters(str) { + if (typeof str !== "string") throw $ERR_INVALID_ARG_TYPE("str", "string", str); + return stripANSI(str); +} + +let util; +function load() { + return (util ??= require("internal/util/inspect")); +} + +export default { + getStringWidth, + stripVTControlCharacters, + get inspect() { + return load().inspect; + }, + get format() { + return load().format; + }, + get formatWithOptions() { + return load().formatWithOptions; + }, +}; diff --git a/src/js/internal/repl/node-primordials.js b/src/js/internal/repl/node-primordials.js new file mode 100644 index 000000000000..a380c2b0d145 --- /dev/null +++ b/src/js/internal/repl/node-primordials.js @@ -0,0 +1,158 @@ +// Uncurried "primordials" shims for Node.js sources ported into Bun +// (node:repl and the internal/readline stack). Each helper captures its +// intrinsic once at module load and invokes it through the tamper-proof +// `$call`/`$apply` intrinsics, so replacing a prototype method (e.g. +// `Array.prototype.push = ...`) after this module loads does not affect the +// ported code. Safe* containers re-export the real makeSafe()-wrapped +// implementations from internal/primordials so there is one definition per +// name. Weaker than Node only in that capture happens at (lazy) module load +// rather than realm bootstrap; SafePromiseRace additionally wraps its input in +// a SafeArrayIterator because spec Promise.race reads +// Array.prototype[Symbol.iterator] at CALL time. +const { SafeMap, SafeSet, SafeWeakSet, SafeStringIterator, SafeArrayIterator } = require("internal/primordials"); + +const ArrayFromFn = Array.from; +const ArrayPrototypeAtFn = Array.prototype.at; +const ArrayPrototypeConcatFn = Array.prototype.concat; +const ArrayPrototypeFilterFn = Array.prototype.filter; +const ArrayPrototypeFindFn = Array.prototype.find; +const ArrayPrototypeFindLastIndexFn = Array.prototype.findLastIndex; +const ArrayPrototypeFlatFn = Array.prototype.flat; +const ArrayPrototypeForEachFn = Array.prototype.forEach; +const ArrayPrototypeIncludesFn = Array.prototype.includes; +const ArrayPrototypeIndexOfFn = Array.prototype.indexOf; +const ArrayPrototypeJoinFn = Array.prototype.join; +const ArrayPrototypeMapFn = Array.prototype.map; +const ArrayPrototypePopFn = Array.prototype.pop; +const ArrayPrototypePushFn = Array.prototype.push; +const ArrayPrototypeReverseFn = Array.prototype.reverse; +const ArrayPrototypeShiftFn = Array.prototype.shift; +const ArrayPrototypeSliceFn = Array.prototype.slice; +const ArrayPrototypeSomeFn = Array.prototype.some; +const ArrayPrototypeSortFn = Array.prototype.sort; +const ArrayPrototypeSpliceFn = Array.prototype.splice; +const ArrayPrototypeToSortedFn = Array.prototype.toSorted; +const ArrayPrototypeUnshiftFn = Array.prototype.unshift; +const DateNowFn = Date.now; +const FunctionPrototypeBindFn = Function.prototype.bind; +const JSONStringifyFn = JSON.stringify; +const MathMaxFn = Math.max; +const PromisePrototypeThenFn = Promise.prototype.then; +const PromiseRejectFn = Promise.reject; +const PromiseResolveFn = Promise.resolve; +const PromiseRaceFn = Promise.race; +const RegExpPrototypeExecFn = RegExp.prototype.exec; +const RegExpPrototypeSymbolReplaceFn = RegExp.prototype[Symbol.replace]; +const RegExpPrototypeSymbolSplitFn = RegExp.prototype[Symbol.split]; +const StringPrototypeCharAtFn = String.prototype.charAt; +const StringPrototypeCharCodeAtFn = String.prototype.charCodeAt; +const StringPrototypeCodePointAtFn = String.prototype.codePointAt; +const StringPrototypeEndsWithFn = String.prototype.endsWith; +const StringPrototypeIncludesFn = String.prototype.includes; +const StringPrototypeIndexOfFn = String.prototype.indexOf; +const StringPrototypeLastIndexOfFn = String.prototype.lastIndexOf; +const StringPrototypeRepeatFn = String.prototype.repeat; +const StringPrototypeReplaceFn = String.prototype.replace; +const StringPrototypeReplaceAllFn = String.prototype.replaceAll; +const StringPrototypeSliceFn = String.prototype.slice; +const StringPrototypeSplitFn = String.prototype.split; +const StringPrototypeStartsWithFn = String.prototype.startsWith; +const StringPrototypeToLocaleLowerCaseFn = String.prototype.toLocaleLowerCase; +const StringPrototypeToLowerCaseFn = String.prototype.toLowerCase; +const StringPrototypeTrimFn = String.prototype.trim; +const StringPrototypeTrimStartFn = String.prototype.trimStart; + +export default { + ArrayFrom: (...args) => ArrayFromFn.$apply(Array, args), + ArrayIsArray: Array.isArray, + ArrayPrototypeAt: (a, i) => ArrayPrototypeAtFn.$call(a, i), + ArrayPrototypeConcat: (a, ...args) => ArrayPrototypeConcatFn.$apply(a, args), + ArrayPrototypeFilter: (a, fn) => ArrayPrototypeFilterFn.$call(a, fn), + ArrayPrototypeFind: (a, fn) => ArrayPrototypeFindFn.$call(a, fn), + ArrayPrototypeFindLastIndex: (a, fn) => ArrayPrototypeFindLastIndexFn.$call(a, fn), + ArrayPrototypeFlat: (a, d) => ArrayPrototypeFlatFn.$call(a, d), + ArrayPrototypeForEach: (a, fn) => ArrayPrototypeForEachFn.$call(a, fn), + ArrayPrototypeIncludes: (a, v, i) => ArrayPrototypeIncludesFn.$call(a, v, i), + ArrayPrototypeIndexOf: (a, v, i) => ArrayPrototypeIndexOfFn.$call(a, v, i), + ArrayPrototypeJoin: (a, s) => ArrayPrototypeJoinFn.$call(a, s), + ArrayPrototypeMap: (a, fn) => ArrayPrototypeMapFn.$call(a, fn), + ArrayPrototypePop: a => ArrayPrototypePopFn.$call(a), + ArrayPrototypePush: (a, ...items) => ArrayPrototypePushFn.$apply(a, items), + ArrayPrototypePushApply: (a, items) => ArrayPrototypePushFn.$apply(a, items), + ArrayPrototypeReverse: a => ArrayPrototypeReverseFn.$call(a), + ArrayPrototypeShift: a => ArrayPrototypeShiftFn.$call(a), + ArrayPrototypeSlice: (a, b, e) => ArrayPrototypeSliceFn.$call(a, b, e), + ArrayPrototypeSome: (a, fn) => ArrayPrototypeSomeFn.$call(a, fn), + ArrayPrototypeSort: (a, fn) => ArrayPrototypeSortFn.$call(a, fn), + ArrayPrototypeSplice: (a, ...args) => ArrayPrototypeSpliceFn.$apply(a, args), + ArrayPrototypeToSorted: (a, fn) => ArrayPrototypeToSortedFn.$call(a, fn), + ArrayPrototypeUnshift: (a, ...items) => ArrayPrototypeUnshiftFn.$apply(a, items), + Boolean, + DateNow: () => DateNowFn.$call(Date), + Error, + FunctionPrototype: function () {}, + FunctionPrototypeBind: (fn, thisArg, ...args) => { + ArrayPrototypeUnshiftFn.$call(args, thisArg); + return FunctionPrototypeBindFn.$apply(fn, args); + }, + FunctionPrototypeCall: (fn, thisArg, ...args) => fn.$apply(thisArg, args), + JSONStringify: (...args) => JSONStringifyFn.$apply(JSON, args), + MathCeil: Math.ceil, + MathFloor: Math.floor, + MathMax: Math.max, + MathMaxApply: args => MathMaxFn.$apply(Math, args), + MathMin: Math.min, + Number, + NumberIsFinite: Number.isFinite, + NumberIsNaN: Number.isNaN, + NumberParseFloat: Number.parseFloat, + NumberParseInt: Number.parseInt, + ObjectAssign: Object.assign, + ObjectCreate: Object.create, + ObjectDefineProperties: Object.defineProperties, + ObjectDefineProperty: Object.defineProperty, + ObjectEntries: Object.entries, + ObjectFreeze: Object.freeze, + ObjectGetOwnPropertyDescriptor: Object.getOwnPropertyDescriptor, + ObjectGetOwnPropertyNames: Object.getOwnPropertyNames, + ObjectGetPrototypeOf: Object.getPrototypeOf, + ObjectKeys: Object.keys, + ObjectSetPrototypeOf: Object.setPrototypeOf, + Promise, + PromisePrototypeThen: (p, onFulfilled, onRejected) => PromisePrototypeThenFn.$call(p, onFulfilled, onRejected), + PromiseReject: v => PromiseRejectFn.$call(Promise, v), + PromiseResolve: v => PromiseResolveFn.$call(Promise, v), + ReflectApply: (fn, thisArg, args) => fn.$apply(thisArg, args), + RegExp, + RegExpPrototypeExec: (re, s) => RegExpPrototypeExecFn.$call(re, s), + RegExpPrototypeSymbolReplace: (re, s, replacement) => RegExpPrototypeSymbolReplaceFn.$call(re, s, replacement), + RegExpPrototypeSymbolSplit: (re, s, limit) => RegExpPrototypeSymbolSplitFn.$call(re, s, limit), + SafePromiseRace: promises => PromiseRaceFn.$call(Promise, new SafeArrayIterator(promises)), + SafeSet, + SafeMap, + SafeWeakSet, + SafeStringIterator, + StringFromCharCode: String.fromCharCode, + StringPrototypeCharAt: (s, i) => StringPrototypeCharAtFn.$call(s, i), + StringPrototypeCharCodeAt: (s, i) => StringPrototypeCharCodeAtFn.$call(s, i), + StringPrototypeCodePointAt: (s, i) => StringPrototypeCodePointAtFn.$call(s, i), + StringPrototypeEndsWith: (s, v, e) => StringPrototypeEndsWithFn.$call(s, v, e), + StringPrototypeIncludes: (s, v, i) => StringPrototypeIncludesFn.$call(s, v, i), + StringPrototypeIndexOf: (s, v, i) => StringPrototypeIndexOfFn.$call(s, v, i), + StringPrototypeLastIndexOf: (s, v, i) => StringPrototypeLastIndexOfFn.$call(s, v, i), + StringPrototypeRepeat: (s, n) => StringPrototypeRepeatFn.$call(s, n), + StringPrototypeReplace: (s, a, b) => StringPrototypeReplaceFn.$call(s, a, b), + StringPrototypeReplaceAll: (s, a, b) => StringPrototypeReplaceAllFn.$call(s, a, b), + StringPrototypeSlice: (s, b, e) => StringPrototypeSliceFn.$call(s, b, e), + StringPrototypeSplit: (s, sep, limit) => StringPrototypeSplitFn.$call(s, sep, limit), + StringPrototypeStartsWith: (s, v, i) => StringPrototypeStartsWithFn.$call(s, v, i), + StringPrototypeToLocaleLowerCase: s => StringPrototypeToLocaleLowerCaseFn.$call(s), + StringPrototypeToLowerCase: s => StringPrototypeToLowerCaseFn.$call(s), + StringPrototypeTrim: s => StringPrototypeTrimFn.$call(s), + StringPrototypeTrimStart: s => StringPrototypeTrimStartFn.$call(s), + Symbol, + SymbolAsyncIterator: Symbol.asyncIterator, + SymbolDispose: Symbol.dispose, + SyntaxError, + globalThis, +}; diff --git a/src/js/internal/repl/node-shims.js b/src/js/internal/repl/node-shims.js new file mode 100644 index 000000000000..7551d0b05048 --- /dev/null +++ b/src/js/internal/repl/node-shims.js @@ -0,0 +1,456 @@ +// Consolidated shims for Node.js internal modules consumed by the ported +// node:repl / internal/readline stack. Each export matches the name and +// calling convention of the Node internal it replaces; implementations +// delegate to Bun equivalents. +const util = require("node:util"); +const Module = require("node:module"); +const path = require("node:path"); +const { + ArrayPrototypeJoin, + ArrayPrototypeMap, + ArrayPrototypePush, + ArrayPrototypeSlice, + RegExpPrototypeExec, + RegExpPrototypeSymbolReplace, + RegExpPrototypeSymbolSplit, + StringPrototypeIncludes, + StringPrototypeSplit, +} = require("internal/repl/node-primordials"); + +// ---- internal/util ---------------------------------------------------- + +const { kEmptyObject } = require("internal/shared"); + +// Node's real implementation reconstructs the regex in an internal realm so a +// tampered `RegExp.prototype[Symbol.replace]` can't observe it. Bun has no +// internal realm; the load-time-captured intrinsics close the `[Symbol.*]` +// override hole (a tampered `RegExp.prototype.exec` is still observable per +// spec — see `@@replace`/`@@split` `Get(rx,"exec")`). +function SideEffectFreeRegExpPrototypeSymbolReplace(regexp, str, replacement) { + return RegExpPrototypeSymbolReplace(regexp, str, replacement); +} + +function SideEffectFreeRegExpPrototypeSymbolSplit(regexp, str, limit) { + return RegExpPrototypeSymbolSplit(regexp, str, limit); +} + +function decorateErrorStack(err) { + // JSC materializes stacks eagerly so Node's overrideStackTrace never runs; + // reproduce it by normalizing " (loc)" frames and cutting at the + // last REPLn:l:c frame (drops the REPL top-level + vm runner frames). + if (typeof err?.stack !== "string") return err; + let lines = StringPrototypeSplit(err.stack, "\n"); + lines = ArrayPrototypeMap(lines, l => RegExpPrototypeSymbolReplace(/^(\s+at ) \((.+)\)$/, l, "$1$2")); + let anonIdx = -1; + for (let i = 0; i < lines.length; i++) { + if (RegExpPrototypeExec(/^\s+at REPL\d*:\d+:\d+$/, lines[i]) !== null) anonIdx = i; + } + if (anonIdx !== -1) lines = ArrayPrototypeSlice(lines, 0, anonIdx); + const newStack = ArrayPrototypeJoin(lines, "\n"); + if (newStack !== err.stack) { + // Errors with a non-writable .stack (Object.freeze, getter-only) must + // not turn into a TypeError that escapes the REPL's error handler. + try { + err.stack = newStack; + } catch {} + } + return err; +} + +function isError(e) { + return util.types.isNativeError(e) || e instanceof Error; +} + +// ---- internal/util/colors ---------------------------------------------- + +const { shouldColorize } = require("internal/util/colors"); + +// ---- internal/util/debuglog ---------------------------------------------- + +function debuglog(set, cb) { + const fn = util.debuglog(set); + if (typeof cb === "function") cb(fn); + return fn; +} + +// ---- internal/util/inspector ---------------------------------------------- + +function sendInspectorCommand(cb, onError) { + // JSC's inspector protocol has no `Runtime.globalLexicalScopeNames` (V8-only), + // so let/const/class tab-completion in useGlobal:true mode is inert until a + // native binding enumerates JSGlobalObject::globalLexicalEnvironment(). + return onError(); +} + +// ---- internal/util/types ---------------------------------------------- + +const isProxy = util.types.isProxy; + +// ---- internal/options ---------------------------------------------- + +function getOptionValue(name) { + switch (name) { + case "--pending-deprecation": + return process.execArgv.includes("--pending-deprecation"); + case "--experimental-repl-await": + return true; + case "--use-strict": + return false; + default: + return undefined; + } +} + +// ---- internal/process/permission ---------------------------------------------- + +function isEnabled() { + return false; +} + +function has() { + return true; +} + +// ---- internal/streams/utils ---------------------------------------------- + +function isWritable(stream) { + return typeof stream?.write === "function"; +} + +// ---- internal/events/abort_listener ---------------------------------------------- + +const { addAbortListener } = require("internal/abort_listener"); + +// ---- internal/bootstrap/realm ---------------------------------------------- + +const BuiltinModule = { + getSchemeOnlyModuleNames() { + // Bare names; completion.js prefixes them with "node:" itself. + return ["test"]; + }, + exists(id) { + return Module.isBuiltin(id); + }, + canBeRequiredByUsers(id) { + return Module.isBuiltin(id); + }, + canBeRequiredWithoutScheme(id) { + return Module.isBuiltin(id) && Module.isBuiltin("node:" + id); + }, +}; + +// ---- internal/modules/esm/get_format ---------------------------------------------- + +const extensionFormatMap = { + __proto__: null, + ".cjs": "commonjs", + ".js": "module", + ".json": "json", + ".mjs": "module", + ".node": "addon", + ".wasm": "wasm", +}; + +// ---- internal/modules/esm/loader ---------------------------------------------- + +const cascadedLoader = { + kEvaluationPhase: "evaluation", + kSourcePhase: "source", + import(specifier, parentURL, _importAttributes, _phase) { + // Relative specifiers resolve against the referrer the REPL threads + // through (cwd/repl), not against this bundled module. + if (parentURL && (specifier.startsWith("./") || specifier.startsWith("../"))) { + return import(new URL(specifier, parentURL).href); + } + return import(specifier); + }, +}; + +function getOrInitializeCascadedLoader() { + return cascadedLoader; +} + +// ---- internal/modules/helpers ---------------------------------------------- + +function makeRequireFunction(_mod) { + // Anchor relative requires to the REPL's cwd. process.cwd() throws when the + // working directory has been deleted; same fallback as fixReplRequire + // (internal/repl/utils.js). + let cwd; + try { + cwd = process.cwd(); + } catch { + cwd = path.dirname(process.execPath); + } + return Module.createRequire(path.join(cwd, "")); +} + +let builtinLibs; + +function getBuiltinLibs() { + if (!builtinLibs) { + // Bun's builtinModules also lists `bun`, `bun:*`, `undici`, `ws`; none + // resolve under `node:`, so exclude them so completion and the REPL + // global scope match Node's. + builtinLibs = Module.builtinModules.filter( + id => !id.startsWith("_") && !id.startsWith("node:") && !id.startsWith("bun") && id !== "undici" && id !== "ws", + ); + } + return builtinLibs; +} + +function addBuiltinLibsToObject(object, _dummy) { + // Make built-in modules available directly (loaded lazily). Builtin + // specifiers don't need a cwd-anchored referrer, so anchor to execPath + // (avoids ENOENT from process.cwd() in a deleted working directory). + const builtinRequire = Module.createRequire(process.execPath); + getBuiltinLibs().forEach(name => { + // Node filters slash-modules here (not in getBuiltinLibs), so + // repl.builtinModules and require-completion still offer them. + if (StringPrototypeIncludes(name, "/") || Object.getOwnPropertyDescriptor(object, name)) { + return; + } + + const setReal = val => { + // Deleting the property before re-assigning it disables the + // getter/setter mechanism. + delete object[name]; + object[name] = val; + }; + + Object.defineProperty(object, name, { + __proto__: null, + get: () => { + const lib = builtinRequire(name); + + try { + // Override the current getter/setter pair with the lib itself. + delete object[name]; + Object.defineProperty(object, name, { + __proto__: null, + get: () => lib, + set: setReal, + configurable: true, + enumerable: false, + }); + } catch { + // If the property is no longer configurable, ignore the error. + } + + return lib; + }, + set: setReal, + configurable: true, + enumerable: false, + }); + }); +} + +// ---- internal/vm ---------------------------------------------- + +const vm = require("node:vm"); + +function makeContextifyScript( + code, + filename, + lineOffset, + columnOffset, + cachedData, + produceCachedData, + parsingContext, + hostDefinedOptionId, + importModuleDynamically, +) { + return new vm.Script(code, { + filename, + lineOffset, + columnOffset, + cachedData, + produceCachedData, + importModuleDynamically: importModuleDynamically ?? (specifier => import(specifier)), + }); +} + +function runScriptInThisContext(script, displayErrors, _breakOnFirstLine) { + return script.runInThisContext({ displayErrors }); +} + +// ---- internal/modules/cjs/loader (constructible Module shim) ---------------- + +class CJSModuleShim { + constructor(id = "", parent = undefined) { + this.id = id; + this.path = ""; + this.exports = {}; + this.filename = null; + this.loaded = false; + this.children = []; + this.paths = []; + this.parent = parent; + } + + static builtinModules = Module.builtinModules; + static globalPaths = Module.globalPaths; + static _extensions = Module._extensions; + static _nodeModulePaths(from) { + return Module._nodeModulePaths(from); + } + static _resolveLookupPaths(request, parent) { + if (typeof Module._resolveLookupPaths === "function") { + return Module._resolveLookupPaths(request, parent); + } + return Module._nodeModulePaths(process.cwd()).concat(Module.globalPaths ?? []); + } + static _resolveFilename(request, parent, isMain, options) { + return Module._resolveFilename(request, parent, isMain, options); + } +} + +// ---- internalBinding('contextify') ---------------------------------------------- + +function startSigintWatchdog() { + // breakOnSigint interruption of synchronous eval WORKS via Bun's own + // SigintWatcher (wired in NodeVMScript.cpp). Only Node's `had_pending_ + // signals` race — SIGINT landing after the script exits but before raw mode + // is restored — is unimplemented, so stopSigintWatchdog() always reports no + // pending signal. + return true; +} + +function stopSigintWatchdog() { + return false; +} + +// ---- internalBinding('util') ---------------------------------------------- + +const ALL_PROPERTIES = 0; +const ONLY_WRITABLE = 1; +const ONLY_ENUMERABLE = 2; +const ONLY_CONFIGURABLE = 4; +const SKIP_STRINGS = 8; +const SKIP_SYMBOLS = 16; + +function getOwnNonIndexProperties(obj, filter = ALL_PROPERTIES) { + const indexRegex = /^(0|[1-9][0-9]*)$/; + const keys = []; + if (!(filter & SKIP_STRINGS)) { + const names = Object.getOwnPropertyNames(obj); + for (let i = 0; i < names.length; i++) { + const key = names[i]; + if (RegExpPrototypeExec(indexRegex, key) !== null) continue; + if (filter & ONLY_ENUMERABLE) { + const desc = Object.getOwnPropertyDescriptor(obj, key); + if (!desc?.enumerable) continue; + } + ArrayPrototypePush(keys, key); + } + } + if (!(filter & SKIP_SYMBOLS)) { + const syms = Object.getOwnPropertySymbols(obj); + for (let i = 0; i < syms.length; i++) { + const sym = syms[i]; + if (filter & ONLY_ENUMERABLE) { + const desc = Object.getOwnPropertyDescriptor(obj, sym); + if (!desc?.enumerable) continue; + } + ArrayPrototypePush(keys, sym); + } + } + return keys; +} + +// ---- process.addUncaughtExceptionCaptureCallback polyfill ---------------- +// Bun only implements the single-callback set/clear API; emulate Node's +// additive API with a dispatcher list. The shim occupies the exclusive slot +// for the process lifetime once the first REPL starts — see repl.js +// setupExceptionCapture() for the rationale. + +let captureCallbacks = null; + +function addUncaughtExceptionCaptureCallback(cb) { + if (!captureCallbacks) { + captureCallbacks = []; + try { + process.setUncaughtExceptionCaptureCallback(err => { + // Indexed, not for..of: user code can delete Array.prototype[Symbol.iterator] + // and this runs while reporting that very error, so an unsafe iteration here + // replaces the user's exception with "{} is not iterable". + for (let i = 0; i < captureCallbacks.length; i++) { + if (captureCallbacks[i](err)) return; + } + // No callback claimed it: Node's aux API falls through to the + // regular 'uncaughtException' flow (with the origin arg), then to + // the native fatal handler. + if (process.emit("uncaughtException", err, "uncaughtException")) return; + try { + process.stderr.write(`Uncaught ${util.inspect(err)}\n`); + } catch {} + process.exit(1); + }); + } catch { + // A user capture callback already occupies the exclusive slot. Node's + // additive API coexists with it natively; without that engine support, + // defer to the user's callback and don't push (the dispatcher isn't + // wired, so a queued cb would never fire). + return; + } + } + captureCallbacks.push(cb); +} + +export default { + addUncaughtExceptionCaptureCallback, + // internalBinding('contextify') + startSigintWatchdog, + stopSigintWatchdog, + // internalBinding('util') + constants: { + ALL_PROPERTIES, + ONLY_WRITABLE, + ONLY_ENUMERABLE, + ONLY_CONFIGURABLE, + SKIP_STRINGS, + SKIP_SYMBOLS, + }, + getOwnNonIndexProperties, + // internal/util + SideEffectFreeRegExpPrototypeSymbolReplace, + SideEffectFreeRegExpPrototypeSymbolSplit, + decorateErrorStack, + deprecate: util.deprecate, + isError, + kEmptyObject, + promisify: util.promisify, + // internal/util/colors + shouldColorize, + // internal/util/debuglog + debuglog, + // internal/util/inspector + sendInspectorCommand, + // internal/util/types + isProxy, + // internal/options + getOptionValue, + // internal/process/permission (consumed as a namespace: permission.isEnabled()) + isEnabled, + has, + // internal/streams/utils + isWritable, + // internal/events/abort_listener + addAbortListener, + // internal/bootstrap/realm + BuiltinModule, + // internal/modules/esm/get_format + extensionFormatMap, + // internal/modules/esm/loader + getOrInitializeCascadedLoader, + // internal/modules/cjs/loader + Module: CJSModuleShim, + // internal/modules/helpers + addBuiltinLibsToObject, + getBuiltinLibs, + makeRequireFunction, + // internal/vm + makeContextifyScript, + runScriptInThisContext, +}; diff --git a/src/js/internal/repl/utils.js b/src/js/internal/repl/utils.js new file mode 100644 index 000000000000..ba8b4ba420fa --- /dev/null +++ b/src/js/internal/repl/utils.js @@ -0,0 +1,838 @@ +// Ported from Node.js v26.3.0 lib/internal/repl/utils.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 { + ArrayPrototypeFilter, + ArrayPrototypeIncludes, + ArrayPrototypeMap, + Boolean, + FunctionPrototypeBind, + MathMin, + RegExpPrototypeExec, + SafeSet, + SafeStringIterator, + StringPrototypeIndexOf, + StringPrototypeLastIndexOf, + StringPrototypeReplaceAll, + StringPrototypeSlice, + StringPrototypeToLowerCase, + StringPrototypeTrim, + Symbol, +} = primordials; + +// Lazy: don't destructure — the vm.Script parse of acorn's ~122 KB source +// stays deferred until isRecoverableError/isValidSyntax first runs. +const acorn = require("internal/repl/acorn"); + +const { sendInspectorCommand, getBuiltinLibs } = require("internal/repl/node-shims"); + +const { ERR_INSPECTOR_NOT_AVAILABLE } = require("internal/repl/node-errors").codes; + +const { clearLine, clearScreenDown, cursorTo, moveCursor } = require("internal/readline/callbacks"); + +const { kIsMultiline, kSetLine } = require("internal/readline/interface"); + +const { commonPrefix, kSubstringSearch } = require("internal/readline/utils"); + +const { getStringWidth, inspect } = require("internal/repl/node-inspect"); + +const CJSModule = require("internal/repl/node-shims").Module; + +const vm = require("node:vm"); + +let debug = require("internal/repl/node-shims").debuglog("repl", fn => { + debug = fn; +}); + +const previewOptions = { + colors: false, + depth: 1, + showHidden: false, +}; + +const { REPL_MODE_SLOPPY, REPL_MODE_STRICT } = require("internal/repl/mode"); + +// If the error is that we've unexpectedly ended the input, +// then let the user try to recover by adding more input. +// Note: `e` (the original exception) is not used by the current implementation, +// but may be needed in the future. +function isRecoverableError(e, code) { + // For similar reasons as `defaultEval`, wrap expressions starting with a + // curly brace with parenthesis. Note: only the open parenthesis is added + // here as the point is to test for potentially valid but incomplete + // expressions. + if (RegExpPrototypeExec(/^\s*\{/, code) !== null && isRecoverableError(e, `(${code}`)) return true; + + let recoverable = false; + + // Determine if the point of any error raised is at the end of the input. + // There are two cases to consider: + // + // 1. Any error raised after we have encountered the 'eof' token. + // This prevents us from declaring partial tokens (like '2e') as + // recoverable. + // + // 2. Three cases where tokens can legally span lines. This is + // template, comment, and strings with a backslash at the end of + // the line, indicating a continuation. Note that we need to look + // for the specific errors of 'unterminated' kind (not, for example, + // a syntax error in a ${} expression in a template), and the only + // way to do that currently is to look at the message. Should Acorn + // change these messages in the future, this will lead to a test + // failure, indicating that this code needs to be updated. + // + const RecoverableParser = acorn.Parser.extend(Parser => { + return class extends Parser { + nextToken() { + super.nextToken(); + if (this.type === acorn.tokTypes.eof) recoverable = true; + } + raise(pos, message) { + switch (message) { + case "Unterminated template": + case "Unterminated comment": + recoverable = true; + break; + + case "Unterminated string constant": { + const token = StringPrototypeSlice(this.input, this.lastTokStart, this.pos); + // See https://www.ecma-international.org/ecma-262/#sec-line-terminators + if (RegExpPrototypeExec(/\\(?:\r\n?|\n|\u2028|\u2029)$/, token) !== null) { + recoverable = true; + } + } + } + super.raise(pos, message); + } + }; + }); + + // Try to parse the code with acorn. If the parse fails, ignore the acorn + // error and return the recoverable status. + try { + RecoverableParser.parse(code, { ecmaVersion: "latest" }); + + // Odd case: the underlying JS engine (V8, Chakra) rejected this input + // but Acorn detected no issue. Presume that additional text won't + // address this issue. + return false; + } catch { + return recoverable; + } +} + +function setupPreview(repl, contextSymbol, bufferSymbol, active) { + // Simple terminals can't handle previews. + if (process.env.TERM === "dumb" || !active) { + return { showPreview() {}, clearPreview() {} }; + } + + let inputPreview = null; + + let previewCompletionCounter = 0; + let completionPreview = null; + + let hasCompletions = false; + + let wrapped = false; + + let escaped = null; + + function getPreviewPos() { + const displayPos = repl._getDisplayPos(`${repl.getPrompt()}${repl.line}`); + const cursorPos = repl.line.length !== repl.cursor ? repl.getCursorPos() : displayPos; + return { displayPos, cursorPos }; + } + + function isCursorAtInputEnd() { + const { cursorPos, displayPos } = getPreviewPos(); + return cursorPos.rows === displayPos.rows && cursorPos.cols === displayPos.cols; + } + + const clearPreview = key => { + if (inputPreview !== null) { + const { displayPos, cursorPos } = getPreviewPos(); + const rows = displayPos.rows - cursorPos.rows + 1; + moveCursor(repl.output, 0, rows); + clearLine(repl.output); + moveCursor(repl.output, 0, -rows); + inputPreview = null; + } + if (completionPreview !== null) { + // Prevent cursor moves if not necessary! + const move = repl.line.length !== repl.cursor; + let pos, rows; + if (move) { + pos = getPreviewPos(); + cursorTo(repl.output, pos.displayPos.cols); + rows = pos.displayPos.rows - pos.cursorPos.rows; + moveCursor(repl.output, 0, rows); + } + const totalLine = `${repl.getPrompt()}${repl.line}${completionPreview}`; + const newPos = repl._getDisplayPos(totalLine); + // Minimize work for the terminal. It is enough to clear the right part of + // the current line in case the preview is visible on a single line. + if (newPos.rows === 0 || (pos && pos.displayPos.rows === newPos.rows)) { + clearLine(repl.output, 1); + } else { + clearScreenDown(repl.output); + } + if (move) { + cursorTo(repl.output, pos.cursorPos.cols); + moveCursor(repl.output, 0, -rows); + } + if (!key.ctrl && !key.shift) { + if (key.name === "escape") { + if (escaped === null && key.meta) { + escaped = repl.line; + } + } else if ( + (key.name === "return" || key.name === "enter") && + !key.meta && + escaped !== repl.line && + isCursorAtInputEnd() + ) { + repl._insertString(completionPreview); + } + } + completionPreview = null; + } + if (escaped !== repl.line) { + escaped = null; + } + }; + + function showCompletionPreview(line, insertPreview) { + previewCompletionCounter++; + + const count = previewCompletionCounter; + + repl.completer(line, (error, data) => { + // Tab completion might be async and the result might already be outdated. + if (count !== previewCompletionCounter) { + return; + } + + if (error) { + debug("Error while generating completion preview", error); + return; + } + + // Result and the text that was completed. + const { 0: rawCompletions, 1: completeOn } = data; + + if (!rawCompletions || rawCompletions.length === 0) { + return; + } + + hasCompletions = true; + + // If there is a common prefix to all matches, then apply that portion. + const completions = ArrayPrototypeFilter(rawCompletions, Boolean); + const prefix = commonPrefix(completions); + + // No common prefix found. + if (prefix.length <= completeOn.length) { + return; + } + + const suffix = StringPrototypeSlice(prefix, completeOn.length); + + if (insertPreview) { + repl._insertString(suffix); + return; + } + + completionPreview = suffix; + + const result = repl.useColors ? `\u001b[90m${suffix}\u001b[39m` : ` // ${suffix}`; + + const { cursorPos, displayPos } = getPreviewPos(); + if (repl.line.length !== repl.cursor) { + cursorTo(repl.output, displayPos.cols); + moveCursor(repl.output, 0, displayPos.rows - cursorPos.rows); + } + repl.output.write(result); + cursorTo(repl.output, cursorPos.cols); + const totalLine = `${repl.getPrompt()}${repl.line}${suffix}`; + const newPos = repl._getDisplayPos(totalLine); + const rows = newPos.rows - cursorPos.rows - (newPos.cols === 0 ? 1 : 0); + moveCursor(repl.output, 0, -rows); + }); + } + + function isInStrictMode(repl) { + return ( + repl.replMode === REPL_MODE_STRICT || + ArrayPrototypeIncludes( + ArrayPrototypeMap(process.execArgv, e => StringPrototypeReplaceAll(StringPrototypeToLowerCase(e), "_", "-")), + "--use-strict", + ) + ); + } + + // This returns a code preview for arbitrary input code. + function getInputPreview(input, callback) { + // For similar reasons as `defaultEval`, wrap expressions starting with a + // curly brace with parenthesis. + if (!wrapped && input[0] === "{" && input[input.length - 1] !== ";" && isValidSyntax(input)) { + input = `(${input})`; + wrapped = true; + } + sendInspectorCommand( + session => { + session.post( + "Runtime.evaluate", + { + expression: input, + throwOnSideEffect: true, + timeout: 333, + contextId: repl[contextSymbol], + }, + (error, preview) => { + if (error) { + callback(error); + return; + } + const { result } = preview; + if (result.value !== undefined) { + callback(null, inspect(result.value, previewOptions)); + // Ignore EvalErrors, SyntaxErrors and ReferenceErrors. It is not clear + // where they came from and if they are recoverable or not. Other errors + // may be inspected. + } else if ( + preview.exceptionDetails && + (result.className === "EvalError" || + result.className === "SyntaxError" || + // Report ReferenceError in case the strict mode is active + // for input that has no completions. + (result.className === "ReferenceError" && (hasCompletions || !isInStrictMode(repl)))) + ) { + callback(null, null); + } else if (result.objectId) { + // The writer options might change and have influence on the inspect + // output. The user might change e.g., `showProxy`, `getters` or + // `showHidden`. Use `inspect` instead of `JSON.stringify` to keep + // `Infinity` and similar intact. + const inspectOptions = inspect( + { + ...repl.writer.options, + colors: false, + depth: 1, + compact: true, + breakLength: Infinity, + }, + previewOptions, + ); + session.post( + "Runtime.callFunctionOn", + { + functionDeclaration: `(v) => + Reflect + .getOwnPropertyDescriptor(globalThis, 'util') + .get().inspect(v, ${inspectOptions})`, + objectId: result.objectId, + arguments: [result], + }, + (error, preview) => { + if (error) { + callback(error); + } else { + callback(null, preview.result.value); + } + }, + ); + } else { + // Either not serializable or undefined. + callback(null, result.unserializableValue || result.type); + } + }, + ); + }, + () => callback(new ERR_INSPECTOR_NOT_AVAILABLE()), + ); + } + + const showPreview = (showCompletion = true) => { + // Prevent duplicated previews after a refresh or in a multiline command. + if (inputPreview !== null || repl[kIsMultiline] || !repl.isCompletionEnabled || !process.features.inspector) { + return; + } + + const line = StringPrototypeTrim(repl.line); + + // Do not preview in case the line only contains whitespace. + if (line === "") { + return; + } + + hasCompletions = false; + + // Add the autocompletion preview. + if (showCompletion) { + const insertPreview = false; + showCompletionPreview(repl.line, insertPreview); + } + + // Do not preview if the command is buffered. + if (repl[bufferSymbol]) { + return; + } + + const inputPreviewCallback = (error, inspected) => { + if (inspected == null) { + return; + } + + wrapped = false; + + // Ignore the output if the value is identical to the current line. + if (line === inspected) { + return; + } + + if (error) { + debug("Error while generating preview", error); + return; + } + // Do not preview `undefined` if colors are deactivated or explicitly + // requested. + if (inspected === "undefined" && (!repl.useColors || repl.ignoreUndefined)) { + return; + } + + inputPreview = inspected; + + // Limit the output to maximum 250 characters. Otherwise it becomes a) + // difficult to read and b) non terminal REPLs would visualize the whole + // output. + let maxColumns = MathMin(repl.columns, 250); + + // Support unicode characters of width other than one by checking the + // actual width. + if (inspected.length * 2 >= maxColumns && getStringWidth(inspected) > maxColumns) { + maxColumns -= 4 + (repl.useColors ? 0 : 3); + let res = ""; + for (const char of new SafeStringIterator(inspected)) { + maxColumns -= getStringWidth(char); + if (maxColumns < 0) break; + res += char; + } + inspected = `${res}...`; + } + + // Line breaks are very rare and probably only occur in case of error + // messages with line breaks. + const lineBreakMatch = RegExpPrototypeExec(/[\r\n\v]/, inspected); + if (lineBreakMatch !== null) { + inspected = `${StringPrototypeSlice(inspected, 0, lineBreakMatch.index)}`; + } + + const result = repl.useColors ? `\u001b[90m${inspected}\u001b[39m` : `// ${inspected}`; + + const { cursorPos, displayPos } = getPreviewPos(); + const rows = displayPos.rows - cursorPos.rows; + // Moves one line below all the user lines + moveCursor(repl.output, 0, rows); + // Writes the preview there + repl.output.write(`\n${result}`); + + // Go back to the horizontal position of the cursor + cursorTo(repl.output, cursorPos.cols); + // Go back to the vertical position of the cursor + moveCursor(repl.output, 0, -rows - 1); + }; + + let previewLine = line; + + if (completionPreview !== null && isCursorAtInputEnd() && escaped !== repl.line) { + previewLine += completionPreview; + } + + getInputPreview(previewLine, inputPreviewCallback); + if (wrapped) { + getInputPreview(previewLine, inputPreviewCallback); + } + wrapped = false; + }; + + // -------------------------------------------------------------------------// + // Replace multiple interface functions. This is required to fully support // + // previews without changing readlines behavior. // + // -------------------------------------------------------------------------// + + // Refresh prints the whole screen again and the preview will be removed + // during that procedure. Print the preview again. This also makes sure + // the preview is always correct after resizing the terminal window. + const originalRefresh = FunctionPrototypeBind(repl._refreshLine, repl); + repl._refreshLine = () => { + inputPreview = null; + originalRefresh(); + showPreview(); + }; + + let insertCompletionPreview = true; + // Insert the longest common suffix of the current input in case the user + // moves to the right while already being at the current input end. + const originalMoveCursor = FunctionPrototypeBind(repl._moveCursor, repl); + repl._moveCursor = dx => { + const currentCursor = repl.cursor; + originalMoveCursor(dx); + if (currentCursor + dx > repl.line.length && typeof repl.completer === "function" && insertCompletionPreview) { + const insertPreview = true; + showCompletionPreview(repl.line, insertPreview); + } + }; + + // This is the only function that interferes with the completion insertion. + // Monkey patch it to prevent inserting the completion when it shouldn't be. + const originalClearLine = FunctionPrototypeBind(repl.clearLine, repl); + repl.clearLine = () => { + insertCompletionPreview = false; + originalClearLine(); + insertCompletionPreview = true; + }; + + return { showPreview, clearPreview }; +} + +function setupReverseSearch(repl) { + // Simple terminals can't use reverse search. + if (process.env.TERM === "dumb") { + return { + reverseSearch() { + return false; + }, + }; + } + + const alreadyMatched = new SafeSet(); + const labels = { + r: "bck-i-search: ", + s: "fwd-i-search: ", + }; + let isInReverseSearch = false; + let historyIndex = -1; + let input = ""; + let cursor = -1; + let dir = "r"; + let lastMatch = -1; + let lastCursor = -1; + let promptPos; + + function checkAndSetDirectionKey(keyName) { + if (!labels[keyName]) { + return false; + } + if (dir !== keyName) { + // Reset the already matched set in case the direction is changed. That + // way it's possible to find those entries again. + alreadyMatched.clear(); + dir = keyName; + } + return true; + } + + function goToNextHistoryIndex() { + // Ignore this entry for further searches and continue to the next + // history entry. + alreadyMatched.add(repl.history[historyIndex]); + historyIndex += dir === "r" ? 1 : -1; + cursor = -1; + } + + function search() { + // Just print an empty line in case the user removed the search parameter. + if (input === "") { + print(repl.line, `${labels[dir]}_`); + return; + } + // Fix the bounds in case the direction has changed in the meanwhile. + if (dir === "r") { + if (historyIndex < 0) { + historyIndex = 0; + } + } else if (historyIndex >= repl.history.length) { + historyIndex = repl.history.length - 1; + } + // Check the history entries until a match is found. + while (historyIndex >= 0 && historyIndex < repl.history.length) { + let entry = repl.history[historyIndex]; + // Visualize all potential matches only once. + if (alreadyMatched.has(entry)) { + historyIndex += dir === "r" ? 1 : -1; + continue; + } + // Match the next entry either from the start or from the end, depending + // on the current direction. + if (dir === "r") { + // Update the cursor in case it's necessary. + if (cursor === -1) { + cursor = entry.length; + } + cursor = StringPrototypeLastIndexOf(entry, input, cursor - 1); + } else { + cursor = StringPrototypeIndexOf(entry, input, cursor + 1); + } + // Match not found. + if (cursor === -1) { + goToNextHistoryIndex(); + // Match found. + } else { + if (repl.useColors) { + const start = StringPrototypeSlice(entry, 0, cursor); + const end = StringPrototypeSlice(entry, cursor + input.length); + entry = `${start}\x1B[4m${input}\x1B[24m${end}`; + } + print(entry, `${labels[dir]}${input}_`, cursor); + lastMatch = historyIndex; + lastCursor = cursor; + // Explicitly go to the next history item in case no further matches are + // possible with the current entry. + if ((dir === "r" && cursor === 0) || (dir === "s" && entry.length === cursor + input.length)) { + goToNextHistoryIndex(); + } + return; + } + } + print(repl.line, `failed-${labels[dir]}${input}_`); + } + + function print(outputLine, inputLine, cursor = repl.cursor) { + // upstream-todo(BridgeAR): Resizing the terminal window hides the overlay. To fix + // that, readline must be aware of this information. It's probably best to + // add a couple of properties to readline that allow to do the following: + // 1. Add arbitrary data to the end of the current line while not counting + // towards the line. This would be useful for the completion previews. + // 2. Add arbitrary extra lines that do not count towards the regular line. + // This would be useful for both, the input preview and the reverse + // search. It might be combined with the first part? + // 3. Add arbitrary input that is "on top" of the current line. That is + // useful for the reverse search. + // 4. To trigger the line refresh, functions should be used to pass through + // the information. Alternatively, getters and setters could be used. + // That might even be more elegant. + // The data would then be accounted for when calling `_refreshLine()`. + // This function would then look similar to: + // repl.overlay(outputLine); + // repl.addTrailingLine(inputLine); + // repl.setCursor(cursor); + // More potential improvements: use something similar to stream.cork(). + // Multiple cursor moves on the same tick could be prevented in case all + // writes from the same tick are combined and the cursor is moved at the + // tick end instead of after each operation. + let rows = 0; + if (lastMatch !== -1) { + const line = StringPrototypeSlice(repl.history[lastMatch], 0, lastCursor); + rows = repl._getDisplayPos(`${repl.getPrompt()}${line}`).rows; + cursorTo(repl.output, promptPos.cols); + } else if (isInReverseSearch && repl.line !== "") { + rows = repl.getCursorPos().rows; + cursorTo(repl.output, promptPos.cols); + } + if (rows !== 0) moveCursor(repl.output, 0, -rows); + + if (isInReverseSearch) { + clearScreenDown(repl.output); + repl.output.write(`${outputLine}\n${inputLine}`); + } else { + repl.output.write(`\n${inputLine}`); + } + + lastMatch = -1; + + // To know exactly how many rows we have to move the cursor back we need the + // cursor rows, the output rows and the input rows. + const prompt = repl.getPrompt(); + const cursorLine = prompt + StringPrototypeSlice(outputLine, 0, cursor); + const cursorPos = repl._getDisplayPos(cursorLine); + const outputPos = repl._getDisplayPos(`${prompt}${outputLine}`); + const inputPos = repl._getDisplayPos(inputLine); + const inputRows = inputPos.rows - (inputPos.cols === 0 ? 1 : 0); + + rows = -1 - inputRows - (outputPos.rows - cursorPos.rows); + + moveCursor(repl.output, 0, rows); + cursorTo(repl.output, cursorPos.cols); + } + + function reset(string) { + isInReverseSearch = string !== undefined; + + // In case the reverse search ends and a history entry is found, reset the + // line to the found entry. + if (!isInReverseSearch) { + if (lastMatch !== -1) { + repl[kSetLine](repl.history[lastMatch]); + repl.cursor = lastCursor; + repl.historyIndex = lastMatch; + } + + lastMatch = -1; + + // Clear screen and write the current repl.line before exiting. + cursorTo(repl.output, promptPos.cols); + moveCursor(repl.output, 0, promptPos.rows); + clearScreenDown(repl.output); + if (repl.line !== "") { + repl.output.write(repl.line); + if (repl.line.length !== repl.cursor) { + const { cols, rows } = repl.getCursorPos(); + cursorTo(repl.output, cols); + moveCursor(repl.output, 0, rows); + } + } + } + + input = string || ""; + cursor = -1; + historyIndex = repl.historyIndex; + alreadyMatched.clear(); + } + + function reverseSearch(string, key) { + if (!isInReverseSearch) { + if (key.ctrl && checkAndSetDirectionKey(key.name)) { + historyIndex = repl.historyIndex; + promptPos = repl._getDisplayPos(`${repl.getPrompt()}`); + print(repl.line, `${labels[dir]}_`); + isInReverseSearch = true; + } + } else if (key.ctrl && checkAndSetDirectionKey(key.name)) { + search(); + } else if (key.name === "backspace" || (key.ctrl && (key.name === "h" || key.name === "w"))) { + reset(StringPrototypeSlice(input, 0, input.length - 1)); + search(); + // Special handle + c and escape. Those should only cancel the + // reverse search. The original line is visible afterwards again. + } else if ((key.ctrl && key.name === "c") || key.name === "escape") { + lastMatch = -1; + reset(); + return true; + // End search in case either enter is pressed or if any non-reverse-search + // key (combination) is pressed. + } else if ( + key.ctrl || + key.meta || + key.name === "return" || + key.name === "enter" || + typeof string !== "string" || + string === "" + ) { + reset(); + repl[kSubstringSearch] = ""; + } else { + reset(`${input}${string}`); + search(); + } + return isInReverseSearch; + } + + return { reverseSearch }; +} + +const startsWithBraceRegExp = /^\s*{/; +const endsWithSemicolonRegExp = /;\s*$/; +function isValidSyntax(input) { + try { + acorn.Parser.parse(input, { + ecmaVersion: "latest", + allowAwaitOutsideFunction: true, + }); + return true; + } catch { + try { + acorn.Parser.parse(`_=${input}`, { + ecmaVersion: "latest", + allowAwaitOutsideFunction: true, + }); + return true; + } catch { + return false; + } + } +} + +/** + * Checks if some provided code represents an object literal. + * This is helpful to prevent confusing repl code evaluations where + * strings such as `{ a : 1 }` would get interpreted as block statements + * rather than object literals. + * @param {string} code the code to check + * @returns {boolean} true if the code represents an object literal, false otherwise + */ +function isObjectLiteral(code) { + return ( + RegExpPrototypeExec(startsWithBraceRegExp, code) !== null && + RegExpPrototypeExec(endsWithSemicolonRegExp, code) === null + ); +} + +const kContextId = Symbol("contextId"); + +const path = require("node:path"); + +function fixReplRequire(replModule) { + try { + // Hack for require.resolve("./relative") to work properly. + replModule.filename = path.resolve("repl"); + } catch { + // path.resolve('repl') fails when the current working directory has been + // deleted. Fall back to the directory name of the (absolute) executable + // path. It's not really correct but what are the alternatives? + const dirname = path.dirname(process.execPath); + replModule.filename = path.resolve(dirname, "repl"); + } + + // Hack for repl require to work properly with node_modules folders + replModule.paths = CJSModule._nodeModulePaths(replModule.filename); +} + +let nextREPLResourceNumber = 1; +// This prevents v8 code cache from getting confused and using a different +// cache from a resource of the same name +function getREPLResourceName() { + return `REPL${nextREPLResourceNumber++}`; +} + +const globalBuiltins = new SafeSet(vm.runInNewContext("Object.getOwnPropertyNames(globalThis)")); + +// node-shims' getBuiltinLibs() also excludes Bun-specific entries (`bun*`, +// `undici`, `ws`) so completion doesn't offer e.g. `node:undici`. +let _builtinLibs = getBuiltinLibs().slice(); + +// Note: the `getReplBuiltinLibs` and `setReplBuiltinLibs` are functions used to provide getters and +// setters for the `builtinModules` and `_builtinLibs` properties of the repl module and for making +// sure that all internal repl modules share the same value, which can potentially be updated by users. +// Also note that both `repl.builtinModules` and `repl._builtinLibs` are deprecated, once such properties +// are removed these two functions should also be removed as no longer necessary. + +function getReplBuiltinLibs() { + return _builtinLibs; +} + +function setReplBuiltinLibs(value) { + _builtinLibs = value; +} + +__node_module__.exports = { + REPL_MODE_SLOPPY, + REPL_MODE_STRICT, + isRecoverableError, + kStandaloneREPL: Symbol("kStandaloneREPL"), + setupPreview, + setupReverseSearch, + isObjectLiteral, + isValidSyntax, + kContextId, + getREPLResourceName, + globalBuiltins, + getReplBuiltinLibs, + setReplBuiltinLibs, + fixReplRequire, +}; + +export default __node_module__.exports; diff --git a/src/js/internal/util/inspect.js b/src/js/internal/util/inspect.js index c8a020127376..09d55ecafcbe 100644 --- a/src/js/internal/util/inspect.js +++ b/src/js/internal/util/inspect.js @@ -749,6 +749,9 @@ ObjectDefineProperty(inspect, "replDefaults", { validateObject(options, "options"); return ObjectAssign(inspectReplDefaults, options); }, + // node:repl re-defines this property with its own writer-backed accessor + // (see REPLServer constructor); keep it configurable so that works. + configurable: true, }); // Set Graphics Rendition https://en.wikipedia.org/wiki/ANSI_escape_code#graphics @@ -1585,7 +1588,7 @@ function formatRaw(ctx, value, recurseTimes, typedArray) { if (keys.length === 0 && protoProps === undefined) { return ctx.stylize(base, "date"); } - } else if (value instanceof Error) { + } else if (isNativeError(value) || value instanceof Error) { base = formatError(value, constructor, tag, ctx, keys); if (keys.length === 0 && protoProps === undefined) return base; } else if (isAnyArrayBuffer(value)) { @@ -1890,7 +1893,7 @@ function getStackFrames(ctx, err, stack) { } // Remove stack frames identical to frames in cause. - if (cause != null && cause instanceof Error) { + if (cause != null && (isNativeError(cause) || cause instanceof Error)) { const causeStack = getStackString(cause); const causeStackStart = StringPrototypeIndexOf(causeStack, "\n at"); if (causeStackStart !== -1) { @@ -2992,6 +2995,7 @@ export default { inspect, format, formatWithOptions, + getStringWidth, stripVTControlCharacters, //! non-standard properties, should these be kept? (not currently exposed) //stylizeWithColor, diff --git a/src/js/node/async_hooks.ts b/src/js/node/async_hooks.ts index 43917fa9e39d..ade40926f8da 100644 --- a/src/js/node/async_hooks.ts +++ b/src/js/node/async_hooks.ts @@ -145,7 +145,7 @@ class AsyncLocalStorage { var prev = get(); set(context); try { - return fn(...args); + return fn.$apply(undefined, args); } finally { set(prev); } @@ -192,7 +192,7 @@ class AsyncLocalStorage { // so a match here would skip installing store_value and let the callback // read the unmasked frame value instead. if (!this.#disabled && sameValue(this.getStore(), store_value)) { - return callback(...args); + return callback.$apply(undefined, args); } var context = get() as any[]; // we make sure to .slice() before mutating var hasPrevious = false; @@ -230,7 +230,9 @@ class AsyncLocalStorage { $assert(i > -1, "i was not set"); $assert(sameValue(this.getStore(), store_value), "run: store_value was not set"); try { - return callback(...args); + // $apply, not a spread: spreading goes through Array.prototype[Symbol.iterator], + // which userland can delete (node uses ReflectApply here for the same reason). + return callback.$apply(undefined, args); } finally { // Note: early `return` will prevent `throw` above from working. I think... // Set AsyncContextFrame to undefined if we are out of context values. diff --git a/src/js/node/readline.js b/src/js/node/readline.js new file mode 100644 index 000000000000..e57a2ad9e74d --- /dev/null +++ b/src/js/node/readline.js @@ -0,0 +1,538 @@ +// Ported from Node.js v26.3.0 lib/readline.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: {} }; +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +const { + DateNow, + FunctionPrototypeBind, + FunctionPrototypeCall, + ObjectDefineProperties, + ObjectSetPrototypeOf, + Promise, + PromiseReject, + StringPrototypeSlice, + SymbolDispose, +} = primordials; + +const { clearLine, clearScreenDown, cursorTo, moveCursor } = require("internal/readline/callbacks"); +const emitKeypressEvents = require("internal/readline/emitKeypressEvents"); +const promises = require("node:readline/promises"); + +const { AbortError } = require("internal/repl/node-errors"); +// Don't destructure `inspect` — reading it loads internal/util/inspect (99 KB). +const nodeInspect = require("internal/repl/node-inspect"); +// node-shims eagerly loads node:{util,module,path,vm}; readline only needs +// kEmptyObject/promisify, so import from their tiny sources. +const { kEmptyObject } = require("internal/shared"); +const { promisify } = require("internal/promisify"); +const { validateAbortSignal } = require("internal/validators"); + +/** + * @typedef {import('./stream.js').Readable} Readable + * @typedef {import('./stream.js').Writable} Writable + */ + +const { + Interface: _Interface, + InterfaceConstructor, + kAddHistory, + kDecoder, + kDeleteLeft, + kDeleteLineLeft, + kDeleteLineRight, + kDeleteRight, + kDeleteWordLeft, + kDeleteWordRight, + kGetDisplayPos, + kHistoryNext, + kHistoryPrev, + kInsertString, + kLine, + kLine_buffer, + kMoveCursor, + kNormalWrite, + kOldPrompt, + kOnLine, + kPreviousKey, + kPrompt, + kQuestion, + kQuestionCallback, + kQuestionCancel, + kRefreshLine, + kSawKeyPress, + kSawReturnAt, + kSetRawMode, + kTabComplete, + kTabCompleter, + kTtyWrite, + kWordLeft, + kWordRight, + kWriteToOutput, +} = require("internal/readline/interface"); +let addAbortListener; + +function Interface(input, output, completer, terminal) { + if (!(this instanceof Interface)) { + return new Interface(input, output, completer, terminal); + } + + if (input?.input && typeof input.completer === "function" && input.completer.length !== 2) { + const { completer } = input; + input.completer = (v, cb) => cb(null, completer(v)); + } else if (typeof completer === "function" && completer.length !== 2) { + const realCompleter = completer; + completer = (v, cb) => cb(null, realCompleter(v)); + } + + FunctionPrototypeCall(InterfaceConstructor, this, input, output, completer, terminal); + + if (process.env.TERM === "dumb") { + this._ttyWrite = FunctionPrototypeBind(_ttyWriteDumb, this); + } +} + +$toClass(Interface, "Interface", _Interface); + +/** + * Displays `query` by writing it to the `output`. + * @param {string} query + * @param {{ signal?: AbortSignal; }} [options] + * @param {Function} cb + * @returns {void} + */ +Interface.prototype.question = function question(query, options, cb) { + cb = typeof options === "function" ? options : cb; + if (options === null || typeof options !== "object") { + options = kEmptyObject; + } + + if (options.signal) { + validateAbortSignal(options.signal, "options.signal"); + if (options.signal.aborted) { + return; + } + + const onAbort = () => { + this[kQuestionCancel](); + }; + addAbortListener ??= require("internal/abort_listener").addAbortListener; + const disposable = addAbortListener(options.signal, onAbort); + const originalCb = cb; + cb = + typeof cb === "function" + ? answer => { + disposable[SymbolDispose](); + return originalCb(answer); + } + : disposable[SymbolDispose]; + } + + if (typeof cb === "function") { + this[kQuestion](query, cb); + } +}; +Interface.prototype.question[promisify.custom] = function question(query, options) { + if (options === null || typeof options !== "object") { + options = kEmptyObject; + } + + if (options.signal?.aborted) { + return PromiseReject(new AbortError(undefined, { cause: options.signal.reason })); + } + + return new Promise((resolve, reject) => { + let cb = resolve; + + if (options.signal) { + const onAbort = () => { + reject(new AbortError(undefined, { cause: options.signal.reason })); + }; + addAbortListener ??= require("internal/abort_listener").addAbortListener; + const disposable = addAbortListener(options.signal, onAbort); + cb = answer => { + disposable[SymbolDispose](); + resolve(answer); + }; + } + + this.question(query, options, cb); + }); +}; + +/** + * Creates a new `readline.Interface` instance. + * @param {Readable | { + * input: Readable; + * output: Writable; + * completer?: Function; + * terminal?: boolean; + * history?: string[]; + * historySize?: number; + * removeHistoryDuplicates?: boolean; + * prompt?: string; + * crlfDelay?: number; + * escapeCodeTimeout?: number; + * tabSize?: number; + * signal?: AbortSignal; + * }} input + * @param {Writable} [output] + * @param {Function} [completer] + * @param {boolean} [terminal] + * @returns {Interface} + */ +function createInterface(input, output, completer, terminal) { + return new Interface(input, output, completer, terminal); +} + +ObjectDefineProperties(Interface.prototype, { + // Redirect internal prototype methods to the underscore notation for backward + // compatibility. + [kSetRawMode]: { + __proto__: null, + get() { + return this._setRawMode; + }, + }, + [kOnLine]: { + __proto__: null, + get() { + return this._onLine; + }, + }, + [kWriteToOutput]: { + __proto__: null, + get() { + return this._writeToOutput; + }, + }, + [kAddHistory]: { + __proto__: null, + get() { + return this._addHistory; + }, + }, + [kRefreshLine]: { + __proto__: null, + get() { + return this._refreshLine; + }, + }, + [kNormalWrite]: { + __proto__: null, + get() { + return this._normalWrite; + }, + }, + [kInsertString]: { + __proto__: null, + get() { + return this._insertString; + }, + }, + [kTabComplete]: { + __proto__: null, + get() { + return this._tabComplete; + }, + }, + [kWordLeft]: { + __proto__: null, + get() { + return this._wordLeft; + }, + }, + [kWordRight]: { + __proto__: null, + get() { + return this._wordRight; + }, + }, + [kDeleteLeft]: { + __proto__: null, + get() { + return this._deleteLeft; + }, + }, + [kDeleteRight]: { + __proto__: null, + get() { + return this._deleteRight; + }, + }, + [kDeleteWordLeft]: { + __proto__: null, + get() { + return this._deleteWordLeft; + }, + }, + [kDeleteWordRight]: { + __proto__: null, + get() { + return this._deleteWordRight; + }, + }, + [kDeleteLineLeft]: { + __proto__: null, + get() { + return this._deleteLineLeft; + }, + }, + [kDeleteLineRight]: { + __proto__: null, + get() { + return this._deleteLineRight; + }, + }, + [kLine]: { + __proto__: null, + get() { + return this._line; + }, + }, + [kHistoryNext]: { + __proto__: null, + get() { + return this._historyNext; + }, + }, + [kHistoryPrev]: { + __proto__: null, + get() { + return this._historyPrev; + }, + }, + [kGetDisplayPos]: { + __proto__: null, + get() { + return this._getDisplayPos; + }, + }, + [kMoveCursor]: { + __proto__: null, + get() { + return this._moveCursor; + }, + }, + [kTtyWrite]: { + __proto__: null, + get() { + return this._ttyWrite; + }, + }, + + // Defining proxies for the internal instance properties for backward + // compatibility. + _decoder: { + __proto__: null, + get() { + return this[kDecoder]; + }, + set(value) { + this[kDecoder] = value; + }, + }, + _line_buffer: { + __proto__: null, + get() { + return this[kLine_buffer]; + }, + set(value) { + this[kLine_buffer] = value; + }, + }, + _oldPrompt: { + __proto__: null, + get() { + return this[kOldPrompt]; + }, + set(value) { + this[kOldPrompt] = value; + }, + }, + _previousKey: { + __proto__: null, + get() { + return this[kPreviousKey]; + }, + set(value) { + this[kPreviousKey] = value; + }, + }, + _prompt: { + __proto__: null, + get() { + return this[kPrompt]; + }, + set(value) { + this[kPrompt] = value; + }, + }, + _questionCallback: { + __proto__: null, + get() { + return this[kQuestionCallback]; + }, + set(value) { + this[kQuestionCallback] = value; + }, + }, + _sawKeyPress: { + __proto__: null, + get() { + return this[kSawKeyPress]; + }, + set(value) { + this[kSawKeyPress] = value; + }, + }, + _sawReturnAt: { + __proto__: null, + get() { + return this[kSawReturnAt]; + }, + set(value) { + this[kSawReturnAt] = value; + }, + }, +}); + +// Make internal methods public for backward compatibility. +Interface.prototype._setRawMode = _Interface.prototype[kSetRawMode]; +Interface.prototype._onLine = _Interface.prototype[kOnLine]; +Interface.prototype._writeToOutput = _Interface.prototype[kWriteToOutput]; +Interface.prototype._addHistory = _Interface.prototype[kAddHistory]; +Interface.prototype._refreshLine = _Interface.prototype[kRefreshLine]; +Interface.prototype._normalWrite = _Interface.prototype[kNormalWrite]; +Interface.prototype._insertString = _Interface.prototype[kInsertString]; +Interface.prototype._tabComplete = function (lastKeypressWasTab) { + // Overriding parent method because `this.completer` in the legacy + // implementation takes a callback instead of being an async function. + this.pause(); + const string = StringPrototypeSlice(this.line, 0, this.cursor); + this.completer(string, (err, value) => { + this.resume(); + + if (err) { + this._writeToOutput(`Tab completion error: ${nodeInspect.inspect(err)}`); + return; + } + + this[kTabCompleter](lastKeypressWasTab, value); + }); +}; +Interface.prototype._wordLeft = _Interface.prototype[kWordLeft]; +Interface.prototype._wordRight = _Interface.prototype[kWordRight]; +Interface.prototype._deleteLeft = _Interface.prototype[kDeleteLeft]; +Interface.prototype._deleteRight = _Interface.prototype[kDeleteRight]; +Interface.prototype._deleteWordLeft = _Interface.prototype[kDeleteWordLeft]; +Interface.prototype._deleteWordRight = _Interface.prototype[kDeleteWordRight]; +Interface.prototype._deleteLineLeft = _Interface.prototype[kDeleteLineLeft]; +Interface.prototype._deleteLineRight = _Interface.prototype[kDeleteLineRight]; +Interface.prototype._line = _Interface.prototype[kLine]; +Interface.prototype._historyNext = _Interface.prototype[kHistoryNext]; +Interface.prototype._historyPrev = _Interface.prototype[kHistoryPrev]; +Interface.prototype._getDisplayPos = _Interface.prototype[kGetDisplayPos]; +Interface.prototype._getCursorPos = _Interface.prototype.getCursorPos; +Interface.prototype._moveCursor = _Interface.prototype[kMoveCursor]; +Interface.prototype._ttyWrite = _Interface.prototype[kTtyWrite]; + +function _ttyWriteDumb(s, key) { + key ||= kEmptyObject; + if (key.name === "escape") return; + + if (this[kSawReturnAt] && key.name !== "enter") this[kSawReturnAt] = 0; + + if (key.ctrl) { + if (key.name === "c") { + if (this.listenerCount("SIGINT") > 0) { + this.emit("SIGINT"); + } else { + // This readline instance is finished + this.close(); + } + + return; + } else if (key.name === "d") { + this.close(); + return; + } + } + + switch (key.name) { + case "return": // Carriage return, i.e. \r + this[kSawReturnAt] = DateNow(); + this._line(); + break; + + case "enter": + // When key interval > crlfDelay + if (this[kSawReturnAt] === 0 || DateNow() - this[kSawReturnAt] > this.crlfDelay) { + this._line(); + } + this[kSawReturnAt] = 0; + break; + + default: + if (typeof s === "string" && s) { + this.line += s; + this.cursor += s.length; + this._writeToOutput(s); + } + } +} + +__node_module__.exports = { + Interface, + clearLine, + clearScreenDown, + createInterface, + cursorTo, + emitKeypressEvents, + moveCursor, + promises, +}; + +// Bun-internal hook consumed by pre-existing readline tests/utilities. +// Non-enumerable so it stays off the public node:readline surface. +Object.defineProperty(__node_module__.exports, Symbol.for("__BUN_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED__"), { + __proto__: null, + // A test-only hook; keep it lazy so `require("node:readline")` doesn't pull + // in internal/util/inspect and node:util just to publish it. + get() { + return { + CSI: require("internal/readline/utils").CSI, + utils: { + getStringWidth: require("internal/util/inspect").getStringWidth, + stripVTControlCharacters: require("node:util").stripVTControlCharacters, + }, + }; + }, +}); + +// The builtin bundler dedupe-renames the second `function question`; +// promisify(question).name must stay 'question' (test-util-promisify-custom-names). +Object.defineProperty(Interface.prototype.question[promisify.custom], "name", { value: "question" }); + +export default __node_module__.exports; diff --git a/src/js/node/readline.promises.js b/src/js/node/readline.promises.js new file mode 100644 index 000000000000..59bd5cd9c7ad --- /dev/null +++ b/src/js/node/readline.promises.js @@ -0,0 +1,60 @@ +// Ported from Node.js v26.3.0 lib/readline/promises.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 { Promise, SymbolDispose } = primordials; + +const { Readline } = require("internal/readline/promises"); + +const { Interface: _Interface, kQuestion, kQuestionCancel, kQuestionReject } = require("internal/readline/interface"); + +const { AbortError } = require("internal/repl/node-errors"); +const { validateAbortSignal } = require("internal/validators"); + +const { kEmptyObject } = require("internal/shared"); +let addAbortListener; + +class Interface extends _Interface { + question(query, options = kEmptyObject) { + return new Promise((resolve, reject) => { + let cb = resolve; + + if (options?.signal) { + validateAbortSignal(options.signal, "options.signal"); + if (options.signal.aborted) { + return reject(new AbortError(undefined, { cause: options.signal.reason })); + } + + const onAbort = () => { + this[kQuestionCancel](); + reject(new AbortError(undefined, { cause: options.signal.reason })); + }; + addAbortListener ??= require("internal/abort_listener").addAbortListener; + const disposable = addAbortListener(options.signal, onAbort); + + cb = answer => { + disposable[SymbolDispose](); + resolve(answer); + }; + } + + this[kQuestionReject] = reject; + + this[kQuestion](query, cb); + }); + } +} + +function createInterface(input, output, completer, terminal) { + return new Interface(input, output, completer, terminal); +} + +__node_module__.exports = { + Interface, + Readline, + createInterface, +}; + +export default __node_module__.exports; diff --git a/src/js/node/readline.promises.ts b/src/js/node/readline.promises.ts deleted file mode 100644 index 6ab60f0b2b98..000000000000 --- a/src/js/node/readline.promises.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Hardcoded module "node:readline/promises" -export default require("node:readline").promises; diff --git a/src/js/node/readline.ts b/src/js/node/readline.ts deleted file mode 100644 index dc1e52d686f1..000000000000 --- a/src/js/node/readline.ts +++ /dev/null @@ -1,2753 +0,0 @@ -// Hardcoded module "node:readline" -// Attribution: Some parts of of this module are derived from code originating from the Node.js -// readline module which is licensed under an MIT license: -// -// Copyright Node.js contributors. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -// IN THE SOFTWARE. - -// ---------------------------------------------------------------------------- -// Section: Imports -// ---------------------------------------------------------------------------- -const EventEmitter = require("node:events"); -const { StringDecoder } = require("node:string_decoder"); -const { promisify } = require("internal/promisify"); -const { SafeStringIterator } = require("internal/primordials"); - -const { - validateFunction, - validateAbortSignal, - validateArray, - validateString, - validateBoolean, - validateInteger, - validateUint32, - validateNumber, -} = require("internal/validators"); - -const internalGetStringWidth = $newCppFunction("stringWidth.cpp", "jsFunctionBunStringWidth", 1); - -const PromiseReject = Promise.$reject; - -var isWritable; - -var { inspect } = Bun; -var debug = process.env.BUN_JS_DEBUG ? console.log : () => {}; - -// ---------------------------------------------------------------------------- -// Section: Preamble -// ---------------------------------------------------------------------------- - -const SymbolAsyncIterator = Symbol.asyncIterator; -const SymbolFor = Symbol.for; -const ArrayFrom = Array.from; -const ArrayPrototypeFilter = Array.prototype.filter; -const ArrayPrototypeSort = Array.prototype.sort; -const ArrayPrototypeIndexOf = Array.prototype.indexOf; -const ArrayPrototypeJoin = Array.prototype.join; -const ArrayPrototypeMap = Array.prototype.map; -const ArrayPrototypePop = Array.prototype.pop; -const ArrayPrototypePush = Array.prototype.push; -const ArrayPrototypeSlice = Array.prototype.slice; -const ArrayPrototypeSplice = Array.prototype.splice; -const ArrayPrototypeReverse = Array.prototype.reverse; -const ArrayPrototypeShift = Array.prototype.shift; -const ArrayPrototypeUnshift = Array.prototype.unshift; -const RegExpPrototypeExec = RegExp.prototype.exec; -const StringFromCharCode = String.fromCharCode; -const StringPrototypeCharCodeAt = String.prototype.charCodeAt; -const StringPrototypeCodePointAt = String.prototype.codePointAt; -const StringPrototypeSlice = String.prototype.slice; -const StringPrototypeToLowerCase = String.prototype.toLowerCase; -const StringPrototypeEndsWith = String.prototype.endsWith; -const StringPrototypeRepeat = String.prototype.repeat; -const StringPrototypeStartsWith = String.prototype.startsWith; -const StringPrototypeTrim = String.prototype.trim; -const NumberIsNaN = Number.isNaN; -const NumberIsFinite = Number.isFinite; -const MathCeil = Math.ceil; -const MathFloor = Math.floor; -const MathMax = Math.max; -const DateNow = Date.now; -const ObjectDefineProperties = Object.defineProperties; -const ObjectFreeze = Object.freeze; -const ObjectCreate = Object.create; - -// ---------------------------------------------------------------------------- -// Section: "Internal" modules -// ---------------------------------------------------------------------------- - -/** - * Returns the number of columns required to display the given string. - */ -var getStringWidth = function getStringWidth(str, removeControlChars = true) { - return internalGetStringWidth(str, removeControlChars); -}; - -const stripANSI = Bun.stripANSI; - -/** - * Remove all VT control characters. Use to estimate displayed string width. - */ -function stripVTControlCharacters(str) { - validateString(str, "str"); - return stripANSI(str); -} - -// Constants - -const kUTF16SurrogateThreshold = 0x10000; // 2 ** 16 -const kEscape = "\x1b"; -const kSubstringSearch = Symbol("kSubstringSearch"); - -// ---------------------------------------------------------------------------- -// Section: Utils -// ---------------------------------------------------------------------------- - -function CSI(strings, ...args) { - var ret = `${kEscape}[`; - for (var n = 0; n < strings.length; n++) { - ret += strings[n]; - if (n < args.length) ret += args[n]; - } - return ret; -} - -var kClearLine, kClearScreenDown, kClearToLineBeginning, kClearToLineEnd; - -CSI.kEscape = kEscape; -CSI.kClearLine = kClearLine = CSI`2K`; -CSI.kClearScreenDown = kClearScreenDown = CSI`0J`; -CSI.kClearToLineBeginning = kClearToLineBeginning = CSI`1K`; -CSI.kClearToLineEnd = kClearToLineEnd = CSI`0K`; - -function charLengthLeft(str: string, i: number) { - if (i <= 0) return 0; - if ( - (i > 1 && StringPrototypeCodePointAt.$call(str, i - 2) >= kUTF16SurrogateThreshold) || - StringPrototypeCodePointAt.$call(str, i - 1) >= kUTF16SurrogateThreshold - ) { - return 2; - } - return 1; -} - -function charLengthAt(str, i) { - if (str.length <= i) { - // Pretend to move to the right. This is necessary to autocomplete while - // moving to the right. - return 1; - } - return StringPrototypeCodePointAt.$call(str, i) >= kUTF16SurrogateThreshold ? 2 : 1; -} - -/* - Some patterns seen in terminal key escape codes, derived from combos seen - at http://www.midnight-commander.org/browser/lib/tty/key.c - - ESC letter - ESC [ letter - ESC [ modifier letter - ESC [ 1 ; modifier letter - ESC [ num char - ESC [ num ; modifier char - ESC O letter - ESC O modifier letter - ESC O 1 ; modifier letter - ESC N letter - ESC [ [ num ; modifier char - ESC [ [ 1 ; modifier letter - ESC ESC [ num char - ESC ESC O letter - - - char is usually ~ but $ and ^ also happen with rxvt - - modifier is 1 + - (shift * 1) + - (left_alt * 2) + - (ctrl * 4) + - (right_alt * 8) - - two leading ESCs apparently mean the same as one leading ESC -*/ -function* emitKeys(stream) { - while (true) { - let ch = yield; - let s = ch; - let escaped = false; - const key: { - sequence: string | null; - name?: string; - code?: string; - ctrl: boolean; - meta: boolean; - shift: boolean; - } = { - sequence: null, - name: undefined, - ctrl: false, - meta: false, - shift: false, - }; - - if (ch === kEscape) { - escaped = true; - s += ch = yield; - - if (ch === kEscape) { - s += ch = yield; - } - } - - if (escaped && (ch === "O" || ch === "[")) { - // ANSI escape sequence - let code = ch; - let modifier = 0; - - if (ch === "O") { - // ESC O letter - // ESC O modifier letter - s += ch = yield; - - if (ch >= "0" && ch <= "9") { - modifier = (ch >> 0) - 1; - s += ch = yield; - } - - code += ch; - } else if (ch === "[") { - // ESC [ letter - // ESC [ modifier letter - // ESC [ [ modifier letter - // ESC [ [ num char - s += ch = yield; - - if (ch === "[") { - // \x1b[[A - // ^--- escape codes might have a second bracket - code += ch; - s += ch = yield; - } - - /* - * Here and later we try to buffer just enough data to get - * a complete ascii sequence. - * - * We have basically two classes of ascii characters to process: - * - * - * 1. `\x1b[24;5~` should be parsed as { code: '[24~', modifier: 5 } - * - * This particular example is featuring Ctrl+F12 in xterm. - * - * - `;5` part is optional, e.g. it could be `\x1b[24~` - * - first part can contain one or two digits - * - there is also special case when there can be 3 digits - * but without modifier. They are the case of paste bracket mode - * - * So the generic regexp is like /^(?:\d\d?(;\d)?[~^$]|\d{3}~)$/ - * - * - * 2. `\x1b[1;5H` should be parsed as { code: '[H', modifier: 5 } - * - * This particular example is featuring Ctrl+Home in xterm. - * - * - `1;5` part is optional, e.g. it could be `\x1b[H` - * - `1;` part is optional, e.g. it could be `\x1b[5H` - * - * So the generic regexp is like /^((\d;)?\d)?[A-Za-z]$/ - * - */ - const cmdStart = s.length - 1; - - // Skip one or two leading digits - if (ch >= "0" && ch <= "9") { - s += ch = yield; - - if (ch >= "0" && ch <= "9") { - s += ch = yield; - - if (ch >= "0" && ch <= "9") { - s += ch = yield; - } - } - } - - // skip modifier - if (ch === ";") { - s += ch = yield; - - if (ch >= "0" && ch <= "9") { - s += yield; - } - } - - /* - * We buffered enough data, now trying to extract code - * and modifier from it - */ - const cmd = StringPrototypeSlice.$call(s, cmdStart); - let match; - - if ((match = RegExpPrototypeExec.$call(/^(?:(\d\d?)(?:;(\d))?([~^$])|(\d{3}~))$/, cmd))) { - if (match[4]) { - code += match[4]; - } else { - code += match[1] + match[3]; - modifier = (match[2] || 1) - 1; - } - } else if ((match = RegExpPrototypeExec.$call(/^((\d;)?(\d))?([A-Za-z])$/, cmd))) { - code += match[4]; - modifier = (match[3] || 1) - 1; - } else { - code += cmd; - } - } - - // Parse the key modifier - key.ctrl = !!(modifier & 4); - key.meta = !!(modifier & 10); - key.shift = !!(modifier & 1); - key.code = code; - - // Parse the key itself - switch (code) { - /* xterm/gnome ESC [ letter (with modifier) */ - case "[P": - key.name = "f1"; - break; - case "[Q": - key.name = "f2"; - break; - case "[R": - key.name = "f3"; - break; - case "[S": - key.name = "f4"; - break; - - /* xterm/gnome ESC O letter (without modifier) */ - case "OP": - key.name = "f1"; - break; - case "OQ": - key.name = "f2"; - break; - case "OR": - key.name = "f3"; - break; - case "OS": - key.name = "f4"; - break; - - /* xterm/rxvt ESC [ number ~ */ - case "[11~": - key.name = "f1"; - break; - case "[12~": - key.name = "f2"; - break; - case "[13~": - key.name = "f3"; - break; - case "[14~": - key.name = "f4"; - break; - - /* paste bracket mode */ - case "[200~": - key.name = "paste-start"; - break; - case "[201~": - key.name = "paste-end"; - break; - - /* from Cygwin and used in libuv */ - case "[[A": - key.name = "f1"; - break; - case "[[B": - key.name = "f2"; - break; - case "[[C": - key.name = "f3"; - break; - case "[[D": - key.name = "f4"; - break; - case "[[E": - key.name = "f5"; - break; - - /* common */ - case "[15~": - key.name = "f5"; - break; - case "[17~": - key.name = "f6"; - break; - case "[18~": - key.name = "f7"; - break; - case "[19~": - key.name = "f8"; - break; - case "[20~": - key.name = "f9"; - break; - case "[21~": - key.name = "f10"; - break; - case "[23~": - key.name = "f11"; - break; - case "[24~": - key.name = "f12"; - break; - - /* xterm ESC [ letter */ - case "[A": - key.name = "up"; - break; - case "[B": - key.name = "down"; - break; - case "[C": - key.name = "right"; - break; - case "[D": - key.name = "left"; - break; - case "[E": - key.name = "clear"; - break; - case "[F": - key.name = "end"; - break; - case "[H": - key.name = "home"; - break; - - /* xterm/gnome ESC O letter */ - case "OA": - key.name = "up"; - break; - case "OB": - key.name = "down"; - break; - case "OC": - key.name = "right"; - break; - case "OD": - key.name = "left"; - break; - case "OE": - key.name = "clear"; - break; - case "OF": - key.name = "end"; - break; - case "OH": - key.name = "home"; - break; - - /* xterm/rxvt ESC [ number ~ */ - case "[1~": - key.name = "home"; - break; - case "[2~": - key.name = "insert"; - break; - case "[3~": - key.name = "delete"; - break; - case "[4~": - key.name = "end"; - break; - case "[5~": - key.name = "pageup"; - break; - case "[6~": - key.name = "pagedown"; - break; - - /* putty */ - case "[[5~": - key.name = "pageup"; - break; - case "[[6~": - key.name = "pagedown"; - break; - - /* rxvt */ - case "[7~": - key.name = "home"; - break; - case "[8~": - key.name = "end"; - break; - - /* rxvt keys with modifiers */ - case "[a": - key.name = "up"; - key.shift = true; - break; - case "[b": - key.name = "down"; - key.shift = true; - break; - case "[c": - key.name = "right"; - key.shift = true; - break; - case "[d": - key.name = "left"; - key.shift = true; - break; - case "[e": - key.name = "clear"; - key.shift = true; - break; - - case "[2$": - key.name = "insert"; - key.shift = true; - break; - case "[3$": - key.name = "delete"; - key.shift = true; - break; - case "[5$": - key.name = "pageup"; - key.shift = true; - break; - case "[6$": - key.name = "pagedown"; - key.shift = true; - break; - case "[7$": - key.name = "home"; - key.shift = true; - break; - case "[8$": - key.name = "end"; - key.shift = true; - break; - - case "Oa": - key.name = "up"; - key.ctrl = true; - break; - case "Ob": - key.name = "down"; - key.ctrl = true; - break; - case "Oc": - key.name = "right"; - key.ctrl = true; - break; - case "Od": - key.name = "left"; - key.ctrl = true; - break; - case "Oe": - key.name = "clear"; - key.ctrl = true; - break; - - case "[2^": - key.name = "insert"; - key.ctrl = true; - break; - case "[3^": - key.name = "delete"; - key.ctrl = true; - break; - case "[5^": - key.name = "pageup"; - key.ctrl = true; - break; - case "[6^": - key.name = "pagedown"; - key.ctrl = true; - break; - case "[7^": - key.name = "home"; - key.ctrl = true; - break; - case "[8^": - key.name = "end"; - key.ctrl = true; - break; - - /* misc. */ - case "[Z": - key.name = "tab"; - key.shift = true; - break; - default: - key.name = "undefined"; - break; - } - } else if (ch === "\r") { - // carriage return - key.name = "return"; - key.meta = escaped; - } else if (ch === "\n") { - // Enter, should have been called linefeed - key.name = "enter"; - key.meta = escaped; - } else if (ch === "\t") { - // tab - key.name = "tab"; - key.meta = escaped; - } else if (ch === "\b" || ch === "\x7f") { - // backspace or ctrl+h - key.name = "backspace"; - key.meta = escaped; - } else if (ch === kEscape) { - // escape key - key.name = "escape"; - key.meta = escaped; - } else if (ch === " ") { - key.name = "space"; - key.meta = escaped; - } else if (!escaped && ch <= "\x1a") { - // ctrl+letter - key.name = StringFromCharCode(StringPrototypeCharCodeAt.$call(ch, 0) + StringPrototypeCharCodeAt.$call("a", 0) - 1); // prettier-ignore - key.ctrl = true; - } else if (RegExpPrototypeExec.$call(/^[0-9A-Za-z]$/, ch) !== null) { - // Letter, number, shift+letter - key.name = StringPrototypeToLowerCase.$call(ch); - key.shift = RegExpPrototypeExec.$call(/^[A-Z]$/, ch) !== null; - key.meta = escaped; - } else if (escaped) { - // Escape sequence timeout - key.name = ch.length ? undefined : "escape"; - key.meta = true; - } - - key.sequence = s; - - if (s.length !== 0 && (key.name !== undefined || escaped)) { - /* Named character or sequence */ - stream.emit("keypress", escaped ? undefined : s, key); - } else if (charLengthAt(s, 0) === s.length) { - /* Single unnamed character, e.g. "." */ - stream.emit("keypress", s, key); - } - /* Unrecognized or broken escape sequence, don't emit anything */ - } -} - -// This runs in O(n log n). -function commonPrefix(strings) { - if (strings.length === 0) { - return ""; - } - if (strings.length === 1) { - return strings[0]; - } - var sorted = ArrayPrototypeSort.$call(ArrayPrototypeSlice.$call(strings)); - var min = sorted[0]; - var max = sorted[sorted.length - 1]; - for (var i = 0; i < min.length; i++) { - if (min[i] !== max[i]) { - return StringPrototypeSlice.$call(min, 0, i); - } - } - return min; -} - -// ---------------------------------------------------------------------------- -// Section: Cursor Functions -// ---------------------------------------------------------------------------- - -/** - * 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 $ERR_INVALID_ARG_VALUE("x", x); - if (NumberIsNaN(y)) throw $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 $ERR_INVALID_CURSOR_POS(); - - var 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; - } - - var 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; - } - - var 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); -} - -// ---------------------------------------------------------------------------- -// Section: Emit keypress events -// ---------------------------------------------------------------------------- - -var KEYPRESS_DECODER = Symbol("keypress-decoder"); -var ESCAPE_DECODER = Symbol("escape-decoder"); - -// GNU readline library - keyseq-timeout is 500ms (default) -var ESCAPE_CODE_TIMEOUT = 500; - -/** - * accepts a readable Stream instance and makes it emit "keypress" events - */ - -function emitKeypressEvents(stream, iface = {}) { - if (stream[KEYPRESS_DECODER]) return; - - stream[KEYPRESS_DECODER] = new StringDecoder("utf8"); - - stream[ESCAPE_DECODER] = emitKeys(stream); - stream[ESCAPE_DECODER].next(); - - var triggerEscape = () => stream[ESCAPE_DECODER].next(""); - var { escapeCodeTimeout = ESCAPE_CODE_TIMEOUT } = iface; - var timeoutId; - - function onData(input) { - if (stream.listenerCount("keypress") > 0) { - var string = stream[KEYPRESS_DECODER].write(input); - if (string) { - clearTimeout(timeoutId); - - // This supports characters of length 2. - iface[kSawKeyPress] = charLengthAt(string, 0) === string.length; - iface.isCompletionEnabled = false; - - var length = 0; - for (var character of new SafeStringIterator(string)) { - length += character.length; - if (length === string.length) { - iface.isCompletionEnabled = true; - } - - try { - stream[ESCAPE_DECODER].next(character); - // Escape letter at the tail position - if (length === string.length && character === kEscape) { - timeoutId = setTimeout(triggerEscape, escapeCodeTimeout); - } - } catch (err) { - // If the generator throws (it could happen in the `keypress` - // event), we need to restart it. - stream[ESCAPE_DECODER] = emitKeys(stream); - stream[ESCAPE_DECODER].next(); - throw err; - } - } - } - } else { - // Nobody's watching anyway - stream.removeListener("data", onData); - stream.on("newListener", onNewListener); - } - } - - function onNewListener(event) { - if (event === "keypress") { - stream.on("data", onData); - stream.removeListener("newListener", onNewListener); - } - } - - if (stream.listenerCount("keypress") > 0) { - stream.on("data", onData); - } else { - stream.on("newListener", onNewListener); - } -} - -// ---------------------------------------------------------------------------- -// Section: Interface -// ---------------------------------------------------------------------------- - -var kEmptyObject = ObjectFreeze(ObjectCreate(null)); - -// Some constants regarding configuration of interface -var kHistorySize = 30; -var kMaxUndoRedoStackSize = 2048; -var kMincrlfDelay = 100; -// \r\n, \n, or \r followed by something other than \n -var lineEnding = /\r?\n|\r(?!\n)/g; - -// Max length of the kill ring -var kMaxLengthOfKillRing = 32; - -// Symbols - -// Public symbols -var kLineObjectStream = Symbol("line object stream"); -var kQuestionCancel = Symbol("kQuestionCancel"); -var kQuestion = Symbol("kQuestion"); - -// Private symbols -var kAddHistory = Symbol("_addHistory"); -var kBeforeEdit = Symbol("_beforeEdit"); -var kDecoder = Symbol("_decoder"); -var kDeleteLeft = Symbol("_deleteLeft"); -var kDeleteLineLeft = Symbol("_deleteLineLeft"); -var kDeleteLineRight = Symbol("_deleteLineRight"); -var kDeleteRight = Symbol("_deleteRight"); -var kDeleteWordLeft = Symbol("_deleteWordLeft"); -var kDeleteWordRight = Symbol("_deleteWordRight"); -var kGetDisplayPos = Symbol("_getDisplayPos"); -var kHistoryNext = Symbol("_historyNext"); -var kHistoryPrev = Symbol("_historyPrev"); -var kInsertString = Symbol("_insertString"); -var kLine = Symbol("_line"); -var kLine_buffer = Symbol("_line_buffer"); -var kKillRing = Symbol("_killRing"); -var kKillRingCursor = Symbol("_killRingCursor"); -var kMoveCursor = Symbol("_moveCursor"); -var kNormalWrite = Symbol("_normalWrite"); -var kOldPrompt = Symbol("_oldPrompt"); -var kOnLine = Symbol("_onLine"); -var kPreviousKey = Symbol("_previousKey"); -var kPrompt = Symbol("_prompt"); -var kPushToKillRing = Symbol("_pushToKillRing"); -var kPushToUndoStack = Symbol("_pushToUndoStack"); -var kQuestionCallback = Symbol("_questionCallback"); -var kRedo = Symbol("_redo"); -var kRedoStack = Symbol("_redoStack"); -var kRefreshLine = Symbol("_refreshLine"); -var kSawKeyPress = Symbol("_sawKeyPress"); -var kSawReturnAt = Symbol("_sawReturnAt"); -var kSetRawMode = Symbol("_setRawMode"); -var kTabComplete = Symbol("_tabComplete"); -var kTabCompleter = Symbol("_tabCompleter"); -var kTtyWrite = Symbol("_ttyWrite"); -var kUndo = Symbol("_undo"); -var kUndoStack = Symbol("_undoStack"); -var kWordLeft = Symbol("_wordLeft"); -var kWordRight = Symbol("_wordRight"); -var kWriteToOutput = Symbol("_writeToOutput"); -var kYank = Symbol("_yank"); -var kYanking = Symbol("_yanking"); -var kYankPop = Symbol("_yankPop"); - -// Event symbols -var kFirstEventParam = SymbolFor("nodejs.kFirstEventParam"); - -// class InterfaceConstructor extends EventEmitter { -// #onSelfCloseWithTerminal; -// #onSelfCloseWithoutTerminal; - -// #onError; -// #onData; -// #onEnd; -// #onTermEnd; -// #onKeyPress; -// #onResize; - -// [kSawReturnAt]; -// isCompletionEnabled = true; -// [kSawKeyPress]; -// [kPreviousKey]; -// escapeCodeTimeout; -// tabSize; - -// line; -// [kSubstringSearch]; -// output; -// input; -// [kUndoStack]; -// [kRedoStack]; -// history; -// historySize; - -// [kKillRing]; -// [kKillRingCursor]; - -// removeHistoryDuplicates; -// crlfDelay; -// completer; - -// terminal; -// [kLineObjectStream]; - -// cursor; -// historyIndex; - -// constructor(input, output, completer, terminal) { -// super(); - -var kOnSelfCloseWithTerminal = Symbol("_onSelfCloseWithTerminal"); -var kOnSelfCloseWithoutTerminal = Symbol("_onSelfCloseWithoutTerminal"); -var kOnKeyPress = Symbol("_onKeyPress"); -var kOnError = Symbol("_onError"); -var kOnData = Symbol("_onData"); -var kOnEnd = Symbol("_onEnd"); -var kOnTermEnd = Symbol("_onTermEnd"); -var kOnResize = Symbol("_onResize"); - -function onSelfCloseWithTerminal() { - var input = this.input; - var output = this.output; - - if (!input) throw new Error("Input not set, invalid state for readline!"); - - input.removeListener("keypress", this[kOnKeyPress]); - input.removeListener("error", this[kOnError]); - input.removeListener("end", this[kOnTermEnd]); - if (output !== null && output !== undefined) { - output.removeListener("resize", this[kOnResize]); - } -} - -function onSelfCloseWithoutTerminal() { - var input = this.input; - if (!input) throw new Error("Input not set, invalid state for readline!"); - - input.removeListener("data", this[kOnData]); - input.removeListener("error", this[kOnError]); - input.removeListener("end", this[kOnEnd]); -} - -function onError(err) { - this.emit("error", err); -} - -function onData(data) { - debug("onData"); - this[kNormalWrite](data); -} - -function onEnd() { - debug("onEnd"); - if (typeof this[kLine_buffer] === "string" && this[kLine_buffer].length > 0) { - this.emit("line", this[kLine_buffer]); - } - this.close(); -} - -function onTermEnd() { - debug("onTermEnd"); - const line = this.line; - if (typeof line === "string" && line.length > 0) { - this.emit("line", line); - } - this.close(); -} - -function onKeyPress(s, key) { - this[kTtyWrite](s, key); - const sequence = key ? key.sequence : undefined; - if (sequence) { - // If the keySeq is half of a surrogate pair - // (>= 0xd800 and <= 0xdfff), refresh the line so - // the character is displayed appropriately. - var ch = StringPrototypeCodePointAt.$call(sequence, 0)!; - if (ch >= 0xd800 && ch <= 0xdfff) this[kRefreshLine](); - } -} - -function onResize() { - this[kRefreshLine](); -} - -function InterfaceConstructor(input, output, completer, terminal) { - if (!(this instanceof InterfaceConstructor)) { - return new InterfaceConstructor(input, output, completer, terminal); - } - - EventEmitter.$call(this); - - this[kOnSelfCloseWithoutTerminal] = onSelfCloseWithoutTerminal.bind(this); - this[kOnSelfCloseWithTerminal] = onSelfCloseWithTerminal.bind(this); - - this[kOnError] = onError.bind(this); - this[kOnData] = onData.bind(this); - this[kOnEnd] = onEnd.bind(this); - this[kOnTermEnd] = onTermEnd.bind(this); - this[kOnKeyPress] = onKeyPress.bind(this); - this[kOnResize] = onResize.bind(this); - - this[kSawReturnAt] = 0; - this.isCompletionEnabled = true; - this[kSawKeyPress] = false; - this[kPreviousKey] = null; - this.escapeCodeTimeout = ESCAPE_CODE_TIMEOUT; - this.tabSize = 8; - - var history; - var historySize; - var removeHistoryDuplicates = false; - var crlfDelay; - var prompt = "> "; - var signal; - - if (input?.input) { - // An options object was given - output = input.output; - completer = input.completer; - terminal = input.terminal; - history = input.history; - historySize = input.historySize; - signal = input.signal; - - var tabSize = input.tabSize; - if (tabSize !== undefined) { - validateUint32(tabSize, "tabSize", true); - this.tabSize = tabSize; - } - removeHistoryDuplicates = input.removeHistoryDuplicates; - - var inputPrompt = input.prompt; - if (inputPrompt !== undefined) { - prompt = inputPrompt; - } - - var inputEscapeCodeTimeout = input.escapeCodeTimeout; - if (inputEscapeCodeTimeout !== undefined) { - if (NumberIsFinite(inputEscapeCodeTimeout)) { - this.escapeCodeTimeout = inputEscapeCodeTimeout; - } else { - throw $ERR_INVALID_ARG_VALUE("input.escapeCodeTimeout", this.escapeCodeTimeout); - } - } - - if (signal) { - validateAbortSignal(signal, "options.signal"); - } - - crlfDelay = input.crlfDelay; - input = input.input; - } - - if (completer !== undefined && typeof completer !== "function") { - throw $ERR_INVALID_ARG_VALUE("completer", completer); - } - - if (history === undefined) { - history = []; - } else { - validateArray(history, "history"); - } - - if (historySize === undefined) { - historySize = kHistorySize; - } - - validateNumber(historySize, "historySize", 0); - - // Backwards compat; check the isTTY prop of the output stream - // when `terminal` was not specified - if (terminal === undefined && !(output == null)) { - terminal = !!output.isTTY; - } - - this.line = ""; - this[kSubstringSearch] = null; - this.output = output; - this.input = input; - this[kUndoStack] = []; - this[kRedoStack] = []; - this.history = history; - this.historySize = historySize; - - // The kill ring is a global list of blocks of text that were previously - // killed (deleted). If its size exceeds kMaxLengthOfKillRing, the oldest - // element will be removed to make room for the latest deletion. With kill - // ring, users are able to recall (yank) or cycle (yank pop) among previously - // killed texts, quite similar to the behavior of Emacs. - this[kKillRing] = []; - this[kKillRingCursor] = 0; - - this.removeHistoryDuplicates = !!removeHistoryDuplicates; - this.crlfDelay = crlfDelay ? MathMax(kMincrlfDelay, crlfDelay) : kMincrlfDelay; - this.completer = completer; - - this.setPrompt(prompt); - - this.terminal = !!terminal; - - this[kLineObjectStream] = undefined; - - input.on("error", this[kOnError]); - - if (!this.terminal) { - this[kDecoder] = new StringDecoder("utf8"); - input.on("data", this[kOnData]); - input.on("end", this[kOnEnd]); - this.once("close", this[kOnSelfCloseWithoutTerminal]); - } else { - emitKeypressEvents(input, this); - - // `input` usually refers to stdin - input.on("keypress", this[kOnKeyPress]); - input.on("end", this[kOnTermEnd]); - - this[kSetRawMode](true); - this.terminal = true; - - // Cursor position on the line. - this.cursor = 0; - this.historyIndex = -1; - - if (output !== null && output !== undefined) output.on("resize", this[kOnResize]); - - this.once("close", this[kOnSelfCloseWithTerminal]); - } - - if (signal) { - var onAborted = (() => this.close()).bind(this); - if (signal.aborted) { - process.nextTick(onAborted); - } else { - signal.addEventListener("abort", onAborted, { once: true }); - this.once("close", () => signal.removeEventListener("abort", onAborted)); - } - } - - // Current line - this.line = ""; - - input.resume(); -} -$toClass(InterfaceConstructor, "InterfaceConstructor", EventEmitter); - -var _Interface = class Interface extends InterfaceConstructor { - // eslint-disable-next-line no-useless-constructor - constructor(input, output, completer, terminal) { - super(input, output, completer, terminal); - } - [Symbol.dispose]() { - this.close(); - } - get columns() { - var output = this.output; - var columns = output ? output.columns : undefined; - if (columns) return columns; - return Infinity; - } - - /** - * Sets the prompt written to the output. - * @param {string} prompt - * @returns {void} - */ - setPrompt(prompt) { - this[kPrompt] = prompt; - } - - /** - * Returns the current prompt used by `rl.prompt()`. - * @returns {string} - */ - getPrompt() { - return this[kPrompt]; - } - - [kSetRawMode](mode) { - const wasInRawMode = this.input.isRaw; - - var setRawMode = this.input.setRawMode; - if (typeof setRawMode === "function") { - setRawMode.$call(this.input, mode); - } - - return wasInRawMode; - } - - /** - * Writes the configured `prompt` to a new line in `output`. - * @param {boolean} [preserveCursor] - * @returns {void} - */ - prompt(preserveCursor?) { - if (this.paused) this.resume(); - if (this.terminal && process.env.TERM !== "dumb") { - if (!preserveCursor) this.cursor = 0; - this[kRefreshLine](); - } else { - this[kWriteToOutput](this[kPrompt]); - } - } - - [kQuestion](query, cb) { - if (this.closed) { - throw $ERR_USE_AFTER_CLOSE("readline"); - } - if (this[kQuestionCallback]) { - this.prompt(); - } else { - this[kOldPrompt] = this[kPrompt]; - this.setPrompt(query); - this[kQuestionCallback] = cb; - this.prompt(); - } - } - - [kOnLine](line) { - if (this[kQuestionCallback]) { - var cb = this[kQuestionCallback]; - this[kQuestionCallback] = null; - this.setPrompt(this[kOldPrompt]); - cb(line); - } else { - this.emit("line", line); - } - } - - [kBeforeEdit](oldText, oldCursor) { - this[kPushToUndoStack](oldText, oldCursor); - } - - [kQuestionCancel]() { - if (this[kQuestionCallback]) { - this[kQuestionCallback] = null; - this.setPrompt(this[kOldPrompt]); - this.clearLine(); - } - } - - [kWriteToOutput](stringToWrite) { - validateString(stringToWrite, "stringToWrite"); - - const output = this.output; - if (output !== null && output !== undefined) { - output.write(stringToWrite); - } - } - - [kAddHistory]() { - const line = this.line; - if (line.length === 0) return ""; - - // If the history is disabled then return the line - if (this.historySize === 0) return line; - - // If the trimmed line is empty then return the line - if (StringPrototypeTrim.$call(line).length === 0) return line; - - const history = this.history; - const historyEmpty = history.length === 0; - if (historyEmpty || history[0] !== line) { - if (this.removeHistoryDuplicates) { - // Remove older history line if identical to new one - var dupIndex = ArrayPrototypeIndexOf.$call(history, line); - if (dupIndex !== -1) ArrayPrototypeSplice.$call(history, dupIndex, 1); - } - - ArrayPrototypeUnshift.$call(history, line); - - // Only store so many - if (history.length > this.historySize) ArrayPrototypePop.$call(history); - } - - this.historyIndex = -1; - - // The listener could change the history object, possibly - // to remove the last added entry if it is sensitive and should - // not be persisted in the history, like a password - const latest = this.history[0]; - - // Emit history event to notify listeners of update - this.emit("history", this.history); - - return latest; - } - - [kRefreshLine]() { - // line length - var line = this[kPrompt] + this.line; - var dispPos = this[kGetDisplayPos](line); - var lineCols = dispPos.cols; - var lineRows = dispPos.rows; - - // cursor position - var cursorPos = this.getCursorPos(); - - // First move to the bottom of the current line, based on cursor pos - var prevRows = this.prevRows || 0; - if (prevRows > 0) { - moveCursor(this.output, 0, -prevRows); - } - - // Cursor to left edge. - cursorTo(this.output, 0); - // erase data - clearScreenDown(this.output); - - // Write the prompt and the current buffer content. - this[kWriteToOutput](line); - - // Force terminal to allocate a new line - if (lineCols === 0) { - this[kWriteToOutput](" "); - } - - // Move cursor to original position. - cursorTo(this.output, cursorPos.cols); - - var diff = lineRows - cursorPos.rows; - if (diff > 0) { - moveCursor(this.output, 0, -diff); - } - - this.prevRows = cursorPos.rows; - } - - /** - * Closes the `readline.Interface` instance. - * @returns {void} - */ - close() { - if (this.closed) return; - this.pause(); - if (this.terminal) { - this[kSetRawMode](false); - } - this.closed = true; - this.emit("close"); - } - - /** - * Pauses the `input` stream. - * @returns {void | Interface} - */ - pause() { - if (this.paused) return; - this.input.pause(); - this.paused = true; - this.emit("pause"); - return this; - } - - /** - * Resumes the `input` stream if paused. - * @returns {void | Interface} - */ - resume() { - if (!this.paused) return; - this.input.resume(); - this.paused = false; - this.emit("resume"); - return this; - } - - /** - * Writes either `data` or a `key` sequence identified by - * `key` to the `output`. - * @param {string} d - * @param {{ - * ctrl?: boolean; - * meta?: boolean; - * shift?: boolean; - * name?: string; - * }} [key] - * @returns {void} - */ - write(d, key) { - if (this.paused) this.resume(); - if (this.terminal) { - this[kTtyWrite](d, key); - } else { - this[kNormalWrite](d); - } - } - - [kNormalWrite](b) { - if (b === undefined) { - return; - } - var string = this[kDecoder].write(b); - if (this[kSawReturnAt] && DateNow() - this[kSawReturnAt] <= this.crlfDelay) { - if (StringPrototypeCodePointAt.$call(string) === 10) string = StringPrototypeSlice.$call(string, 1); - this[kSawReturnAt] = 0; - } - - // Run test() on the new string chunk, not on the entire line buffer. - var newPartContainsEnding = RegExpPrototypeExec.$call(lineEnding, string); - if (newPartContainsEnding !== null) { - if (this[kLine_buffer]) { - string = this[kLine_buffer] + string; - this[kLine_buffer] = null; - lineEnding.lastIndex = 0; // Start the search from the beginning of the string. - newPartContainsEnding = RegExpPrototypeExec.$call(lineEnding, string); - } - this[kSawReturnAt] = StringPrototypeEndsWith.$call(string, "\r") ? DateNow() : 0; - - var indexes = [0, newPartContainsEnding.index, lineEnding.lastIndex]; - var nextMatch; - while ((nextMatch = RegExpPrototypeExec.$call(lineEnding, string)) !== null) { - ArrayPrototypePush.$call(indexes, nextMatch.index, lineEnding.lastIndex); - } - var lastIndex = indexes.length - 1; - // Either '' or (conceivably) the unfinished portion of the next line - this[kLine_buffer] = StringPrototypeSlice.$call(string, indexes[lastIndex]); - for (var i = 1; i < lastIndex; i += 2) { - this[kOnLine](StringPrototypeSlice.$call(string, indexes[i - 1], indexes[i])); - } - } else if (string) { - // No newlines this time, save what we have for next time - if (this[kLine_buffer]) { - this[kLine_buffer] += string; - } else { - this[kLine_buffer] = string; - } - } - } - - [kInsertString](c) { - this[kBeforeEdit](this.line, this.cursor); - const line = this.line; - const lineLength = line.length; - if (this.cursor < lineLength) { - var beg = StringPrototypeSlice.$call(line, 0, this.cursor); - var end = StringPrototypeSlice.$call(line, this.cursor, lineLength); - this.line = beg + c + end; - this.cursor += c.length; - this[kRefreshLine](); - } else { - var oldPos = this.getCursorPos(); - this.line += c; - this.cursor += c.length; - var newPos = this.getCursorPos(); - - if (oldPos.rows < newPos.rows) { - this[kRefreshLine](); - } else { - this[kWriteToOutput](c); - } - } - } - - async [kTabComplete](lastKeypressWasTab) { - this.pause(); - var string = StringPrototypeSlice.$call(this.line, 0, this.cursor); - var value; - try { - value = await this.completer(string); - } catch (err) { - this[kWriteToOutput](`Tab completion error: ${inspect(err)}`); - return; - } finally { - this.resume(); - } - this[kTabCompleter](lastKeypressWasTab, value); - } - - [kTabCompleter](lastKeypressWasTab, { 0: completions, 1: completeOn }) { - // Result and the text that was completed. - - if (!completions || completions.length === 0) { - return; - } - - // If there is a common prefix to all matches, then apply that portion. - var prefix = commonPrefix(ArrayPrototypeFilter.$call(completions, e => e !== "")); - var completeOnLength = completeOn.length; - if (StringPrototypeStartsWith.$call(prefix, completeOn) && prefix.length > completeOnLength) { - this[kInsertString](StringPrototypeSlice.$call(prefix, completeOnLength)); - return; - } else if (!StringPrototypeStartsWith.$call(completeOn, prefix)) { - this.line = - StringPrototypeSlice.$call(this.line, 0, this.cursor - completeOn.length) + - prefix + - StringPrototypeSlice.$call(this.line, this.cursor, this.line.length); - this.cursor = this.cursor - completeOn.length + prefix.length; - this[kRefreshLine](); - return; - } - - if (!lastKeypressWasTab) { - return; - } - - this[kBeforeEdit](this.line, this.cursor); - - // Apply/show completions. - var completionsWidth = ArrayPrototypeMap.$call(completions, e => getStringWidth(e)); - var width = MathMax.$apply(null, completionsWidth) + 2; // 2 space padding - var maxColumns = MathFloor(this.columns / width) || 1; - if (maxColumns === Infinity) { - maxColumns = 1; - } - var output = "\r\n"; - var lineIndex = 0; - var whitespace = 0; - for (var i = 0; i < completions.length; i++) { - var completion = completions[i]; - if (completion === "" || lineIndex === maxColumns) { - output += "\r\n"; - lineIndex = 0; - whitespace = 0; - } else { - output += StringPrototypeRepeat.$call(" ", whitespace); - } - if (completion !== "") { - output += completion; - whitespace = width - completionsWidth[i]; - lineIndex++; - } else { - output += "\r\n"; - } - } - if (lineIndex !== 0) { - output += "\r\n\r\n"; - } - this[kWriteToOutput](output); - this[kRefreshLine](); - } - - [kWordLeft]() { - const cursor = this.cursor; - if (cursor > 0) { - // Reverse the string and match a word near beginning - // to avoid quadratic time complexity - var leading = StringPrototypeSlice.$call(this.line, 0, cursor); - var reversed = ArrayPrototypeJoin.$call(ArrayPrototypeReverse.$call(ArrayFrom(leading)), ""); - var match = RegExpPrototypeExec.$call(/^\s*(?:[^\w\s]+|\w+)?/, reversed); - this[kMoveCursor](-match[0].length); - } - } - - [kWordRight]() { - const cursor = this.cursor; - const line = this.line; - if (cursor < line.length) { - var trailing = StringPrototypeSlice.$call(line, cursor); - var match = RegExpPrototypeExec.$call(/^(?:\s+|[^\w\s]+|\w+)\s*/, trailing); - this[kMoveCursor](match[0].length); - } - } - - [kDeleteLeft]() { - const cursor = this.cursor; - const line = this.line; - const lineLength = line.length; - if (cursor > 0 && lineLength > 0) { - this[kBeforeEdit](line, cursor); - // The number of UTF-16 units comprising the character to the left - var charSize = charLengthLeft(line, cursor); - this.line = - StringPrototypeSlice.$call(line, 0, cursor - charSize) + StringPrototypeSlice.$call(line, cursor, lineLength); - - this.cursor -= charSize; - this[kRefreshLine](); - } - } - - [kDeleteRight]() { - const cursor = this.cursor; - const line = this.line; - const lineLength = line.length; - if (cursor < lineLength) { - this[kBeforeEdit](line, cursor); - // The number of UTF-16 units comprising the character to the left - var charSize = charLengthAt(line, cursor); - this.line = - StringPrototypeSlice.$call(line, 0, cursor) + StringPrototypeSlice.$call(line, cursor + charSize, lineLength); - this[kRefreshLine](); - } - } - - [kDeleteWordLeft]() { - if (this.cursor > 0) { - this[kBeforeEdit](this.line, this.cursor); - // Reverse the string and match a word near beginning - // to avoid quadratic time complexity - var leading = StringPrototypeSlice.$call(this.line, 0, this.cursor); - var reversed = ArrayPrototypeJoin.$call(ArrayPrototypeReverse.$call(ArrayFrom(leading)), ""); - var match = RegExpPrototypeExec.$call(/^\s*(?:[^\w\s]+|\w+)?/, reversed); - leading = StringPrototypeSlice.$call(leading, 0, leading.length - match[0].length); - this.line = leading + StringPrototypeSlice.$call(this.line, this.cursor, this.line.length); - this.cursor = leading.length; - this[kRefreshLine](); - } - } - - [kDeleteWordRight]() { - const cursor = this.cursor; - const line = this.line; - if (cursor < line.length) { - this[kBeforeEdit](line, cursor); - var trailing = StringPrototypeSlice.$call(line, cursor); - var match = RegExpPrototypeExec.$call(/^(?:\s+|\W+|\w+)\s*/, trailing); - this.line = StringPrototypeSlice.$call(line, 0, cursor) + StringPrototypeSlice.$call(trailing, match[0].length); - this[kRefreshLine](); - } - } - - [kDeleteLineLeft]() { - this[kBeforeEdit](this.line, this.cursor); - var del = StringPrototypeSlice.$call(this.line, 0, this.cursor); - this.line = StringPrototypeSlice.$call(this.line, this.cursor); - this.cursor = 0; - this[kPushToKillRing](del); - this[kRefreshLine](); - } - - [kDeleteLineRight]() { - this[kBeforeEdit](this.line, this.cursor); - var del = StringPrototypeSlice.$call(this.line, this.cursor); - this.line = StringPrototypeSlice.$call(this.line, 0, this.cursor); - this[kPushToKillRing](del); - this[kRefreshLine](); - } - - [kPushToKillRing](del) { - if (!del || del === this[kKillRing][0]) return; - ArrayPrototypeUnshift.$call(this[kKillRing], del); - this[kKillRingCursor] = 0; - while (this[kKillRing].length > kMaxLengthOfKillRing) ArrayPrototypePop.$call(this[kKillRing]); - } - - [kYank]() { - if (this[kKillRing].length > 0) { - this[kYanking] = true; - this[kInsertString](this[kKillRing][this[kKillRingCursor]]); - } - } - - [kYankPop]() { - if (!this[kYanking]) { - return; - } - if (this[kKillRing].length > 1) { - var lastYank = this[kKillRing][this[kKillRingCursor]]; - this[kKillRingCursor]++; - if (this[kKillRingCursor] >= this[kKillRing].length) { - this[kKillRingCursor] = 0; - } - var currentYank = this[kKillRing][this[kKillRingCursor]]; - var head = StringPrototypeSlice.$call(this.line, 0, this.cursor - lastYank.length); - var tail = StringPrototypeSlice.$call(this.line, this.cursor); - this.line = head + currentYank + tail; - this.cursor = head.length + currentYank.length; - this[kRefreshLine](); - } - } - - clearLine() { - this[kMoveCursor](+Infinity); - this[kWriteToOutput]("\r\n"); - this.line = ""; - this.cursor = 0; - this.prevRows = 0; - } - - [kLine]() { - var line = this[kAddHistory](); - this[kUndoStack] = []; - this[kRedoStack] = []; - this.clearLine(); - this[kOnLine](line); - } - - [kPushToUndoStack](text, cursor) { - if (ArrayPrototypePush.$call(this[kUndoStack], { text, cursor }) > kMaxUndoRedoStackSize) { - ArrayPrototypeShift.$call(this[kUndoStack]); - } - } - - [kUndo]() { - if (this[kUndoStack].length <= 0) return; - - ArrayPrototypePush.$call(this[kRedoStack], { - text: this.line, - cursor: this.cursor, - }); - - var entry = ArrayPrototypePop.$call(this[kUndoStack]); - this.line = entry.text; - this.cursor = entry.cursor; - - this[kRefreshLine](); - } - - [kRedo]() { - if (this[kRedoStack].length <= 0) return; - - ArrayPrototypePush.$call(this[kUndoStack], { - text: this.line, - cursor: this.cursor, - }); - - var entry = ArrayPrototypePop.$call(this[kRedoStack]); - this.line = entry.text; - this.cursor = entry.cursor; - - this[kRefreshLine](); - } - - [kHistoryNext]() { - if (this.historyIndex >= 0) { - this[kBeforeEdit](this.line, this.cursor); - var search = this[kSubstringSearch] || ""; - var index = this.historyIndex - 1; - while ( - index >= 0 && - (!StringPrototypeStartsWith.$call(this.history[index], search) || this.line === this.history[index]) - ) { - index--; - } - if (index === -1) { - this.line = search; - } else { - this.line = this.history[index]; - } - this.historyIndex = index; - this.cursor = this.line.length; // Set cursor to end of line. - this[kRefreshLine](); - } - } - - [kHistoryPrev]() { - const history = this.history; - const historyLength = history.length; - if (this.historyIndex < historyLength && historyLength) { - this[kBeforeEdit](this.line, this.cursor); - var search = this[kSubstringSearch] || ""; - var index = this.historyIndex + 1; - while ( - index < historyLength && - (!StringPrototypeStartsWith.$call(history[index], search) || this.line === history[index]) - ) { - index++; - } - if (index === historyLength) { - this.line = search; - } else { - this.line = history[index]; - } - this.historyIndex = index; - this.cursor = this.line.length; // Set cursor to end of line. - this[kRefreshLine](); - } - } - - // Returns the last character's display position of the given string - [kGetDisplayPos](str) { - var offset = 0; - var col = this.columns; - var rows = 0; - str = stripVTControlCharacters(str); - for (var char of new SafeStringIterator(str)) { - if (char === "\n") { - // Rows must be incremented by 1 even if offset = 0 or col = +Infinity. - rows += MathCeil(offset / col) || 1; - offset = 0; - continue; - } - // Tabs must be aligned by an offset of the tab size. - if (char === "\t") { - offset += this.tabSize - (offset % this.tabSize); - continue; - } - var width = getStringWidth(char, false /* stripVTControlCharacters */); - if (width === 0 || width === 1) { - offset += width; - } else { - // width === 2 - if ((offset + 1) % col === 0) { - offset++; - } - offset += 2; - } - } - var cols = offset % col; - rows += (offset - cols) / col; - return { cols, rows }; - } - - /** - * Returns the real position of the cursor in relation - * to the input prompt + string. - * @returns {{ - * rows: number; - * cols: number; - * }} - */ - getCursorPos() { - var strBeforeCursor = this[kPrompt] + StringPrototypeSlice.$call(this.line, 0, this.cursor); - return this[kGetDisplayPos](strBeforeCursor); - } - - // This function moves cursor dx places to the right - // (-dx for left) and refreshes the line if it is needed. - [kMoveCursor](dx) { - if (dx === 0) { - return; - } - var oldPos = this.getCursorPos(); - this.cursor += dx; - - // Bounds check - let lineLength; - if (this.cursor < 0) { - this.cursor = 0; - } else if (this.cursor > (lineLength = this.line.length)) { - this.cursor = lineLength; - } - - var newPos = this.getCursorPos(); - - // Check if cursor stayed on the line. - if (oldPos.rows === newPos.rows) { - var diffWidth = newPos.cols - oldPos.cols; - moveCursor(this.output, diffWidth, 0); - } else { - this[kRefreshLine](); - } - } - - // Handle a write from the tty - [kTtyWrite](s, key) { - var previousKey = this[kPreviousKey]; - key = key || kEmptyObject; - this[kPreviousKey] = key; - var { name: keyName, meta: keyMeta, ctrl: keyCtrl, shift: keyShift, sequence: keySeq } = key; - - if (!keyMeta || keyName !== "y") { - // Reset yanking state unless we are doing yank pop. - this[kYanking] = false; - } - - // Activate or deactivate substring search. - if ((keyName === "up" || keyName === "down") && !keyCtrl && !keyMeta && !keyShift) { - if (this[kSubstringSearch] === null) { - this[kSubstringSearch] = StringPrototypeSlice.$call(this.line, 0, this.cursor); - } - } else if (this[kSubstringSearch] !== null) { - this[kSubstringSearch] = null; - // Reset the index in case there's no match. - if (this.history.length === this.historyIndex) { - this.historyIndex = -1; - } - } - - // Undo & Redo - if (typeof keySeq === "string") { - switch (StringPrototypeCodePointAt.$call(keySeq, 0)) { - case 0x1f: - this[kUndo](); - return; - case 0x1e: - this[kRedo](); - return; - default: - break; - } - } - - // Ignore escape key, fixes - // https://github.com/nodejs/node-v0.x-archive/issues/2876. - if (keyName === "escape") return; - - if (keyCtrl && keyShift) { - /* Control and shift pressed */ - switch (keyName) { - // TODO(BridgeAR): The transmitted escape sequence is `\b` and that is - // identical to -h. It should have a unique escape sequence. - case "backspace": - this[kDeleteLineLeft](); - break; - - case "delete": - this[kDeleteLineRight](); - break; - } - } else if (keyCtrl) { - /* Control key pressed */ - - switch (keyName) { - case "c": - if (this.listenerCount("SIGINT") > 0) { - this.emit("SIGINT"); - } else { - // This readline instance is finished - this.close(); - } - break; - - case "h": // delete left - this[kDeleteLeft](); - break; - - case "d": // delete right or EOF - if (this.cursor === 0 && this.line.length === 0) { - // This readline instance is finished - this.close(); - } else if (this.cursor < this.line.length) { - this[kDeleteRight](); - } - break; - - case "u": // Delete from current to start of line - this[kDeleteLineLeft](); - break; - - case "k": // Delete from current to end of line - this[kDeleteLineRight](); - break; - - case "a": // Go to the start of the line - this[kMoveCursor](-Infinity); - break; - - case "e": // Go to the end of the line - this[kMoveCursor](+Infinity); - break; - - case "b": // back one character - this[kMoveCursor](-charLengthLeft(this.line, this.cursor)); - break; - - case "f": // Forward one character - this[kMoveCursor](+charLengthAt(this.line, this.cursor)); - break; - - case "l": // Clear the whole screen - cursorTo(this.output, 0, 0); - clearScreenDown(this.output); - this[kRefreshLine](); - break; - - case "n": // next history item - this[kHistoryNext](); - break; - - case "p": // Previous history item - this[kHistoryPrev](); - break; - - case "y": // Yank killed string - this[kYank](); - break; - - case "z": - if (process.platform === "win32") break; - if (this.listenerCount("SIGTSTP") > 0) { - this.emit("SIGTSTP"); - } else { - process.once("SIGCONT", () => { - // Don't raise events if stream has already been abandoned. - if (!this.paused) { - // Stream must be paused and resumed after SIGCONT to catch - // SIGINT, SIGTSTP, and EOF. - this.pause(); - this.emit("SIGCONT"); - } - // Explicitly re-enable "raw mode" and move the cursor to - // the correct position. - // See https://github.com/joyent/node/issues/3295. - this[kSetRawMode](true); - this[kRefreshLine](); - }); - this[kSetRawMode](false); - process.kill(process.pid, "SIGTSTP"); - } - break; - - case "w": // Delete backwards to a word boundary - case "backspace": - this[kDeleteWordLeft](); - break; - - case "delete": // Delete forward to a word boundary - this[kDeleteWordRight](); - break; - - case "left": - this[kWordLeft](); - break; - - case "right": - this[kWordRight](); - break; - } - } else if (keyMeta) { - /* Meta key pressed */ - - switch (keyName) { - case "b": // backward word - this[kWordLeft](); - break; - - case "f": // forward word - this[kWordRight](); - break; - - case "d": // delete forward word - case "delete": - this[kDeleteWordRight](); - break; - - case "backspace": // Delete backwards to a word boundary - this[kDeleteWordLeft](); - break; - - case "y": // Doing yank pop - this[kYankPop](); - break; - } - } else { - /* No modifier keys used */ - - // \r bookkeeping is only relevant if a \n comes right after. - if (this[kSawReturnAt] && keyName !== "enter") this[kSawReturnAt] = 0; - - switch (keyName) { - case "return": // Carriage return, i.e. \r - this[kSawReturnAt] = DateNow(); - this[kLine](); - break; - - case "enter": - // When key interval > crlfDelay - if (this[kSawReturnAt] === 0 || DateNow() - this[kSawReturnAt] > this.crlfDelay) { - this[kLine](); - } - this[kSawReturnAt] = 0; - break; - - case "backspace": - this[kDeleteLeft](); - break; - - case "delete": - this[kDeleteRight](); - break; - - case "left": - // Obtain the code point to the left - this[kMoveCursor](-charLengthLeft(this.line, this.cursor)); - break; - - case "right": - this[kMoveCursor](+charLengthAt(this.line, this.cursor)); - break; - - case "home": - this[kMoveCursor](-Infinity); - break; - - case "end": - this[kMoveCursor](+Infinity); - break; - - case "up": - this[kHistoryPrev](); - break; - - case "down": - this[kHistoryNext](); - break; - - case "tab": - // If tab completion enabled, do that... - if (typeof this.completer === "function" && this.isCompletionEnabled) { - var lastKeypressWasTab = previousKey && previousKey.name === "tab"; - this[kTabComplete](lastKeypressWasTab); - break; - } - // falls through - default: - if (typeof s === "string" && s) { - // Erase state of previous searches. - lineEnding.lastIndex = 0; - let nextMatch; - // Keep track of the end of the last match. - let lastIndex = 0; - while ((nextMatch = RegExpPrototypeExec.$call(lineEnding, s)) !== null) { - this[kInsertString](StringPrototypeSlice.$call(s, lastIndex, nextMatch.index)); - ({ lastIndex } = lineEnding); - this[kLine](); - // Restore lastIndex as the call to kLine could have mutated it. - lineEnding.lastIndex = lastIndex; - } - // This ensures that the last line is written if it doesn't end in a newline. - // Note that the last line may be the first line, in which case this still works. - this[kInsertString](StringPrototypeSlice.$call(s, lastIndex)); - } - } - } - } - - /** - * Creates an `AsyncIterator` object that iterates through - * each line in the input stream as a string. - * @typedef {{ - * [Symbol.asyncIterator]: () => InterfaceAsyncIterator, - * next: () => Promise - * }} InterfaceAsyncIterator - * @returns {InterfaceAsyncIterator} - */ - [SymbolAsyncIterator]() { - if (this[kLineObjectStream] === undefined) { - this[kLineObjectStream] = EventEmitter.on(this, "line", { - close: ["close"], - highWatermark: 1024, - [kFirstEventParam]: true, - }); - } - return this[kLineObjectStream]; - } -}; - -function Interface(input, output, completer, terminal) { - if (!(this instanceof Interface)) { - return new Interface(input, output, completer, terminal); - } - - if (input?.input && typeof input.completer === "function" && input.completer.length !== 2) { - var { completer } = input; - input.completer = (v, cb) => cb(null, completer(v)); - } else if (typeof completer === "function" && completer.length !== 2) { - var realCompleter = completer; - completer = (v, cb) => cb(null, realCompleter(v)); - } - - InterfaceConstructor.$call(this, input, output, completer, terminal); - - // TODO: Test this - if (process.env.TERM === "dumb") { - this._ttyWrite = _ttyWriteDumb.bind(this); - } -} -$toClass(Interface, "Interface", _Interface); - -/** - * Displays `query` by writing it to the `output`. - * @param {string} query - * @param {{ signal?: AbortSignal; }} [options] - * @param {Function} cb - * @returns {void} - */ -Interface.prototype.question = function question(query, options, cb) { - cb = typeof options === "function" ? options : cb; - if (options === null || typeof options !== "object") { - options = kEmptyObject; - } - - var signal = options?.signal; - if (signal) { - validateAbortSignal(signal, "options.signal"); - if (signal.aborted) { - return; - } - - var onAbort = () => { - this[kQuestionCancel](); - }; - signal.addEventListener("abort", onAbort, { once: true }); - var cleanup = () => { - signal.removeEventListener("abort", onAbort); - }; - var originalCb = cb; - cb = - typeof cb === "function" - ? answer => { - cleanup(); - return originalCb(answer); - } - : cleanup; - } - - if (typeof cb === "function") { - this[kQuestion](query, cb); - } -}; - -Interface.prototype.question[promisify.custom] = { - question(query, options) { - if (options === null || typeof options !== "object") { - options = kEmptyObject; - } - - var signal = options?.signal; - - if (signal && signal.aborted) { - return PromiseReject($makeAbortError(undefined, { cause: signal.reason })); - } - - return new Promise((resolve, reject) => { - var cb = resolve; - if (signal) { - var onAbort = () => { - reject($makeAbortError(undefined, { cause: signal.reason })); - }; - signal.addEventListener("abort", onAbort, { once: true }); - cb = answer => { - signal.removeEventListener("abort", onAbort); - resolve(answer); - }; - } - this.question(query, options, cb); - }); - }, -}.question; - -/** - * Creates a new `readline.Interface` instance. - * @param {Readable | { - * input: Readable; - * output: Writable; - * completer?: Function; - * terminal?: boolean; - * history?: string[]; - * historySize?: number; - * removeHistoryDuplicates?: boolean; - * prompt?: string; - * crlfDelay?: number; - * escapeCodeTimeout?: number; - * tabSize?: number; - * signal?: AbortSignal; - * }} input - * @param {Writable} [output] - * @param {Function} [completer] - * @param {boolean} [terminal] - * @returns {Interface} - */ -function createInterface(input, output, completer, terminal) { - return new Interface(input, output, completer, terminal); -} - -ObjectDefineProperties(Interface.prototype, { - // Redirect internal prototype methods to the underscore notation for backward - // compatibility. - [kSetRawMode]: { - get() { - return this._setRawMode; - }, - }, - [kOnLine]: { - get() { - return this._onLine; - }, - }, - [kWriteToOutput]: { - get() { - return this._writeToOutput; - }, - }, - [kAddHistory]: { - get() { - return this._addHistory; - }, - }, - [kRefreshLine]: { - get() { - return this._refreshLine; - }, - }, - [kNormalWrite]: { - get() { - return this._normalWrite; - }, - }, - [kInsertString]: { - get() { - return this._insertString; - }, - }, - [kTabComplete]: { - get() { - return this._tabComplete; - }, - }, - [kWordLeft]: { - get() { - return this._wordLeft; - }, - }, - [kWordRight]: { - get() { - return this._wordRight; - }, - }, - [kDeleteLeft]: { - get() { - return this._deleteLeft; - }, - }, - [kDeleteRight]: { - get() { - return this._deleteRight; - }, - }, - [kDeleteWordLeft]: { - get() { - return this._deleteWordLeft; - }, - }, - [kDeleteWordRight]: { - get() { - return this._deleteWordRight; - }, - }, - [kDeleteLineLeft]: { - get() { - return this._deleteLineLeft; - }, - }, - [kDeleteLineRight]: { - get() { - return this._deleteLineRight; - }, - }, - [kLine]: { - get() { - return this._line; - }, - }, - [kHistoryNext]: { - get() { - return this._historyNext; - }, - }, - [kHistoryPrev]: { - get() { - return this._historyPrev; - }, - }, - [kGetDisplayPos]: { - get() { - return this._getDisplayPos; - }, - }, - [kMoveCursor]: { - get() { - return this._moveCursor; - }, - }, - [kTtyWrite]: { - get() { - return this._ttyWrite; - }, - }, - - // Defining proxies for the internal instance properties for backward - // compatibility. - _decoder: { - get() { - return this[kDecoder]; - }, - set(value) { - this[kDecoder] = value; - }, - }, - _line_buffer: { - get() { - return this[kLine_buffer]; - }, - set(value) { - this[kLine_buffer] = value; - }, - }, - _oldPrompt: { - get() { - return this[kOldPrompt]; - }, - set(value) { - this[kOldPrompt] = value; - }, - }, - _previousKey: { - get() { - return this[kPreviousKey]; - }, - set(value) { - this[kPreviousKey] = value; - }, - }, - _prompt: { - get() { - return this[kPrompt]; - }, - set(value) { - this[kPrompt] = value; - }, - }, - _questionCallback: { - get() { - return this[kQuestionCallback]; - }, - set(value) { - this[kQuestionCallback] = value; - }, - }, - _sawKeyPress: { - get() { - return this[kSawKeyPress]; - }, - set(value) { - this[kSawKeyPress] = value; - }, - }, - _sawReturnAt: { - get() { - return this[kSawReturnAt]; - }, - set(value) { - this[kSawReturnAt] = value; - }, - }, -}); - -// Make internal methods public for backward compatibility. -Interface.prototype._setRawMode = _Interface.prototype[kSetRawMode]; -Interface.prototype._onLine = _Interface.prototype[kOnLine]; -Interface.prototype._writeToOutput = _Interface.prototype[kWriteToOutput]; -Interface.prototype._addHistory = _Interface.prototype[kAddHistory]; -Interface.prototype._refreshLine = _Interface.prototype[kRefreshLine]; -Interface.prototype._normalWrite = _Interface.prototype[kNormalWrite]; -Interface.prototype._insertString = _Interface.prototype[kInsertString]; -Interface.prototype._tabComplete = function (lastKeypressWasTab) { - // Overriding parent method because `this.completer` in the legacy - // implementation takes a callback instead of being an async function. - this.pause(); - var string = StringPrototypeSlice.$call(this.line, 0, this.cursor); - this.completer(string, (err, value) => { - this.resume(); - - if (err) { - this._writeToOutput(`Tab completion error: ${inspect(err)}`); - return; - } - - this[kTabCompleter](lastKeypressWasTab, value); - }); -}; -Interface.prototype._wordLeft = _Interface.prototype[kWordLeft]; -Interface.prototype._wordRight = _Interface.prototype[kWordRight]; -Interface.prototype._deleteLeft = _Interface.prototype[kDeleteLeft]; -Interface.prototype._deleteRight = _Interface.prototype[kDeleteRight]; -Interface.prototype._deleteWordLeft = _Interface.prototype[kDeleteWordLeft]; -Interface.prototype._deleteWordRight = _Interface.prototype[kDeleteWordRight]; -Interface.prototype._deleteLineLeft = _Interface.prototype[kDeleteLineLeft]; -Interface.prototype._deleteLineRight = _Interface.prototype[kDeleteLineRight]; -Interface.prototype._line = _Interface.prototype[kLine]; -Interface.prototype._historyNext = _Interface.prototype[kHistoryNext]; -Interface.prototype._historyPrev = _Interface.prototype[kHistoryPrev]; -Interface.prototype._getDisplayPos = _Interface.prototype[kGetDisplayPos]; -Interface.prototype._getCursorPos = _Interface.prototype.getCursorPos; -Interface.prototype._moveCursor = _Interface.prototype[kMoveCursor]; -Interface.prototype._ttyWrite = _Interface.prototype[kTtyWrite]; -Interface.prototype[Symbol.dispose] = _Interface.prototype[Symbol.dispose]; - -function _ttyWriteDumb(s, key) { - key = key || kEmptyObject; - - if (key.name === "escape") return; - - if (this[kSawReturnAt] && key.name !== "enter") this[kSawReturnAt] = 0; - - if (key.ctrl) { - if (key.name === "c") { - if (this.listenerCount("SIGINT") > 0) { - this.emit("SIGINT"); - } else { - // This readline instance is finished - this.close(); - } - - return; - } else if (key.name === "d") { - this.close(); - return; - } - } - - switch (key.name) { - case "return": // Carriage return, i.e. \r - this[kSawReturnAt] = DateNow(); - this._line(); - break; - - case "enter": - // When key interval > crlfDelay - if (this[kSawReturnAt] === 0 || DateNow() - this[kSawReturnAt] > this.crlfDelay) { - this._line(); - } - this[kSawReturnAt] = 0; - break; - - default: - if (typeof s === "string" && s) { - this.line += s; - this.cursor += s.length; - this._writeToOutput(s); - } - } -} - -class Readline { - #autoCommit = false; - #stream; - #todo = []; - - constructor(stream, options = undefined) { - isWritable ??= require("node:stream").isWritable; - if (!isWritable(stream)) throw $ERR_INVALID_ARG_TYPE("stream", "Writable", stream); - this.#stream = stream; - if (options?.autoCommit != null) { - validateBoolean(options.autoCommit, "options.autoCommit"); - this.#autoCommit = options.autoCommit; - } - } - - /** - * Moves the cursor to the x and y coordinate on the given stream. - * @param {integer} x - * @param {integer} [y] - * @returns {Readline} this - */ - cursorTo(x, y = undefined) { - validateInteger(x, "x"); - if (y != null) validateInteger(y, "y"); - - var data = y == null ? CSI`${x + 1}G` : CSI`${y + 1};${x + 1}H`; - if (this.#autoCommit) process.nextTick(() => this.#stream.write(data)); - else ArrayPrototypePush.$call(this.#todo, data); - - return this; - } - - /** - * Moves the cursor relative to its current location. - * @param {integer} dx - * @param {integer} dy - * @returns {Readline} this - */ - moveCursor(dx, dy) { - if (dx || dy) { - validateInteger(dx, "dx"); - validateInteger(dy, "dy"); - - var 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`; - } - if (this.#autoCommit) process.nextTick(() => this.#stream.write(data)); - else ArrayPrototypePush.$call(this.#todo, data); - } - return this; - } - - /** - * Clears the current line the cursor is on. - * @param {-1|0|1} dir Direction to clear: - * -1 for left of the cursor - * +1 for right of the cursor - * 0 for the entire line - * @returns {Readline} this - */ - clearLine(dir) { - validateInteger(dir, "dir", -1, 1); - - var data = dir < 0 ? kClearToLineBeginning : dir > 0 ? kClearToLineEnd : kClearLine; - if (this.#autoCommit) process.nextTick(() => this.#stream.write(data)); - else ArrayPrototypePush.$call(this.#todo, data); - return this; - } - - /** - * Clears the screen from the current position of the cursor down. - * @returns {Readline} this - */ - clearScreenDown() { - if (this.#autoCommit) { - process.nextTick(() => this.#stream.write(kClearScreenDown)); - } else { - ArrayPrototypePush.$call(this.#todo, kClearScreenDown); - } - return this; - } - - /** - * Sends all the pending actions to the associated `stream` and clears the - * internal list of pending actions. - * @returns {Promise} Resolves when all pending actions have been - * flushed to the associated `stream`. - */ - commit() { - const { resolve, reject, promise } = $newPromiseCapability(Promise); - - try { - const data = ArrayPrototypeJoin.$call(this.#todo, ""); - this.#stream.write(data, resolve); - this.#todo = []; - } catch (err) { - reject(err); - } finally { - return promise; - } - } - - /** - * Clears the internal list of pending actions without sending it to the - * associated `stream`. - * @returns {Readline} this - */ - rollback() { - this.#todo = []; - return this; - } -} - -var PromisesInterface = class Interface extends _Interface { - // eslint-disable-next-line no-useless-constructor - constructor(input, output, completer, terminal) { - super(input, output, completer, terminal); - } - question(query, options = kEmptyObject) { - var signal = options?.signal; - if (signal) { - validateAbortSignal(signal, "options.signal"); - if (signal.aborted) { - return PromiseReject($makeAbortError(undefined, { cause: signal.reason })); - } - } - const { promise, resolve, reject } = $newPromiseCapability(Promise); - var cb = resolve; - if (options?.signal) { - var onAbort = () => { - this[kQuestionCancel](); - reject($makeAbortError(undefined, { cause: signal.reason })); - }; - signal.addEventListener("abort", onAbort, { once: true }); - cb = answer => { - signal.removeEventListener("abort", onAbort); - resolve(answer); - }; - } - this[kQuestion](query, cb); - return promise; - } -}; - -// ---------------------------------------------------------------------------- -// Exports -// ---------------------------------------------------------------------------- -export default { - Interface, - clearLine, - clearScreenDown, - createInterface, - cursorTo, - emitKeypressEvents, - moveCursor, - promises: { - Readline, - Interface: PromisesInterface, - createInterface(input, output, completer, terminal) { - return new PromisesInterface(input, output, completer, terminal); - }, - }, - - [SymbolFor("__BUN_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED__")]: { - CSI, - utils: { - getStringWidth, - stripVTControlCharacters, - }, - }, -}; diff --git a/src/js/node/repl.js b/src/js/node/repl.js new file mode 100644 index 000000000000..1b13a9ae068b --- /dev/null +++ b/src/js/node/repl.js @@ -0,0 +1,1544 @@ +// Ported from Node.js v26.3.0 lib/repl.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: {} }; +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +/* A REPL library that you can include in your own code to get a runtime + * interface to your program. + * + * const repl = require("node:repl"); + * // start repl on stdin + * repl.start("prompt> "); + * + * // listen for unix socket connections and start repl on them + * net.createServer(function(socket) { + * repl.start("node via Unix socket> ", socket); + * }).listen("/tmp/node-repl-sock"); + * + * // listen for TCP socket connections and start repl on them + * net.createServer(function(socket) { + * repl.start("node via TCP socket> ", socket); + * }).listen(5001); + * + * // expose foo to repl context + * repl.start("node > ").context.foo = "stdin is fun"; + */ + +const { + ArrayPrototypeAt, + ArrayPrototypeFilter, + ArrayPrototypeForEach, + ArrayPrototypeJoin, + ArrayPrototypeMap, + ArrayPrototypePop, + ArrayPrototypePush, + ArrayPrototypeShift, + ArrayPrototypeSlice, + ArrayPrototypeSort, + Boolean, + Error: MainContextError, + FunctionPrototypeBind, + FunctionPrototypeCall, + JSONStringify, + MathMaxApply, + NumberIsNaN, + NumberParseFloat, + ObjectAssign, + ObjectDefineProperty, + ObjectGetOwnPropertyDescriptor, + ObjectGetOwnPropertyNames, + ObjectKeys, + Promise, + ReflectApply, + RegExp, + RegExpPrototypeExec, + SafePromiseRace, + SafeSet, + StringPrototypeCharAt, + StringPrototypeEndsWith, + StringPrototypeIncludes, + StringPrototypeRepeat, + StringPrototypeSlice, + StringPrototypeStartsWith, + StringPrototypeTrim, + Symbol, + SyntaxError, + globalThis, +} = primordials; + +// These five are cheap enough to live outside loadImpl, so a library that +// destructures `{start, Recoverable, REPL_MODE_*}` at import time pays +// nothing until start() is actually called. +const { REPL_MODE_SLOPPY, REPL_MODE_STRICT } = require("internal/repl/mode"); + +class Recoverable extends SyntaxError { + constructor(err) { + super(); + this.err = err; + } +} + +function start() { + return loadImpl().start.$apply(this, arguments); +} + +function isValidSyntax() { + return loadImpl().isValidSyntax.$apply(this, arguments); +} + +// REPLServer / writer / repl / builtinModules stay accessors: REPLServer extends +// readline.Interface so reading it means loading anyway, and writer carries a +// mutable `.options` that references util.inspect.defaultOptions. +let _loaded; +function loadImpl() { + if (_loaded) return _loaded; + // Populated at the bottom; internal references (inside REPLServer methods) + // run only after loadImpl completes. + _loaded = { REPL_MODE_SLOPPY, REPL_MODE_STRICT, Recoverable }; + +const { makeRequireFunction, addBuiltinLibsToObject } = require("internal/repl/node-shims"); +// Lazy: acorn's ~122 KB source parses on first property access, not on +// require('node:repl'); don't destructure at module scope. +const acorn = require("internal/repl/acorn"); +const acornWalk = require("internal/repl/acorn-walk"); +const { + decorateErrorStack, + isError, + deprecate, + SideEffectFreeRegExpPrototypeSymbolReplace, + SideEffectFreeRegExpPrototypeSymbolSplit, +} = require("internal/repl/node-shims"); +const { inspect } = require("internal/repl/node-inspect"); +const vm = require("node:vm"); + +const { runInThisContext, runInContext } = vm.Script.prototype; + +const path = require("node:path"); +const fs = require("node:fs"); +const { Interface } = require("node:readline"); +const { commonPrefix } = require("internal/readline/utils"); +const { Console } = require("node:console"); +const { shouldColorize } = require("internal/repl/node-shims"); +const CJSModule = require("internal/repl/node-shims").Module; +const { AsyncLocalStorage } = require("node:async_hooks"); +// `var`, not `let`: the shim's debuglog calls the callback synchronously, so +// the assignment would hit `let`'s TDZ now that this runs inside a function. +var debug = require("internal/repl/node-shims").debuglog("repl", fn => { + debug = fn; +}); +const { + codes: { + ERR_CANNOT_WATCH_SIGINT, + ERR_INVALID_ARG_VALUE, + ERR_INVALID_REPL_EVAL_CONFIG, + ERR_INVALID_REPL_INPUT, + ERR_INVALID_STATE, + ERR_MISSING_ARGS, + ERR_SCRIPT_EXECUTION_INTERRUPTED, + }, + isErrorStackTraceLimitWritable, +} = require("internal/repl/node-errors"); +const { sendInspectorCommand } = require("internal/repl/node-shims"); +const { getOptionValue } = require("internal/repl/node-shims"); +const { validateFunction, validateObject } = require("internal/validators"); +const experimentalREPLAwait = getOptionValue("--experimental-repl-await"); +const pendingDeprecation = getOptionValue("--pending-deprecation"); +const { + isRecoverableError, + kStandaloneREPL, + setupPreview, + setupReverseSearch, + isObjectLiteral, + isValidSyntax, + kContextId, + getREPLResourceName, + globalBuiltins, + getReplBuiltinLibs, + setReplBuiltinLibs, + fixReplRequire, +} = require("internal/repl/utils"); +// internal/repl/completion (839 lines) is only needed on TAB; load on first use. +let _complete; +const { startSigintWatchdog, stopSigintWatchdog } = require("internal/repl/node-shims"); + +const { makeContextifyScript } = require("internal/repl/node-shims"); +const { kMultilinePrompt, kAddNewLineOnTTY, kLastCommandErrored } = require("internal/readline/interface"); + +// Lazy-loaded. +let processTopLevelAwait; + +const parentModule = __node_module__; + +// AsyncLocalStorage to track which REPL instance owns the current async context +// This replaces the domain-based tracking for error handling +const replContext = new AsyncLocalStorage(); +let exceptionCaptureInstalled = false; + +function replExceptionCaptureCallback(err) { + const store = replContext.getStore(); + if (store?.replServer) { + const result = store.replServer._handleError(err); + return result !== "unhandled"; // We handled it + } + // No active REPL context - let other handlers try +} + +// One-shot install per process. Node's `addUncaughtExceptionCaptureCallback` +// keeps a separate aux list that never counts against +// `hasUncaughtExceptionCaptureCallback()`; Bun lacks that native API, so the +// shim occupies the exclusive slot for the process lifetime once the first +// REPL starts (uninstalling on 'exit' would drop async errors that fire after +// input close — see test-repl-uncaught-exception-after-input-ended). The +// shim's fallthrough re-emits `uncaughtException` with the origin arg so user +// listeners still see it. +function setupExceptionCapture() { + if (exceptionCaptureInstalled) return; + exceptionCaptureInstalled = true; + require("internal/repl/node-shims").addUncaughtExceptionCaptureCallback(replExceptionCaptureCallback); +} + +const kBufferedCommandSymbol = Symbol("bufferedCommand"); +const kLoadingSymbol = Symbol("loading"); + +function processNewListener(event, listener) { + if (event === "uncaughtException") { + const store = replContext.getStore(); + if (store?.replServer) { + // Throw an error so that the event will not be added and the + // current REPL handles it. That way the user is notified about + // the error and the current code evaluation is stopped, just as + // any other code that contains an error. + throw new ERR_INVALID_REPL_INPUT("Listeners for `uncaughtException` cannot be used in the REPL"); + } + } +} + +let processNewListenerUseCount = 0; +function addProcessNewListener() { + if (processNewListenerUseCount++ === 0) { + // Add this listener only once and use a WeakSet that contains the REPLs + // domains. Otherwise we'd have to add a single listener to each REPL + // instance and that could trigger the `MaxListenersExceededWarning`. + process.prependListener("newListener", processNewListener); + } +} + +function removeProcessNewListener() { + if (--processNewListenerUseCount === 0) { + process.removeListener("newListener", processNewListener); + } +} + +fixReplRequire(__node_module__); + +// This is the default "writer" value, if none is passed in the REPL options, +// and it can be overridden by custom print functions, such as `probe` or +// `eyes.js`. +const writer = obj => inspect(obj, writer.options); +writer.options = { ...inspect.defaultOptions, showProxy: true }; + +// Converts static import statement to dynamic import statement +const toDynamicImport = codeLine => { + let dynamicImportStatement = ""; + const ast = acorn.parse(codeLine, { __proto__: null, sourceType: "module", ecmaVersion: "latest" }); + acornWalk.ancestor(ast, { + ImportDeclaration(node) { + const awaitDynamicImport = `await import(${JSONStringify(node.source.value)});`; + if (node.specifiers.length === 0) { + dynamicImportStatement += awaitDynamicImport; + } else if (node.specifiers.length === 1 && node.specifiers[0].type === "ImportNamespaceSpecifier") { + dynamicImportStatement += `const ${node.specifiers[0].local.name} = ${awaitDynamicImport}`; + } else { + const importNames = ArrayPrototypeJoin( + ArrayPrototypeMap(node.specifiers, ({ local, imported }) => + local.name === imported?.name ? local.name : `${imported?.name ?? "default"}: ${local.name}`, + ), + ", ", + ); + dynamicImportStatement += `const { ${importNames} } = ${awaitDynamicImport}`; + } + }, + }); + return dynamicImportStatement; +}; + +class REPLServer extends Interface { + constructor(prompt, stream, eval_, useGlobal, ignoreUndefined, replMode) { + let options; + if (prompt !== null && typeof prompt === "object") { + // An options object was given. + options = { ...prompt }; + stream = options.stream || options.socket; + // Destructuring keeps the "eval" property name out of member-access + // position: JSC's assertion-enabled builtin parser rejects `x.eval` / + // `x["eval"]` inside builtin sources, and minify-syntax would fold a + // bracket access back into dot form. + ({ eval: eval_ } = options); + useGlobal = options.useGlobal; + ignoreUndefined = options.ignoreUndefined; + prompt = options.prompt; + replMode = options.replMode; + } else { + options = {}; + } + + if (!options.input && !options.output) { + // Legacy API, passing a 'stream'/'socket' option. + // Use stdin and stdout as the default streams if none were given. + stream ||= process; + + // We're given a duplex readable/writable Stream, like a `net.Socket` + // or a custom object with 2 streams, or the `process` object. + options.input = stream.stdin || stream; + options.output = stream.stdout || stream; + } + + if (options.terminal === undefined) { + options.terminal = options.output.isTTY; + } + options.terminal = !!options.terminal; + + if (options.terminal && options.useColors === undefined) { + // If possible, check if stdout supports colors or not. + options.useColors = shouldColorize(options.output); + } + + const preview = options.terminal && (options.preview !== undefined ? !!options.preview : !eval_); + + super({ + input: options.input, + output: options.output, + completer: options.completer || completer, + terminal: options.terminal, + historySize: options.historySize, + prompt, + }); + + ObjectDefineProperty(this, "inputStream", { + __proto__: null, + get: pendingDeprecation + ? deprecate( + () => this.input, + "repl.inputStream and repl.outputStream are deprecated. " + "Use repl.input and repl.output instead", + "DEP0141", + ) + : () => this.input, + set: pendingDeprecation + ? deprecate( + val => (this.input = val), + "repl.inputStream and repl.outputStream are deprecated. " + "Use repl.input and repl.output instead", + "DEP0141", + ) + : val => (this.input = val), + enumerable: false, + configurable: true, + }); + ObjectDefineProperty(this, "outputStream", { + __proto__: null, + get: pendingDeprecation + ? deprecate( + () => this.output, + "repl.inputStream and repl.outputStream are deprecated. " + "Use repl.input and repl.output instead", + "DEP0141", + ) + : () => this.output, + set: pendingDeprecation + ? deprecate( + val => (this.output = val), + "repl.inputStream and repl.outputStream are deprecated. " + "Use repl.input and repl.output instead", + "DEP0141", + ) + : val => (this.output = val), + enumerable: false, + configurable: true, + }); + + this.allowBlockingCompletions = !!options.allowBlockingCompletions; + this.useColors = !!options.useColors; + this._isStandalone = !!options[kStandaloneREPL]; + + if (options.domain !== undefined) { + throw new ERR_INVALID_ARG_VALUE("options.domain", options.domain, "is no longer supported"); + } + + this.useGlobal = !!useGlobal; + this.ignoreUndefined = !!ignoreUndefined; + this.replMode = replMode || _loaded.REPL_MODE_SLOPPY; + this.underscoreAssigned = false; + this.last = undefined; + this.underscoreErrAssigned = false; + this.lastError = undefined; + this.breakEvalOnSigint = !!options.breakEvalOnSigint; + this.editorMode = false; + this._userErrorHandler = options.handleError; + // Context id for use with the inspector protocol. + this[kContextId] = undefined; + this[kLastCommandErrored] = false; + + if (this.breakEvalOnSigint && eval_) { + // Allowing this would not reflect user expectations. + // breakEvalOnSigint affects only the behavior of the default eval(). + throw new ERR_INVALID_REPL_EVAL_CONFIG(); + } + + if (options[kStandaloneREPL]) { + // It is possible to introspect the running REPL accessing this variable + // from inside the REPL. This is useful for anyone working on the REPL. + _loaded.repl = this; + } else { + addProcessNewListener(); + this.once("exit", removeProcessNewListener); + } + + // Set up exception capture for async error handling + setupExceptionCapture(); + + const savedRegExMatches = ["", "", "", "", "", "", "", "", "", ""]; + const sep = "\u0000\u0000\u0000"; + const regExMatcher = new RegExp( + `^${sep}(.*)${sep}(.*)${sep}(.*)${sep}(.*)` + `${sep}(.*)${sep}(.*)${sep}(.*)${sep}(.*)` + `${sep}(.*)$`, + ); + + eval_ ||= defaultEval; + + const self = this; + + // Pause taking in new input, and store the keys in a buffer. + const pausedBuffer = []; + let paused = false; + function pause() { + paused = true; + } + + function unpause() { + if (!paused) return; + paused = false; + let entry; + const tmpCompletionEnabled = self.isCompletionEnabled; + while ((entry = ArrayPrototypeShift(pausedBuffer)) !== undefined) { + const { 0: type, 1: payload, 2: isCompletionEnabled } = entry; + switch (type) { + case "key": { + const { 0: d, 1: key } = payload; + self.isCompletionEnabled = isCompletionEnabled; + self._ttyWrite(d, key); + break; + } + case "close": + self.emit("exit"); + break; + } + if (paused) { + break; + } + } + self.isCompletionEnabled = tmpCompletionEnabled; + } + + function defaultEval(code, context, file, cb) { + let result, script, wrappedErr; + let err = null; + let wrappedCmd = false; + let awaitPromise = false; + const input = code; + + if (isObjectLiteral(code) && isValidSyntax(code)) { + // Add parentheses to make sure `code` is parsed as an expression + code = `(${StringPrototypeTrim(code)})\n`; + wrappedCmd = true; + } + + const hostDefinedOptionId = Symbol(`eval:${file}`); + let parentURL; + try { + const { pathToFileURL } = require("node:url"); + // Adding `/repl` prevents dynamic imports from loading relative + // to the parent of `process.cwd()`. + parentURL = pathToFileURL(path.join(process.cwd(), "repl")).href; + } catch { + // Continue regardless of error. + } + async function importModuleDynamically(specifier, _, importAttributes, phase) { + const cascadedLoader = require("internal/repl/node-shims").getOrInitializeCascadedLoader(); + return cascadedLoader.import( + specifier, + parentURL, + importAttributes, + phase === "evaluation" ? cascadedLoader.kEvaluationPhase : cascadedLoader.kSourcePhase, + ); + } + // `experimentalREPLAwait` is set to true by default. + // Shall be false in case `--no-experimental-repl-await` flag is used. + if (experimentalREPLAwait && StringPrototypeIncludes(code, "await")) { + if (processTopLevelAwait === undefined) { + ({ processTopLevelAwait } = require("internal/repl/await")); + } + + try { + const potentialWrappedCode = processTopLevelAwait(code); + if (potentialWrappedCode !== null) { + code = potentialWrappedCode; + wrappedCmd = true; + awaitPromise = true; + } + } catch (e) { + let recoverableError = false; + if (e.name === "SyntaxError") { + // Remove all "await"s and attempt running the script + // in order to detect if error is truly non recoverable + const fallbackCode = SideEffectFreeRegExpPrototypeSymbolReplace(/\bawait\b/g, code, ""); + try { + makeContextifyScript( + fallbackCode, // code + file, // filename, + 0, // lineOffset + 0, // columnOffset, + undefined, // cachedData + false, // produceCachedData + undefined, // parsingContext + hostDefinedOptionId, // hostDefinedOptionId + importModuleDynamically, // importModuleDynamically + ); + } catch (fallbackError) { + if (isRecoverableError(fallbackError, fallbackCode)) { + recoverableError = true; + err = new Recoverable(e); + } + } + } + if (!recoverableError) { + decorateErrorStack(e); + err = e; + } + } + } + + // First, create the Script object to check the syntax + if (code === "\n") return cb(null); + + if (err === null) { + while (true) { + try { + if ( + self.replMode === _loaded.REPL_MODE_STRICT && + RegExpPrototypeExec(/^\s*$/, code) === null + ) { + // "void 0" keeps the repl from returning "use strict" as the result + // value for statements and declarations that don't return a value. + code = `'use strict'; void 0;\n${code}`; + } + script = makeContextifyScript( + code, // code + file, // filename, + 0, // lineOffset + 0, // columnOffset, + undefined, // cachedData + false, // produceCachedData + undefined, // parsingContext + hostDefinedOptionId, // hostDefinedOptionId + importModuleDynamically, // importModuleDynamically + ); + } catch (e) { + debug("parse error %j", code, e); + if (wrappedCmd) { + // Unwrap and try again + wrappedCmd = false; + awaitPromise = false; + code = input; + wrappedErr = e; + continue; + } + // Preserve original error for wrapped command + const error = wrappedErr || e; + if (isRecoverableError(error, code)) err = new Recoverable(error); + else err = error; + } + break; + } + } + + // This will set the values from `savedRegExMatches` to corresponding + // predefined RegExp properties `RegExp.$1`, `RegExp.$2` ... `RegExp.$9` + RegExpPrototypeExec(regExMatcher, ArrayPrototypeJoin(savedRegExMatches, sep)); + + let finished = false; + function finishExecution(err, result) { + if (finished) return; + finished = true; + + // After executing the current expression, store the values of RegExp + // predefined properties back in `savedRegExMatches` + for (let idx = 1; idx < savedRegExMatches.length; idx += 1) { + savedRegExMatches[idx] = RegExp[`$${idx}`]; + } + + cb(err, result); + } + + if (!err) { + // Unset raw mode during evaluation so that Ctrl+C raises a signal. + let previouslyInRawMode; + if (self.breakEvalOnSigint) { + // Start the SIGINT watchdog before entering raw mode so that a very + // quick Ctrl+C doesn't lead to aborting the process completely. + if (!startSigintWatchdog()) throw new ERR_CANNOT_WATCH_SIGINT(); + previouslyInRawMode = self._setRawMode(false); + } + + try { + try { + const scriptOptions = { + displayErrors: false, + breakOnSigint: self.breakEvalOnSigint, + }; + + if (self.useGlobal) { + result = FunctionPrototypeCall(runInThisContext, script, scriptOptions); + } else { + result = FunctionPrototypeCall(runInContext, script, context, scriptOptions); + } + } finally { + if (self.breakEvalOnSigint) { + // Reset terminal mode to its previous value. + self._setRawMode(previouslyInRawMode); + + // Returns true if there were pending SIGINTs *after* the script + // has terminated without being interrupted itself. + if (stopSigintWatchdog()) { + self.emit("SIGINT"); + } + } + } + } catch (e) { + err = e; + // If there's an active domain with error listeners, let it handle the error + if (process.domain?.listenerCount("error") > 0) { + debug("domain handling error"); + process.domain.emit("error", err); + return; + } + // Handle non-recoverable errors directly + debug("not recoverable, handle error"); + self._handleError(err); + return; + } + + if (awaitPromise && !err) { + let sigintListener; + pause(); + let promise = result; + if (self.breakEvalOnSigint) { + const interrupt = new Promise((resolve, reject) => { + sigintListener = () => { + const tmp = MainContextError.stackTraceLimit; + if (isErrorStackTraceLimitWritable()) MainContextError.stackTraceLimit = 0; + const err = new ERR_SCRIPT_EXECUTION_INTERRUPTED(); + if (isErrorStackTraceLimitWritable()) MainContextError.stackTraceLimit = tmp; + reject(err); + }; + prioritizedSigintQueue.add(sigintListener); + }); + promise = SafePromiseRace([promise, interrupt]); + } + + (async () => { + try { + const result = (await promise)?.value; + finishExecution(null, result); + } catch (err) { + // If there's an active domain with error listeners, let it handle the error + if (process.domain?.listenerCount("error") > 0) { + debug("domain handling async error"); + process.domain.emit("error", err); + } else { + // Handle non-recoverable async errors directly + debug("not recoverable, handle error"); + self._handleError(err); + } + } finally { + // Remove prioritized SIGINT listener if it was not called. + prioritizedSigintQueue.delete(sigintListener); + unpause(); + } + })(); + } + } + + if (!awaitPromise || err) { + finishExecution(err, result); + } + } + + // Wrap eval to run within the REPL's async context for error tracking. + // The function names are needed for stack trace filtering - they must not + // be anonymous, but we can't use 'eval' as a name since it's reserved. + const originalEval = eval_; + // ObjectDefineProperty instead of a plain assignment: JSC's + // assertion-enabled builtin parser rejects the "eval" property name in + // member-access position (`self.eval` / `self["eval"]`), and + // minify-syntax folds bracket accesses into dot form. + ObjectDefineProperty(self, "eval", { + __proto__: null, + configurable: true, + enumerable: true, + writable: true, + // eslint-disable-next-line func-name-matching + value: function REPLEval(code, context, file, cb) { + replContext.run({ replServer: self }, function REPLEvalInContext() { + originalEval(code, context, file, cb); + }); + }, + }); + + self.clearBufferedCommand(); + + function completer(text, cb) { + _complete ??= require("internal/repl/completion").complete; + FunctionPrototypeCall(_complete, self, text, self.editorMode ? self.completeOnEditorMode(cb) : cb); + } + + self.resetContext(); + + this.commands = { __proto__: null }; + defineDefaultCommands(this); + + // Figure out which "writer" function to use + self.writer = options.writer || _loaded.writer; + + if (self.writer === writer) { + // Conditionally turn on ANSI coloring. + writer.options.colors = self.useColors; + + if (options[kStandaloneREPL]) { + ObjectDefineProperty(inspect, "replDefaults", { + __proto__: null, + get() { + return writer.options; + }, + set(options) { + validateObject(options, "options"); + return ObjectAssign(writer.options, options); + }, + enumerable: true, + configurable: true, + }); + } + } + + function _parseREPLKeyword(keyword, rest) { + const cmd = this.commands[keyword]; + if (cmd) { + FunctionPrototypeCall(cmd.action, this, rest); + return true; + } + return false; + } + + self.on("close", function emitExit() { + if (paused) { + ArrayPrototypePush(pausedBuffer, ["close"]); + return; + } + self.emit("exit"); + }); + + let sawSIGINT = false; + let sawCtrlD = false; + const prioritizedSigintQueue = new SafeSet(); + self.on("SIGINT", function onSigInt() { + if (prioritizedSigintQueue.size > 0) { + for (const task of prioritizedSigintQueue) { + task(); + } + return; + } + + const empty = self.line.length === 0; + self.clearLine(); + _turnOffEditorMode(self); + + const cmd = self[kBufferedCommandSymbol]; + if (!(cmd && cmd.length > 0) && empty) { + if (sawSIGINT) { + self.close(); + sawSIGINT = false; + return; + } + self.output.write("(To exit, press Ctrl+C again or Ctrl+D or type .exit)\n"); + sawSIGINT = true; + } else { + sawSIGINT = false; + } + + self.clearBufferedCommand(); + self.lines.level = []; + self.displayPrompt(); + }); + + self.on("line", function onLine(cmd) { + debug("line %j", cmd); + cmd ||= ""; + sawSIGINT = false; + + if (self.editorMode) { + self[kBufferedCommandSymbol] += cmd + "\n"; + + // code alignment + const matches = self._sawKeyPress && !self[kLoadingSymbol] ? RegExpPrototypeExec(/^\s+/, cmd) : null; + if (matches) { + const prefix = matches[0]; + self.write(prefix); + self.line = prefix; + self.cursor = prefix.length; + } + FunctionPrototypeCall(_memory, self, cmd); + return; + } + + // Check REPL keywords and empty lines against a trimmed line input. + const trimmedCmd = StringPrototypeTrim(cmd); + + // Check to see if a REPL keyword was used. If it returns true, + // display next prompt and return. + if (trimmedCmd) { + if ( + StringPrototypeCharAt(trimmedCmd, 0) === "." && + StringPrototypeCharAt(trimmedCmd, 1) !== "." && + NumberIsNaN(NumberParseFloat(trimmedCmd)) + ) { + const matches = RegExpPrototypeExec(/^\.([^\s]+)\s*(.*)$/, trimmedCmd); + const keyword = matches?.[1]; + const rest = matches?.[2]; + if (FunctionPrototypeCall(_parseREPLKeyword, self, keyword, rest) === true) { + return; + } + if (!self[kBufferedCommandSymbol]) { + self.output.write("Invalid REPL keyword\n"); + finish(null); + return; + } + } + } + + const evalCmd = self[kBufferedCommandSymbol] + cmd + "\n"; + + debug("eval %j", evalCmd); + // Destructuring read of the "eval" property; see REPLEval above. + // ReflectApply keeps `this === self`, matching `self.eval(...)`. + const { eval: selfEval } = self; + ReflectApply(selfEval, self, [evalCmd, self.context, getREPLResourceName(), finish]); + + function finish(e, ret) { + debug("finish", e, ret); + FunctionPrototypeCall(_memory, self, cmd); + + if ( + e && + !self[kBufferedCommandSymbol] && + StringPrototypeStartsWith(StringPrototypeTrim(cmd), "npm ") && + !(e instanceof Recoverable) + ) { + self.output.write( + "npm should be run outside of the " + "Node.js REPL, in your normal shell.\n" + "(Press Ctrl+D to exit.)\n", + ); + self.displayPrompt(); + return; + } + + // If error was SyntaxError and not JSON.parse error + // We can start a multiline command + if (e instanceof Recoverable && !sawCtrlD) { + if (self.terminal) { + self[kAddNewLineOnTTY](); + } else { + self[kBufferedCommandSymbol] += cmd + "\n"; + self.displayPrompt(); + } + return; + } + + if (e) { + self._handleError(e.err || e); + self[kLastCommandErrored] = true; + } + + // Clear buffer if no SyntaxErrors + self.clearBufferedCommand(); + sawCtrlD = false; + + // If we got any output - print it (if no error) + if ( + !e && + // When an invalid REPL command is used, error message is printed + // immediately. We don't have to print anything else. So, only when + // the second argument to this function is there, print it. + arguments.length === 2 && + (!self.ignoreUndefined || ret !== undefined) + ) { + if (!self.underscoreAssigned) { + self.last = ret; + } + self.output.write(self.writer(ret) + "\n"); + } + + // If the REPL sever hasn't closed display prompt again (unless we already + // did by emitting the 'error' event on the domain instance). + if (!self.closed && !e) { + self[kLastCommandErrored] = false; + self.displayPrompt(); + } + } + }); + + self.on("SIGCONT", function onSigCont() { + if (self.editorMode) { + self.output.write(`${self._initialPrompt}.editor\n`); + self.output.write("// Entering editor mode (Ctrl+D to finish, Ctrl+C to cancel)\n"); + self.output.write(`${self[kBufferedCommandSymbol]}\n`); + self.prompt(true); + } else { + self.displayPrompt(true); + } + }); + + const { reverseSearch } = setupReverseSearch(this); + + const { clearPreview, showPreview } = setupPreview(this, kContextId, kBufferedCommandSymbol, preview); + + // Wrap readline tty to enable editor mode and pausing. + const ttyWrite = FunctionPrototypeBind(self._ttyWrite, self); + self._ttyWrite = (d, key) => { + key ||= {}; + if (paused && !(self.breakEvalOnSigint && key.ctrl && key.name === "c")) { + ArrayPrototypePush(pausedBuffer, ["key", [d, key], self.isCompletionEnabled]); + return; + } + if (!self.editorMode || !self.terminal) { + // Before exiting, make sure to clear the line. + if (key.ctrl && key.name === "d" && self.cursor === 0 && self.line.length === 0) { + self.clearLine(); + } + clearPreview(key); + if (!reverseSearch(d, key)) { + ttyWrite(d, key); + const showCompletionPreview = key.name !== "escape"; + showPreview(showCompletionPreview); + } + return; + } + + // Editor mode + if (key.ctrl && !key.shift) { + switch (key.name) { + // upstream-todo(BridgeAR): There should not be a special mode necessary for full + // multiline support. + case "d": // End editor mode + _turnOffEditorMode(self); + sawCtrlD = true; + ttyWrite(d, { name: "return" }); + break; + case "n": // Override next history item + case "p": // Override previous history item + break; + default: + ttyWrite(d, key); + } + } else { + switch (key.name) { + case "up": // Override previous history item + case "down": // Override next history item + break; + case "tab": + // Prevent double tab behavior + self._previousKey = null; + ttyWrite(d, key); + break; + default: + ttyWrite(d, key); + } + } + }; + + self.displayPrompt(); + } + setupHistory(historyConfig = {}, cb) { + // upstream-todo(puskin94): necessary because historyConfig can be a string for backwards compatibility + const options = typeof historyConfig === "string" ? { filePath: historyConfig } : historyConfig; + + if (typeof cb === "function") { + options.onHistoryFileLoaded = cb; + } + + this.setupHistoryManager(options); + } + clearBufferedCommand() { + this[kBufferedCommandSymbol] = ""; + } + _handleError(e) { + debug("handle error"); + if (this._userErrorHandler) { + const state = this._userErrorHandler(e); + if (state !== "ignore" && state !== "print" && state !== "unhandled") { + throw new ERR_INVALID_STATE( + 'External REPL error handler must return either "ignore", "print"' + + `, or "unhandled", but received: ${state}`, + ); + } + if (state === "ignore") { + return; + } + if (state === "unhandled") { + return "unhandled"; + } + } + let errStack = ""; + + if (typeof e === "object" && e !== null) { + // Node's overrideStackTrace formatter can't fire under JSC (stack is + // already materialized); decorateErrorStack does the REPL-frame trimming. + decorateErrorStack(e); + + if (isError(e)) { + // JSC's `.stack` is an own data property (V8's is an accessor), so under + // Object.freeze the writes below throw in strict mode; swallow that. + try { + if (e.stack) { + if (e.name === "SyntaxError") { + // Remove stack trace. + e.stack = SideEffectFreeRegExpPrototypeSymbolReplace( + /^\s+at\s.*\n?/gm, + SideEffectFreeRegExpPrototypeSymbolReplace(/^REPL\d+:\d+\r?\n/, e.stack, ""), + "", + ); + const importErrorStr = "Cannot use import statement outside a " + "module"; + if (StringPrototypeIncludes(e.message, importErrorStr)) { + e.message = + "Cannot use import statement inside the Node.js " + + "REPL, alternatively use dynamic import: " + + toDynamicImport(ArrayPrototypeAt(this.lines, -1)); + e.stack = SideEffectFreeRegExpPrototypeSymbolReplace( + /SyntaxError:.*\n/, + e.stack, + `SyntaxError: ${e.message}\n`, + ); + } + } else if (this.replMode === _loaded.REPL_MODE_STRICT) { + e.stack = SideEffectFreeRegExpPrototypeSymbolReplace( + /(\s+at\s+REPL\d+:)(\d+)/, + e.stack, + (_, pre, line) => pre + (line - 1), + ); + } + } + } catch {} + errStack = this.writer(e); + + // Remove one line error braces to keep the old style in place. + if (errStack[0] === "[" && errStack[errStack.length - 1] === "]") { + errStack = StringPrototypeSlice(errStack, 1, -1); + } + } + } + + if (!this.underscoreErrAssigned) { + this.lastError = e; + } + + if (this._isStandalone && process.listenerCount("uncaughtException") !== 0) { + process.nextTick(() => { + process.emit("uncaughtException", e); + this.clearBufferedCommand(); + this.lines.level = []; + if (!this.closed) { + this.displayPrompt(); + } + }); + } else { + if (errStack === "") { + errStack = this.writer(e); + } + const lines = SideEffectFreeRegExpPrototypeSymbolSplit(/(?<=\n)/, errStack); + let matched = false; + + errStack = ""; + ArrayPrototypeForEach(lines, line => { + if (!matched && RegExpPrototypeExec(/^\[?([A-Z][a-z0-9_]*)*Error/, line) !== null) { + errStack += writer.options.breakLength >= line.length ? `Uncaught ${line}` : `Uncaught:\n${line}`; + matched = true; + } else { + errStack += line; + } + }); + if (!matched) { + const ln = lines.length === 1 ? " " : ":\n"; + errStack = `Uncaught${ln}${errStack}`; + } + // Normalize line endings. + errStack += StringPrototypeEndsWith(errStack, "\n") ? "" : "\n"; + this.output.write(errStack); + this.clearBufferedCommand(); + this.lines.level = []; + if (!this.closed) { + this.displayPrompt(); + } + } + } + close() { + if (this.terminal && this.historyManager?.isFlushing && !this._closingOnFlush) { + this._closingOnFlush = true; + this.once("flushHistory", () => this.close()); + + return; + } + // Ensure the history file handle is closed before completing + if (this.terminal && this.historyManager?.closeHandle && !this._historyHandleClosed) { + this._historyHandleClosed = true; + this.historyManager.closeHandle().then(() => super.close()); + return; + } + process.nextTick(() => super.close()); + } + createContext() { + let context; + if (this.useGlobal) { + context = globalThis; + } else { + sendInspectorCommand( + session => { + session.post("Runtime.enable"); + session.once("Runtime.executionContextCreated", ({ params }) => { + this[kContextId] = params.context.id; + }); + context = vm.createContext(); + session.post("Runtime.disable"); + }, + () => { + context = vm.createContext(); + }, + ); + ArrayPrototypeForEach(ObjectGetOwnPropertyNames(globalThis), name => { + // Only set properties that do not already exist as a global builtin. + if (!globalBuiltins.has(name)) { + ObjectDefineProperty(context, name, { + __proto__: null, + ...ObjectGetOwnPropertyDescriptor(globalThis, name), + }); + } + }); + context.global = context; + const _console = new Console(this.output); + ObjectDefineProperty(context, "console", { + __proto__: null, + configurable: true, + writable: true, + value: _console, + }); + } + + const replModule = new CJSModule(""); + replModule.paths = CJSModule._resolveLookupPaths("", parentModule); + + ObjectDefineProperty(context, "module", { + __proto__: null, + configurable: true, + writable: true, + value: replModule, + }); + ObjectDefineProperty(context, "require", { + __proto__: null, + configurable: true, + writable: true, + value: makeRequireFunction(replModule), + }); + + addBuiltinLibsToObject(context, ""); + + return context; + } + resetContext() { + this.context = this.createContext(); + this.underscoreAssigned = false; + this.underscoreErrAssigned = false; + // upstream-todo(BridgeAR): Deprecate the lines. + this.lines = []; + this.lines.level = []; + + ObjectDefineProperty(this.context, "_", { + __proto__: null, + configurable: true, + get: () => this.last, + set: value => { + this.last = value; + if (!this.underscoreAssigned) { + this.underscoreAssigned = true; + this.output.write("Expression assignment to _ now disabled.\n"); + } + }, + }); + + ObjectDefineProperty(this.context, "_error", { + __proto__: null, + configurable: true, + get: () => this.lastError, + set: value => { + this.lastError = value; + if (!this.underscoreErrAssigned) { + this.underscoreErrAssigned = true; + this.output.write("Expression assignment to _error now disabled.\n"); + } + }, + }); + + // Allow REPL extensions to extend the new context + this.emit("reset", this.context); + } + displayPrompt(preserveCursor) { + let prompt = this._initialPrompt; + if (this[kBufferedCommandSymbol].length) { + prompt = kMultilinePrompt.description; + } + + // Do not overwrite `_initialPrompt` here + super.setPrompt(prompt); + this.prompt(preserveCursor); + } + // When invoked as an API method, overwrite _initialPrompt + setPrompt(prompt) { + this._initialPrompt = prompt; + super.setPrompt(prompt); + } + complete() { + ReflectApply(this.completer, this, arguments); + } + completeOnEditorMode(callback) { + return (err, results) => { + if (err) return callback(err); + + const { 0: completions, 1: completeOn = "" } = results; + let result = ArrayPrototypeFilter(completions, Boolean); + + if (completeOn && result.length !== 0) { + result = [commonPrefix(result)]; + } + + callback(null, [result, completeOn]); + }; + } + defineCommand(keyword, cmd) { + if (typeof cmd === "function") { + cmd = { action: cmd }; + } else { + validateFunction(cmd.action, "cmd.action"); + } + this.commands[keyword] = cmd; + } +} + +// Prompt is a string to print on each line for the prompt, +// source is a stream to use for I/O, defaulting to stdin/stdout. +function start(prompt, source, eval_, useGlobal, ignoreUndefined, replMode) { + return new REPLServer(prompt, source, eval_, useGlobal, ignoreUndefined, replMode); +} + +// upstream-todo(BridgeAR): This should be replaced with acorn to build an AST. The +// language became more complex and using a simple approach like this is not +// sufficient anymore. +function _memory(cmd) { + const self = this; + self.lines ||= []; + self.lines.level ||= []; + + // Save the line so I can do magic later + if (cmd) { + const len = self.lines.level.length ? self.lines.level.length - 1 : 0; + ArrayPrototypePush(self.lines, StringPrototypeRepeat(" ", len) + cmd); + } else { + // I don't want to not change the format too much... + ArrayPrototypePush(self.lines, ""); + } + + if (!cmd) { + self.lines.level = []; + return; + } + + // I need to know "depth." + // Because I can not tell the difference between a } that + // closes an object literal and a } that closes a function + const countMatches = (regex, str) => { + let count = 0; + while (RegExpPrototypeExec(regex, str) !== null) count++; + return count; + }; + + // Going down is { and ( e.g. function() { + // going up is } and ) + const dw = countMatches(/[{(]/g, cmd); + const up = countMatches(/[})]/g, cmd); + let depth = dw.length - up.length; + + if (depth) { + (function workIt() { + if (depth > 0) { + // Going... down. + // Push the line#, depth count, and if the line is a function. + // Since JS only has functional scope I only need to remove + // "function() {" lines, clearly this will not work for + // "function() + // {" but nothing should break, only tab completion for local + // scope will not work for this function. + ArrayPrototypePush(self.lines.level, { + line: self.lines.length - 1, + depth: depth, + }); + } else if (depth < 0) { + // Going... up. + const curr = ArrayPrototypePop(self.lines.level); + if (curr) { + const tmp = curr.depth + depth; + if (tmp < 0) { + // More to go, recurse + depth += curr.depth; + workIt(); + } else if (tmp > 0) { + // Remove and push back + curr.depth += depth; + ArrayPrototypePush(self.lines.level, curr); + } + } + } + })(); + } +} + +function _turnOnEditorMode(repl) { + repl.editorMode = true; + FunctionPrototypeCall(Interface.prototype.setPrompt, repl, ""); +} + +function _turnOffEditorMode(repl) { + repl.editorMode = false; + repl.setPrompt(repl._initialPrompt); +} + +function defineDefaultCommands(repl) { + repl.defineCommand("break", { + help: "Sometimes you get stuck, this gets you out", + action: function () { + this.clearBufferedCommand(); + this.displayPrompt(); + }, + }); + + let clearMessage; + if (repl.useGlobal) { + clearMessage = "Alias for .break"; + } else { + clearMessage = "Break, and also clear the local context"; + } + repl.defineCommand("clear", { + help: clearMessage, + action: function () { + this.clearBufferedCommand(); + if (!this.useGlobal) { + this.output.write("Clearing context...\n"); + this.resetContext(); + } + this.displayPrompt(); + }, + }); + + repl.defineCommand("exit", { + help: "Exit the REPL", + action: function () { + this.close(); + }, + }); + + repl.defineCommand("help", { + help: "Print this help message", + action: function () { + const names = ArrayPrototypeSort(ObjectKeys(this.commands)); + const longestNameLength = MathMaxApply(ArrayPrototypeMap(names, name => name.length)); + ArrayPrototypeForEach(names, name => { + const cmd = this.commands[name]; + const spaces = StringPrototypeRepeat(" ", longestNameLength - name.length + 3); + const line = `.${name}${cmd.help ? spaces + cmd.help : ""}\n`; + this.output.write(line); + }); + this.output.write("\nPress Ctrl+C to abort current expression, " + "Ctrl+D to exit the REPL\n"); + this.displayPrompt(); + }, + }); + + repl.defineCommand("save", { + help: "Save all evaluated commands in this REPL session to a file", + action: function (file) { + try { + if (file === "") { + throw new ERR_MISSING_ARGS("file"); + } + fs.writeFileSync(file, ArrayPrototypeJoin(this.lines, "\n")); + this.output.write(`Session saved to: ${file}\n`); + } catch (error) { + if (error instanceof ERR_MISSING_ARGS) { + this.output.write(`${error.message}\n`); + } else { + this.output.write(`Failed to save: ${file}\n`); + } + } + this.displayPrompt(); + }, + }); + + repl.defineCommand("load", { + help: "Load JS from a file into the REPL session", + action: function (file) { + try { + if (file === "") { + throw new ERR_MISSING_ARGS("file"); + } + const stats = fs.statSync(file); + if (stats && stats.isFile()) { + _turnOnEditorMode(this); + this[kLoadingSymbol] = true; + const data = fs.readFileSync(file, "utf8"); + this.write(data); + this[kLoadingSymbol] = false; + _turnOffEditorMode(this); + this.write("\n"); + } else { + this.output.write(`Failed to load: ${file} is not a valid file\n`); + } + } catch (error) { + if (error instanceof ERR_MISSING_ARGS) { + this.output.write(`${error.message}\n`); + } else { + this.output.write(`Failed to load: ${file}\n`); + } + } + this.displayPrompt(); + }, + }); + if (repl.terminal) { + repl.defineCommand("editor", { + help: "Enter editor mode", + action() { + _turnOnEditorMode(this); + this.output.write("// Entering editor mode (Ctrl+D to finish, Ctrl+C to cancel)\n"); + }, + }); + } +} + +ObjectAssign(_loaded, { + start, + writer, + REPLServer, + isValidSyntax, +}); + +ObjectDefineProperty(_loaded, "builtinModules", { + __proto__: null, + get: pendingDeprecation + ? deprecate( + () => getReplBuiltinLibs(), + "repl.builtinModules is deprecated. Check module.builtinModules instead", + "DEP0191", + ) + : () => getReplBuiltinLibs(), + set: pendingDeprecation + ? deprecate( + val => setReplBuiltinLibs(val), + "repl.builtinModules is deprecated. Check module.builtinModules instead", + "DEP0191", + ) + : val => setReplBuiltinLibs(val), + enumerable: false, + configurable: true, +}); + +ObjectDefineProperty(_loaded, "_builtinLibs", { + __proto__: null, + get: pendingDeprecation + ? deprecate( + () => getReplBuiltinLibs(), + "repl._builtinLibs is deprecated. Check module.builtinModules instead", + "DEP0142", + ) + : () => getReplBuiltinLibs(), + set: pendingDeprecation + ? deprecate( + val => setReplBuiltinLibs(val), + "repl._builtinLibs is deprecated. Check module.builtinModules instead", + "DEP0142", + ) + : val => setReplBuiltinLibs(val), + enumerable: false, + configurable: true, +}); + return _loaded; +} + +// Data properties: reading these does not run loadImpl; `start()` and +// `isValidSyntax()` forward into it on first call. +ObjectAssign(__node_module__.exports, { + start, + Recoverable, + REPL_MODE_SLOPPY, + REPL_MODE_STRICT, + isValidSyntax, +}); +// Accessors for what can't be hollow: REPLServer extends readline.Interface, +// writer carries a mutable .options bound to util.inspect.defaultOptions, and +// repl is assigned by createInternalRepl. +for (const name of ["REPLServer", "writer", "repl"]) { + ObjectDefineProperty(__node_module__.exports, name, { + __proto__: null, + get: () => loadImpl()[name], + set: v => { loadImpl()[name] = v; }, + enumerable: true, + configurable: true, + }); +} +for (const name of ["builtinModules", "_builtinLibs"]) { + ObjectDefineProperty(__node_module__.exports, name, { + __proto__: null, + get: () => loadImpl()[name], + set: v => { loadImpl()[name] = v; }, + enumerable: false, + configurable: true, + }); +} + +// Lets the bun --interactive entry (a plain eval script, not a builtin) reach +// internal/repl's createInternalRepl so the NODE_REPL_* env parsing has one +// implementation. Lazy getter: internal/repl requires node:repl at its top. +// Non-enumerable so it stays off the public node:repl surface. +ObjectDefineProperty(__node_module__.exports, Symbol.for("bun.repl.createInternalRepl"), { + __proto__: null, + get: () => require("internal/repl").createInternalRepl, +}); + +export default __node_module__.exports; diff --git a/src/js/node/repl.ts b/src/js/node/repl.ts deleted file mode 100644 index 73495f10104f..000000000000 --- a/src/js/node/repl.ts +++ /dev/null @@ -1,124 +0,0 @@ -// Hardcoded module "node:repl" -// 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 builtinModules = [ - "bun", - "ffi", - "assert", - "assert/strict", - "async_hooks", - "buffer", - "child_process", - "cluster", - "console", - "constants", - "crypto", - "dgram", - "diagnostics_channel", - "dns", - "dns/promises", - "domain", - "events", - "fs", - "fs/promises", - "http", - "http2", - "https", - "inspector", - "inspector/promises", - "module", - "net", - "os", - "path", - "path/posix", - "path/win32", - "perf_hooks", - "process", - "punycode", - "querystring", - "readline", - "readline/promises", - "repl", - "stream", - "stream/consumers", - "stream/promises", - "stream/web", - "string_decoder", - "sys", - "timers", - "timers/promises", - "tls", - "trace_events", - "tty", - "url", - "util", - "util/types", - "v8", - "vm", - "wasi", - "worker_threads", - "zlib", - "node:test", -]; - -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"); - }, - }, - ), - _builtinLibs: builtinModules, - builtinModules: builtinModules, -}; diff --git a/src/jsc/ModuleLoader.rs b/src/jsc/ModuleLoader.rs index 0f44c75fe5e5..0b6bf9058bc9 100644 --- a/src/jsc/ModuleLoader.rs +++ b/src/jsc/ModuleLoader.rs @@ -28,6 +28,8 @@ bun_core::declare_scope!(ModuleLoader, hidden); pub struct ModuleLoader { pub transpile_source_code_arena: Option>, pub eval_source: Option>, + /// User's `-e` bytes under `--interactive` (see `Eval::interactive_script`). + pub interactive_eval_script: Option>, } pub static IS_ALLOWED_TO_USE_INTERNAL_TESTING_APIS: core::sync::atomic::AtomicBool = diff --git a/src/jsc/bindings/ErrorCode.ts b/src/jsc/bindings/ErrorCode.ts index ff0a482ba553..75316f273b97 100644 --- a/src/jsc/bindings/ErrorCode.ts +++ b/src/jsc/bindings/ErrorCode.ts @@ -367,5 +367,9 @@ const errors: ErrorCodeMapping = [ // llhttp reports a missing CRLF after a chunk's data as HPE_STRICT, // distinct from a malformed chunk-size line (HPE_INVALID_CHUNK_SIZE). ["HPE_STRICT", Error], + ["ERR_CANNOT_WATCH_SIGINT", Error], + ["ERR_INSPECTOR_NOT_AVAILABLE", Error], + ["ERR_INVALID_REPL_EVAL_CONFIG", TypeError], + ["ERR_INVALID_REPL_INPUT", TypeError], ]; export default errors; diff --git a/src/jsc/bindings/NodeVM.cpp b/src/jsc/bindings/NodeVM.cpp index fd006bcc3ad1..4af8172edc49 100644 --- a/src/jsc/bindings/NodeVM.cpp +++ b/src/jsc/bindings/NodeVM.cpp @@ -167,7 +167,15 @@ JSC::JSFunction* constructAnonymousFunction(JSC::JSGlobalObject* globalObject, c if (actuallyValid) { auto exception = error.toErrorObject(globalObject, sourceCode, -1); + // Building the error materializes its stack, running a user + // Error.prepareStackTrace that may throw; Node throws the + // SyntaxError anyway. Terminations survive tryClearException. + if (exception) + (void)throwScope.tryClearException(); RETURN_IF_EXCEPTION(throwScope, nullptr); + // Node always attaches the arrow header to compile-time SyntaxErrors + // (node_contextify.cc DecorateErrorStack), independent of displayErrors. + decorateParseErrorStack(globalObject, vm, exception, code, options.filename, error, options.lineOffset); throwException(globalObject, throwScope, exception); return nullptr; } @@ -479,6 +487,49 @@ JSC::EncodedJSValue createCachedData(JSGlobalObject* globalObject, const JSC::So return JSValue::encode(buffer); } +// AppendExceptionLine helpers shared by the runtime (handleException) and +// compile-time (decorateParseErrorStack) paths — a single implementation of +// Node's arrow-header format so the two call sites cannot drift. +static String nthSourceLineForArrowHeader(StringView source, int64_t physicalLine1Based) +{ + if (physicalLine1Based < 1 || physicalLine1Based > static_cast(source.length()) + 1) + return {}; + size_t lineStart = 0; + for (int64_t currentLine = 1; currentLine < physicalLine1Based && lineStart != WTF::notFound; currentLine++) { + size_t newline = source.find('\n', lineStart); + lineStart = newline == WTF::notFound ? WTF::notFound : newline + 1; + } + if (lineStart == WTF::notFound) + return {}; + size_t lineEnd = source.find('\n', lineStart); + if (lineEnd == WTF::notFound) + lineEnd = source.length(); + StringView lineView = source.substring(lineStart, lineEnd - lineStart); + if (lineView.endsWith('\r')) + lineView = lineView.left(lineView.length() - 1); + // Like Node, skip the decoration for excessively long lines. + if (lineView.length() > 1024) + return {}; + return lineView.toString(); +} + +static void writeArrowHeaderStack(VM& vm, ErrorInstance* errorInstance, const String& url, int reportedLine, const String& sourceLineText, unsigned caretColumn1Based, const String& stack) +{ + String prepend; + if (!sourceLineText.isNull() && caretColumn1Based >= 1 && caretColumn1Based <= sourceLineText.length() + 1) { + StringBuilder caretLine; + for (unsigned i = 1; i < caretColumn1Based; i++) + caretLine.append(i <= sourceLineText.length() && sourceLineText[i - 1] == '\t' ? '\t' : ' '); + caretLine.append('^'); + prepend = makeString(url, ':', reportedLine, '\n', sourceLineText, '\n', caretLine.toString(), "\n\n"_s, stack); + } else { + prepend = makeString(url, ':', reportedLine, '\n', stack); + } + const auto& decoratedName = WebCore::builtinNames(vm).vmErrorDecoratedPrivateName(); + errorInstance->putDirect(vm, vm.propertyNames->stack, jsString(vm, prepend), JSC::PropertyAttribute::DontEnum | 0); + errorInstance->putDirect(vm, decoratedName, jsBoolean(true), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::ReadOnly); +} + bool handleException(JSGlobalObject* globalObject, VM& vm, NakedPtr exception, ThrowScope& throwScope) { if (auto* errorInstance = dynamicDowncast(exception->value())) { @@ -518,57 +569,24 @@ bool handleException(JSGlobalObject* globalObject, VM& vm, NakedPtr: - // - // - // - // + // :\n\n\n\n String sourceLineText; unsigned caretColumn = 0; if (JSC::CodeBlock* codeBlock = stack_frame.codeBlock()) { if (JSC::SourceProvider* provider = codeBlock->source().provider()) { - StringView providerSource = provider->source(); int64_t startLineZeroBased = provider->startPosition().m_line.zeroBasedInt(); int64_t physicalLine = static_cast(line_and_column.line) - startLineZeroBased; - if (physicalLine >= 1 && physicalLine <= static_cast(providerSource.length()) + 1) { - // Extract the physicalLine-th (1-based) line of the source. - size_t lineStart = 0; - for (int64_t currentLine = 1; currentLine < physicalLine && lineStart != WTF::notFound; currentLine++) { - size_t newline = providerSource.find('\n', lineStart); - lineStart = newline == WTF::notFound ? WTF::notFound : newline + 1; - } - if (lineStart != WTF::notFound) { - size_t lineEnd = providerSource.find('\n', lineStart); - if (lineEnd == WTF::notFound) - lineEnd = providerSource.length(); - StringView lineView = providerSource.substring(lineStart, lineEnd - lineStart); - if (lineView.endsWith('\r')) - lineView = lineView.left(lineView.length() - 1); - // Like Node, skip the decoration for excessively long lines. - if (lineView.length() <= 1024) { - sourceLineText = lineView.toString(); - caretColumn = line_and_column.column; - unsigned startColumnZeroBased = static_cast(provider->startPosition().m_column.zeroBasedInt()); - if (physicalLine == 1 && caretColumn > startColumnZeroBased) - caretColumn -= startColumnZeroBased; - } - } + sourceLineText = nthSourceLineForArrowHeader(provider->source(), physicalLine); + if (!sourceLineText.isNull()) { + caretColumn = line_and_column.column; + unsigned startColumnZeroBased = static_cast(provider->startPosition().m_column.zeroBasedInt()); + if (physicalLine == 1 && caretColumn > startColumnZeroBased) + caretColumn -= startColumnZeroBased; } } } - String prepend; - if (!sourceLineText.isNull() && caretColumn >= 1 && caretColumn <= sourceLineText.length() + 1) { - StringBuilder caretLine; - for (unsigned i = 1; i < caretColumn; i++) - caretLine.append(i <= sourceLineText.length() && sourceLineText[i - 1] == '\t' ? '\t' : ' '); - caretLine.append('^'); - prepend = makeString(source_url, ":"_s, line_and_column.line, "\n"_s, sourceLineText, "\n"_s, caretLine.toString(), "\n\n"_s, stack); - } else { - prepend = makeString(source_url, ":"_s, line_and_column.line, "\n"_s, stack); - } - errorInstance->putDirect(vm, vm.propertyNames->stack, jsString(vm, prepend), JSC::PropertyAttribute::DontEnum | 0); - errorInstance->putDirect(vm, decoratedName, jsBoolean(true), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::ReadOnly); + writeArrowHeaderStack(vm, errorInstance, source_url, static_cast(line_and_column.line), sourceLineText, caretColumn, stack); JSC::throwException(globalObject, throwScope, exception.get()); return true; @@ -576,6 +594,52 @@ bool handleException(JSGlobalObject* globalObject, VM& vm, NakedPtr:\n\n^\n\n`) using ParserError since there is no CodeBlock +// yet. Node applies this unconditionally at compile time (not displayErrors). +void decorateParseErrorStack(JSGlobalObject* globalObject, VM& vm, JSObject* error, StringView sourceString, const String& url, const JSC::ParserError& parseError, OrdinalNumber lineOffset) +{ + UNUSED_PARAM(globalObject); + auto* errorInstance = dynamicDowncast(error); + if (!errorInstance) + return; + + // The caller's toErrorObject() already materialized the stack (running any + // user Error.prepareStackTrace), so this cannot re-enter JS or throw. + errorInstance->materializeErrorInfoIfNeeded(vm, vm.propertyNames->stack); + JSValue stackValue = errorInstance->getDirect(vm, vm.propertyNames->stack); + if (!stackValue || !stackValue.isString()) + return; + auto stackHolder = asString(stackValue)->tryGetValue(); + const String& stack = stackHolder.data; + if (stack.isNull()) + return; + + // `url` is resolved by the caller: `new Script` substitutes + // evalmachine. only when no filename was provided, while + // compileFunction has no such default. An explicit "" renders as ":". + + // parseError.line() is already lineOffset-adjusted (JSC parses against a + // SourceCode whose start position carries the offset), but JSC clamps a + // negative provider start line to zero, so a negative offset comes back as + // the physical line. Undo/re-apply so Node's signed header still renders. + int lineOff = lineOffset.zeroBasedInt(); + int jscLine = parseError.line(); + int64_t physicalLine = lineOff < 0 ? static_cast(jscLine) : static_cast(jscLine) - lineOff; + int reportedLine = static_cast(physicalLine) + lineOff; + + // JSTextPosition::column() = offset - lineStartOffset — physical 0-based + // column into sourceString, so columnOffset needs no adjustment. + String sourceLineText = nthSourceLineForArrowHeader(sourceString, physicalLine); + unsigned caretColumn = 0; + if (!sourceLineText.isNull()) { + int col0 = parseError.token().m_startPosition.column(); + caretColumn = col0 >= 0 ? static_cast(col0) + 1 : 1; + } + + writeArrowHeaderStack(vm, errorInstance, url, reportedLine, sourceLineText, caretColumn, stack); +} + // Returns an encoded exception if the options are invalid. // Otherwise, returns an empty optional. std::optional getNodeVMContextOptions(JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSValue optionsArg, NodeVMContextOptions& outOptions, ASCIILiteral codeGenerationKey, JSValue* importer) diff --git a/src/jsc/bindings/NodeVM.h b/src/jsc/bindings/NodeVM.h index 590f3ce69451..fc9b555552ed 100644 --- a/src/jsc/bindings/NodeVM.h +++ b/src/jsc/bindings/NodeVM.h @@ -12,6 +12,10 @@ #include #include +namespace JSC { +class ParserError; +} + namespace Bun { class NodeVMGlobalObject; @@ -26,6 +30,9 @@ bool extractCachedData(JSValue cachedDataValue, WTF::Vector& outCachedD String stringifyAnonymousFunction(JSGlobalObject* globalObject, const ArgList& args, ThrowScope& scope, int* outOffset); JSC::EncodedJSValue createCachedData(JSGlobalObject* globalObject, const JSC::SourceCode& source); bool handleException(JSGlobalObject* globalObject, VM& vm, NakedPtr exception, ThrowScope& throwScope); +// `url` must be caller-resolved: `new Script` falls back to evalmachine. +// when no filename was provided; compileFunction has no such default. +void decorateParseErrorStack(JSGlobalObject* globalObject, VM& vm, JSObject* error, StringView sourceString, const String& url, const JSC::ParserError& parseError, OrdinalNumber lineOffset); std::optional getNodeVMContextOptions(JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSValue optionsArg, NodeVMContextOptions& outOptions, ASCIILiteral codeGenerationKey, JSValue* importer); NodeVMGlobalObject* getGlobalObjectFromContext(JSGlobalObject* globalObject, JSValue contextValue, bool canThrow); JSC::EncodedJSValue INVALID_ARG_VALUE_VM_VARIATION(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, WTF::ASCIILiteral name, JSC::JSValue value); diff --git a/src/jsc/bindings/NodeVMScript.cpp b/src/jsc/bindings/NodeVMScript.cpp index 90d1df51b524..8ab522bb0711 100644 --- a/src/jsc/bindings/NodeVMScript.cpp +++ b/src/jsc/bindings/NodeVMScript.cpp @@ -108,6 +108,8 @@ constructScript(JSGlobalObject* globalObject, CallFrame* callFrame, JSValue newT if (optionsArg.isString()) { options.filename = optionsArg.toWTFString(globalObject); RETURN_IF_EXCEPTION(scope, {}); + // `new Script(src, "name")` is a provided filename, "" included. + options.filenameProvided = true; } else if (!options.fromJS(globalObject, vm, scope, optionsArg, &importer)) { RETURN_IF_EXCEPTION(scope, JSValue::encode(jsUndefined())); } @@ -131,6 +133,29 @@ constructScript(JSGlobalObject* globalObject, CallFrame* callFrame, JSValue newT SourceCode source = makeSource(sourceString, JSC::SourceOrigin(WTF::URL::fileURLWithFileSystemPath(options.filename), *fetcher), JSC::SourceTaintedOrigin::Untainted, options.filename, TextPosition(options.lineOffset, options.columnOffset)); RETURN_IF_EXCEPTION(scope, {}); + // Node's vm.Script throws SyntaxError at construction; the REPL's + // recoverable-error flow (and user code) relies on that. This is a + // double-parse (checkSyntax discards its AST and runInThisContext reparses + // via JSC::evaluate); compile-once via m_cachedExecutable is the follow-up. + JSC::ParserError parseError; + if (!JSC::checkSyntax(vm, source, parseError)) { + auto exception = parseError.toErrorObject(globalObject, source, -1); + // Building the error materializes its stack, running a user + // Error.prepareStackTrace that may throw; Node throws the SyntaxError + // anyway. tryClearException leaves a termination for the check below. + if (exception) + (void)scope.tryClearException(); + RETURN_IF_EXCEPTION(scope, {}); + // Node always attaches the arrow header to compile-time SyntaxErrors + // (node_contextify.cc DecorateErrorStack), independent of displayErrors. + // An absent filename becomes evalmachine.; an explicitly + // provided one — including "" — is used verbatim. + String url = options.filenameProvided ? options.filename : "evalmachine."_s; + decorateParseErrorStack(globalObject, vm, exception, sourceString, url, parseError, options.lineOffset); + throwException(globalObject, scope, exception); + return {}; + } + const bool produceCachedData = options.produceCachedData; auto filename = options.filename; @@ -392,7 +417,9 @@ static JSC::EncodedJSValue runInContext(NodeVMGlobalObject* globalObject, NodeVM script->setSigintReceived(false); if (exception) [[unlikely]] { - if (handleException(globalObject, vm, exception, scope)) { + // Node only decorates the error stack with the source line when + // displayErrors is not false (lib/vm.js decorateErrorStack). + if (options.displayErrors && handleException(globalObject, vm, exception, scope)) { return {}; } JSC::throwException(globalObject, scope, exception.get()); @@ -455,7 +482,9 @@ JSC_DEFINE_HOST_FUNCTION(scriptRunInThisContext, (JSGlobalObject * globalObject, script->setSigintReceived(false); if (exception) [[unlikely]] { - if (handleException(globalObject, vm, exception, scope)) { + // Node only decorates the error stack with the source line when + // displayErrors is not false (lib/vm.js decorateErrorStack). + if (options.displayErrors && handleException(globalObject, vm, exception, scope)) { return {}; } JSC::throwException(globalObject, scope, exception.get()); diff --git a/src/jsc/modules/NodeModuleModule.cpp b/src/jsc/modules/NodeModuleModule.cpp index 2c406db7d5a1..511f1e035122 100644 --- a/src/jsc/modules/NodeModuleModule.cpp +++ b/src/jsc/modules/NodeModuleModule.cpp @@ -532,7 +532,9 @@ JSC::JSValue resolveLookupPaths(JSC::JSGlobalObject* globalObject, String reques auto len = parent.paths->length(); for (size_t i = 0; i < len; i++) { auto path = parent.paths->getIndex(globalObject, i); + RETURN_IF_EXCEPTION(scope, {}); array->push(globalObject, path); + RETURN_IF_EXCEPTION(scope, {}); } RELEASE_AND_RETURN(scope, array); } else if (parent.pathsArrayLazy && parent.filename) { diff --git a/src/options_types/context.rs b/src/options_types/context.rs index 71b04ade4034..5573c6990794 100644 --- a/src/options_types/context.rs +++ b/src/options_types/context.rs @@ -543,6 +543,9 @@ pub struct RuntimeOptions { /// `--expose-gc` makes `globalThis.gc()` available. Added for Node /// compatibility. pub expose_gc: bool, + /// `--interactive` starts the Node.js-compatible REPL (node:repl), like + /// `node --interactive`. (`-i` is taken by `--install=fallback`.) + pub interactive: bool, pub preserve_symlinks_main: bool, pub console_depth: Option, pub cron_title: Box<[u8]>, @@ -555,6 +558,10 @@ pub struct RuntimeOptions { pub struct Eval { pub script: Box<[u8]>, pub eval_and_print: bool, + /// Under `--interactive`, `script` holds the node:repl bootstrap; this + /// holds the user's actual `-e` bytes so `process._eval` reports them + /// (or `undefined` when empty). `None` = not `--interactive`. + pub interactive_script: Option>, } pub struct CpuProf { @@ -605,6 +612,7 @@ impl Default for RuntimeOptions { experimental_http3_fetch: false, dns_result_order: Box::from(&b"verbatim"[..]), expose_gc: false, + interactive: false, preserve_symlinks_main: false, console_depth: None, cron_title: Box::default(), diff --git a/src/resolve_builtins/HardcodedModule.rs b/src/resolve_builtins/HardcodedModule.rs index 5c56c51716de..51760c622eb9 100644 --- a/src/resolve_builtins/HardcodedModule.rs +++ b/src/resolve_builtins/HardcodedModule.rs @@ -180,6 +180,16 @@ pub enum HardcodedModule { /// This is gated behind '--expose-internals' #[strum(serialize = "bun:internal-for-testing")] BunInternalForTesting, + // Node internal modules exposed for the vendored Node.js test suite. + // Gated like `bun:internal-for-testing` (debug builds / --expose-internals). + #[strum(serialize = "internal:repl")] + NodeInternalRepl, + #[strum(serialize = "internal:repl/await")] + NodeInternalReplAwait, + #[strum(serialize = "internal:repl/history")] + NodeInternalReplHistory, + #[strum(serialize = "internal:util/inspect")] + NodeInternalUtilInspect, /// Node.js-internal testing shim (`require('internal/test/binding')`), /// gated behind '--expose-internals' like `bun:internal-for-testing`. #[strum(serialize = "internal/test/binding")] @@ -201,6 +211,10 @@ bun_core::comptime_string_map! { b"bun:sqlite" => HardcodedModule::BunSqlite, b"bun:wrap" => HardcodedModule::BunWrap, b"bun:internal-for-testing" => HardcodedModule::BunInternalForTesting, + b"internal/repl" => HardcodedModule::NodeInternalRepl, + b"internal/repl/await" => HardcodedModule::NodeInternalReplAwait, + b"internal/repl/history" => HardcodedModule::NodeInternalReplHistory, + b"internal/util/inspect" => HardcodedModule::NodeInternalUtilInspect, b"internal/test/binding" => HardcodedModule::InternalTestBinding, // Node.js b"node:assert" => HardcodedModule::NodeAssert, @@ -708,6 +722,12 @@ const BUN_EXTRA_ALIAS_KVS: &[AliasKv] = &[ entry!("bun:sqlite"), entry!("bun:wrap"), entry!("bun:internal-for-testing"), + // Node internal modules for the vendored Node.js test suite (gated in + // jsc_hooks like bun:internal-for-testing: debug / --expose-internals). + entry!("internal/repl"), + entry!("internal/repl/await"), + entry!("internal/repl/history"), + entry!("internal/util/inspect"), entry!("internal/test/binding"), ( b"ffi", diff --git a/src/runtime/cli/Arguments.rs b/src/runtime/cli/Arguments.rs index caf53675e719..9cd13cc7e6d6 100644 --- a/src/runtime/cli/Arguments.rs +++ b/src/runtime/cli/Arguments.rs @@ -175,6 +175,9 @@ pub(crate) const RUNTIME_PARAMS_: &[ParamType] = &[ parse_param!( "--smol Use less memory, but run garbage collection more often" ), + parse_param!( + "--interactive Start a Node.js-compatible REPL, like node --interactive" + ), parse_param!( "-r, --preload ... Import a module before other modules are loaded" ), @@ -1062,7 +1065,9 @@ pub fn parse(cmd: CommandTag, ctx: Context<'_>) -> crate::Result) -> crate::Result ctx.positionals.is_empty(), + Tag::RunCommand => match ctx.positionals.as_slice() { + [] => true, + [r] => r.as_ref() == b"run", + _ => false, + }, + _ => false, + }; + if no_target { + return run_command::RunCommand::exec_node_repl(ctx); + } + } + if tag == Tag::AutoCommand && !ctx.runtime_options.eval.script.is_empty() { return run_command::RunCommand::exec_eval(ctx); } diff --git a/src/runtime/cli/run_command.rs b/src/runtime/cli/run_command.rs index d21bbf0379a5..8c28e83ecdea 100644 --- a/src/runtime/cli/run_command.rs +++ b/src/runtime/cli/run_command.rs @@ -986,6 +986,8 @@ Full documentation is available at https://bun.com/docs/cli/run }; vm.module_loader.eval_source = Some(Box::new(bun_ast::Source::init_path_string(entry, script))); + vm.module_loader.interactive_eval_script = + ctx.runtime_options.eval.interactive_script.take(); if ctx.runtime_options.eval.eval_and_print { vm.transpiler.options.dead_code_elimination = false; } @@ -2934,6 +2936,24 @@ impl RunCommand { Ok(true) } + /// `bun --interactive` — boots the embedded `eval/node-repl.ts` script, + /// the Node.js-compatible REPL (node:repl). Distinct from `bun repl`, + /// which is Bun's own native REPL. + pub fn exec_node_repl(ctx: &mut ContextData) -> crate::Result<()> { + // Every caller has already established there's no user script target; + // any remaining positionals are dispatch artifacts (e.g. RunCommand's + // leading "run"), not user data — keep them out of `process.argv`. + ctx.positionals.clear(); + let bootstrap = bun_core::runtime_embed_file!(Codegen, "eval/node-repl.ts").as_bytes(); + // Stash the user's `-e` (so `process._eval` is correct) and boot the + // bootstrap via `[eval]`; it runs `process._eval` like Node's + // internal/main/repl.js — no source splicing. + ctx.runtime_options.eval.interactive_script = + Some(::core::mem::take(&mut ctx.runtime_options.eval.script)); + ctx.runtime_options.eval.script = bootstrap.to_vec().into_boxed_slice(); + Self::exec_eval(ctx) + } + /// Synthetic `cwd/[eval]` /// entry point + boot. `Arguments::parse` has already stashed the script /// in `ctx.runtime_options.eval.script`. Public so `Command::start` can @@ -2969,6 +2989,15 @@ impl RunCommand { // `Command::which()` before dispatch. debug_assert!(crate::cli::PRETEND_TO_BE_NODE.load(::core::sync::atomic::Ordering::Relaxed)); + // `node --interactive [-e code]`: same gate as AutoCommand — a script + // positional wins, and `-p` currently bypasses the REPL (see mod.rs). + if ctx.runtime_options.interactive + && !ctx.runtime_options.eval.eval_and_print + && ctx.positionals.is_empty() + { + return Self::exec_node_repl(ctx); + } + if !ctx.runtime_options.eval.script.is_empty() { // synthetic `[eval]` path under cwd let mut entry_point_buf = [0u8; MAX_PATH_BYTES + EVAL_TRIGGER.len()]; @@ -2985,6 +3014,13 @@ impl RunCommand { } if ctx.positionals.is_empty() { + // Node: bare `node` on a TTY starts the REPL. Only in emulation + // mode; bun's own `bun` with no args stays the help text. Use + // Output's cached stdio flag (set at startup via libuv's handle + // probe), which is the same check `bun update --interactive` uses. + if Output::is_stdin_tty() { + return Self::exec_node_repl(ctx); + } Self::exec_as_if_node_missing_script(); } @@ -3031,7 +3067,7 @@ impl RunCommand { )] fn exec_as_if_node_missing_script() -> ! { Output::err_generic( - "Missing script to execute. Bun's provided 'node' cli wrapper does not support a repl.", + "Missing script to execute. Pass --interactive to start the Node.js-compatible REPL.", (), ); Global::exit(1); diff --git a/src/runtime/jsc_hooks.rs b/src/runtime/jsc_hooks.rs index e77b514915e7..0c719212132e 100644 --- a/src/runtime/jsc_hooks.rs +++ b/src/runtime/jsc_hooks.rs @@ -3593,7 +3593,11 @@ fn get_hardcoded_module( } Some(js_synthetic_module(b"node:zlib/iter", specifier)) } - HardcodedModule::BunInternalForTesting => { + HardcodedModule::BunInternalForTesting + | HardcodedModule::NodeInternalRepl + | HardcodedModule::NodeInternalReplAwait + | HardcodedModule::NodeInternalReplHistory + | HardcodedModule::NodeInternalUtilInspect => { // Gated behind `--expose-internals` (release) / always-on (debug). if !bun_core::env::IS_DEBUG { let allowed = bun_jsc::module_loader::IS_ALLOWED_TO_USE_INTERNAL_TESTING_APIS @@ -3602,7 +3606,8 @@ fn get_hardcoded_module( return None; } } - Some(js_synthetic_module(b"bun:internal-for-testing", specifier)) + let name: &'static str = hardcoded.into(); + Some(js_synthetic_module(name.as_bytes(), specifier)) } HardcodedModule::InternalTestBinding => { // Gated behind `--expose-internals` (release) / always-on (debug), diff --git a/src/runtime/node/node_process.rs b/src/runtime/node/node_process.rs index bae48904bf5c..524cd75f1b38 100644 --- a/src/runtime/node/node_process.rs +++ b/src/runtime/node/node_process.rs @@ -390,8 +390,19 @@ mod _impl { pub(super) extern "C" fn get_eval(global_object: &JSGlobalObject) -> JSValue { // SAFETY: `bun_vm()` returns the live per-thread VM for this global. let vm = global_object.bun_vm(); + // `--interactive` boots the bootstrap through `eval_source`, so read + // the user's real `-e` bytes from `interactive_eval_script` instead + // (`undefined` when empty, matching `node -i` without `-e`). + if let Some(script) = vm.module_loader.interactive_eval_script.as_deref() { + if script.is_empty() { + return JSValue::UNDEFINED; + } + return ZigString::init(script).with_encoding().to_js(global_object); + } if let Some(source) = vm.module_loader.eval_source.as_deref() { - return ZigString::init(source.contents()).to_js(global_object); + return ZigString::init(source.contents()) + .with_encoding() + .to_js(global_object); } JSValue::UNDEFINED } diff --git a/test/cli/run/as-node.test.ts b/test/cli/run/as-node.test.ts index 871a1809d2fb..f649c20fd3b8 100644 --- a/test/cli/run/as-node.test.ts +++ b/test/cli/run/as-node.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; import { join } from "path"; -import { fakeNodeRun, tempDirWithFiles } from "../../harness"; +import { bunEnv, bunExe, fakeNodeRun, tempDirWithFiles } from "../../harness"; describe("fake node cli", () => { test("the node cli actually works", () => { @@ -97,8 +97,18 @@ describe("fake node cli", () => { ); }); - test("no args is exit code zero for now", () => { + // Bare `node` now matches Node.js: a TTY stdin enters the REPL, a + // non-TTY stdin (pipe) prints "Missing script". fakeNodeRun's default + // stdin is platform-dependent (Windows may inherit a console), so pin + // a piped stdin here. + test("no args with piped stdin errors with 'Missing script'", () => { const temp = tempDirWithFiles("fake-node", {}); - expect(() => fakeNodeRun(temp, [])).toThrow(); + const result = Bun.spawnSync([bunExe(), "--bun", "node"], { + cwd: temp, + env: { ...bunEnv, NODE_ENV: undefined }, + stdin: Buffer.alloc(0), + }); + expect(result.stderr.toString()).toContain("Missing script"); + expect(result.success).toBe(false); }); }); diff --git a/test/cli/run/run-eval.test.ts b/test/cli/run/run-eval.test.ts index 8f4bf967c094..9b86234fdadf 100644 --- a/test/cli/run/run-eval.test.ts +++ b/test/cli/run/run-eval.test.ts @@ -74,6 +74,20 @@ for (const flag of ["-e", "--print"]) { expect(stdout.toString("utf8")).toEqual(code + "\n"); }); + // The eval source is UTF-8; reading it back as Latin-1 turns every + // multi-byte character into mojibake. The expected text is compared here + // in the parent -- comparing inside the child would pass either way, since + // a Latin-1-decoded source corrupts the literal and process._eval alike. + test("process._eval round-trips multi-byte UTF-8", async () => { + const marker = "/* 한글-🎉-café */"; + const code = (flag === "--print" ? "process._eval" : "console.log(process._eval)") + ` ${marker}`; + const { stdout } = Bun.spawnSync({ + cmd: [bunExe(), flag, code], + env: bunEnv, + }); + expect(stdout.toString("utf8")).toEqual(code + "\n"); + }); + test("does not crash in non-latin1 directory", async () => { const dir = join(tmpdirSync(), "eval-test-开始学习"); await Bun.write(join(dir, "index.js"), "console.log('hello world')"); diff --git a/test/js/bun/repl/repl.test.ts b/test/js/bun/repl/repl.test.ts index 0d8e2ce79af9..6c1a60fdb8d6 100644 --- a/test/js/bun/repl/repl.test.ts +++ b/test/js/bun/repl/repl.test.ts @@ -1249,3 +1249,601 @@ describe.skipIf(isWindows)("REPL history file permissions", () => { expect(exitCode).toBe(0); }); }); + +// `bun --interactive` boots the full node:repl + readline + acorn stack; on a +// debug+asan build that is ~4–5s per spawn, so the 5s default is too tight. +const interactiveTimeout = 20_000; + +describe.concurrent("--interactive", () => { + const env = { ...bunEnv, NO_COLOR: "1", NODE_REPL_HISTORY: "" }; + + async function runInteractive(extra: string[], stdin: string, opts: { cwd?: string; env?: any } = {}) { + await using proc = Bun.spawn({ + cmd: [bunExe(), "--interactive", ...extra], + env: { ...env, ...opts.env }, + cwd: opts.cwd, + // Closing stdin (EOF) exits the REPL; `.exit` adds latency on debug builds. + stdin: Buffer.from(stdin), + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + return { stdout, stderr, exitCode }; + } + + test( + "prints a Bun-branded banner, not 'Welcome to Node.js'", + async () => { + const { stdout, stderr, exitCode } = await runInteractive([], ""); + expect({ stdout, stderr }).toEqual({ + stdout: expect.stringMatching(/^Welcome to Bun v\d+\.\d+\.\d+.*\(Node\.js-compatible REPL/), + stderr: expect.not.stringContaining("error"), + }); + expect(stdout).not.toContain("Welcome to Node.js"); + expect(exitCode).toBe(0); + }, + interactiveTimeout, + ); + + // `node -i -e 'code'`: -e runs as its own Script against globalThis, so + // `var`/`function` declarations are visible from the REPL prompt. + test( + "-e var/function declarations are visible in the REPL", + async () => { + const { stdout, stderr, exitCode } = await runInteractive( + ["-e", "var fromVar = 1; function f() { return 42 }"], + "fromVar + f()\n", + ); + expect(stdout).toContain("43"); + expect(stderr).not.toContain("error"); + expect(exitCode).toBe(0); + }, + interactiveTimeout, + ); + + // `process._eval` carries the raw `-e` bytes, which are UTF-8. Decoding them + // as Latin-1 turns every multi-byte character into mojibake, so both the + // evaluated source and the reported `process._eval` must round-trip. + test( + "-e round-trips multi-byte UTF-8 through process._eval", + async () => { + const source = `console.log("한글-🎉-café")`; + const { stdout, stderr, exitCode } = await runInteractive(["-e", source], "process._eval\n"); + // The -e script itself ran with its literal intact... + expect(stdout).toContain("한글-🎉-café"); + // ...and process._eval reports the source verbatim, not re-encoded. + expect(stdout).toContain(source); + expect(stderr).not.toContain("error"); + expect(exitCode).toBe(0); + }, + interactiveTimeout, + ); + + // `node -i -e ''`: Node exits 1 with a SyntaxError code frame at + // [eval]:1 and never accepts REPL input; not caught by the REPL error handler. + test( + "-e with a syntax error is fatal and never enters the REPL", + async () => { + const { stdout, stderr, exitCode } = await runInteractive(["-e", "console.log(1"], "'stdin-ran'\n"); + expect(stdout).toContain("Welcome to Bun"); + // stdin was never evaluated: + expect(stdout).not.toContain("stdin-ran"); + // The error is reported against the user's [eval] script, not the bootstrap. + expect(stdout + stderr).toMatch(/SyntaxError/); + expect(stdout + stderr).toContain("[eval]"); + expect(stdout + stderr).not.toMatch(/node-repl|createInternalRepl|__BUN_EVAL_SCRIPT__/); + expect(exitCode).toBe(1); + }, + interactiveTimeout, + ); + + test( + "-e with a runtime error is fatal and never enters the REPL", + async () => { + const { stdout, stderr, exitCode } = await runInteractive(["-e", 'throw new Error("BOOM")'], "'stdin-ran'\n"); + expect(stdout).not.toContain("stdin-ran"); + expect(stdout + stderr).toContain("BOOM"); + expect(stdout + stderr).toContain("[eval]"); + expect(exitCode).toBe(1); + }, + interactiveTimeout, + ); + + test.each(["/*", "const x=`foo"])( + "-e with an unterminated template/comment cannot swallow the bootstrap (%j)", + async bad => { + const { stdout, stderr, exitCode } = await runInteractive(["-e", bad], ""); + expect(stdout).toContain("Welcome to Bun"); + expect(stdout + stderr).toMatch(/SyntaxError/); + expect(exitCode).toBe(1); + }, + interactiveTimeout, + ); + + // Node silently ignores `-i` when a script positional is present. + test( + "with a script positional runs the script and does not enter the REPL", + async () => { + using dir = tempDir("interactive-script", { "foo.js": `console.log("script-ran")` }); + const { stdout, stderr, exitCode } = await runInteractive(["foo.js"], "1+1\n", { cwd: String(dir) }); + expect(stdout).toContain("script-ran"); + expect(stdout).not.toContain("Welcome"); + expect(stdout).not.toContain("> "); + expect(stderr).not.toContain("error"); + expect(exitCode).toBe(0); + }, + interactiveTimeout, + ); + + // Documented "for now" deviation: `-p` wins over `--interactive`. + test( + "-p wins over --interactive (prints, no REPL)", + async () => { + const { stdout, stderr, exitCode } = await runInteractive(["-p", "1+1"], "999\n"); + expect(stdout.trim()).toBe("2"); + expect(stdout).not.toContain("Welcome"); + expect(stderr).not.toContain("error"); + expect(exitCode).toBe(0); + }, + interactiveTimeout, + ); + + // exec_node_repl boots the bootstrap through the [eval] slot; process._eval + // must still report the user's -e string (used by child_process.fork's + // execArgv stripping), not the bootstrap. + test( + "process._eval reports the user's -e string, not the bootstrap", + async () => { + const eScript = 'console.log("EVAL=" + JSON.stringify(process._eval)); process.exit(0)'; + const { stdout, stderr, exitCode } = await runInteractive(["-e", eScript], ""); + expect(stdout).toContain(`EVAL=${JSON.stringify(eScript)}`); + expect(stdout + stderr).not.toMatch(/__BUN_EVAL_SCRIPT__|createInternalRepl/); + expect(exitCode).toBe(0); + }, + interactiveTimeout, + ); + + test( + "process._eval is undefined without -e", + async () => { + const { stdout, exitCode } = await runInteractive([], 'console.log("EVAL=" + process._eval)\n'); + expect(stdout).toContain("EVAL=undefined"); + expect(exitCode).toBe(0); + }, + interactiveTimeout, + ); + + // The bootstrap runs -e via vm.runInThisContext (raw JS, matching + // `node -i -e`); TypeScript syntax is a SyntaxError, not transpiled. + test( + "-e is raw JavaScript (not transpiled)", + async () => { + const { stdout, stderr, exitCode } = await runInteractive(["-e", "const x: number = 1"], ""); + expect(stdout + stderr).toMatch(/SyntaxError/); + expect(exitCode).toBe(1); + }, + interactiveTimeout, + ); + + // bun-as-node --interactive routes through exec_as_if_node, which used to + // print "does not support a repl" and exit 1. + test( + "bun-as-node --interactive enters the REPL", + async () => { + await using proc = Bun.spawn({ + cmd: [bunExe(), "--interactive"], + argv0: "node", + env, + stdin: Buffer.from("1+1\n"), + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stdout).toContain("Welcome to Bun"); + expect(stdout).toContain("2"); + expect(stderr).not.toContain("does not support a repl"); + expect(exitCode).toBe(0); + }, + interactiveTimeout, + ); + + // node evaluates `-e` after createInternalRepl, via runScriptInContext, which + // publishes the CJS bindings onto the global before running the body. + test( + "-e sees require/module/__filename/__dirname like `node -i -e`", + async () => { + const { stdout, exitCode } = await runInteractive( + ["-e", "console.log(typeof require, typeof module, typeof __filename, typeof __dirname)"], + "", + ); + expect(stdout).toContain("function object string string"); + expect(exitCode).toBe(0); + }, + interactiveTimeout, + ); + + // node's wrapper compiles as `[eval]-wrapper`, so __dirname is "." — NOT the + // cwd — while module.filename stays the cwd-joined path. + test( + "-e exposes node's exact __dirname/__filename/module.filename", + async () => { + using dir = tempDir("repl-eval-dirname", {}); + const { stdout, exitCode } = await runInteractive( + ["-e", "console.log(JSON.stringify({d: __dirname, f: __filename, m: module.filename}))"], + "", + { cwd: String(dir) }, + ); + const parsed = JSON.parse(stdout.slice(stdout.indexOf("{"), stdout.indexOf("}") + 1)); + expect({ d: parsed.d, f: parsed.f }).toEqual({ d: ".", f: "[eval]" }); + expect(parsed.m).toBe(path.join(String(dir), "[eval]")); + expect(exitCode).toBe(0); + }, + interactiveTimeout, + ); + + test( + "-e can require() a builtin", + async () => { + const { stdout, exitCode } = await runInteractive( + ["-e", 'console.log("plat:" + typeof require("os").platform)'], + "", + ); + expect(stdout).toContain("plat:function"); + expect(exitCode).toBe(0); + }, + interactiveTimeout, + ); + + // Publishing those bindings must not move `var`/`function` off the global — + // node runs the body in global scope, it does not CJS-wrap it. + test( + "-e declarations still land on the REPL's global", + async () => { + const { stdout, exitCode } = await runInteractive(["-e", "var x = 5; function f(){}"], "typeof x + typeof f\n"); + expect(stdout).toContain("numberfunction"); + expect(exitCode).toBe(0); + }, + interactiveTimeout, + ); + + // node's `-i` is an alias for --interactive. Bun's own `-i` is + // --install=fallback, which has no meaning under node emulation, so the node + // meaning wins there; everywhere else `-i` stays --install=fallback. + test( + "bun-as-node: `node -i` enters the REPL", + async () => { + await using proc = Bun.spawn({ + cmd: [bunExe(), "-i"], + argv0: "node", + env, + stdin: Buffer.from("1+1\n"), + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stdout).toContain("Welcome to Bun"); + expect(stdout).toContain("2"); + expect(stderr).not.toContain("Missing script to execute"); + expect(exitCode).toBe(0); + }, + interactiveTimeout, + ); + + test( + "bun run --interactive is not a silent no-op", + async () => { + await using proc = Bun.spawn({ + cmd: [bunExe(), "run", "--interactive"], + env, + stdin: Buffer.from("1+1\n"), + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stdout).toContain("Welcome to Bun"); + expect(stdout).toContain("2"); + expect(stderr).not.toContain("error"); + expect(exitCode).toBe(0); + }, + interactiveTimeout, + ); + + // The "run" subcommand word is a dispatch artifact, not user input: it must + // not survive into the REPL's process.argv the way a script name would. + test( + "bun run --interactive keeps 'run' out of process.argv", + async () => { + await using proc = Bun.spawn({ + cmd: [bunExe(), "run", "--interactive"], + env, + // Tagged so the match can't be confused with the REPL's own echo. + stdin: Buffer.from(`console.log("ARGV:" + JSON.stringify(process.argv.slice(1)))\n`), + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + const match = stdout.match(/ARGV:(\[.*\])/); + expect(match).not.toBeNull(); + expect(JSON.parse(match![1])).toEqual([]); + expect(exitCode).toBe(0); + }, + interactiveTimeout, + ); + + test( + "NODE_REPL_EXTERNAL_MODULE replaces the built-in REPL", + async () => { + using dir = tempDir("ext-repl", { "ext.js": `console.log("external-repl-42")` }); + const { stdout, stderr, exitCode } = await runInteractive([], "", { + cwd: String(dir), + env: { NODE_REPL_EXTERNAL_MODULE: "./ext.js" }, + }); + expect(stdout).toContain("external-repl-42"); + expect(stdout).not.toContain("Welcome"); + expect(stderr).not.toContain("error"); + expect(exitCode).toBe(0); + }, + interactiveTimeout, + ); +}); + +// ts-node does `require("repl")` at import time but only touches +// repl.start/repl.Recoverable inside createRepl(); those (plus the REPL_MODE +// symbols and isValidSyntax) are data properties so the destructure is free, +// and only calling start() or reading REPLServer/writer loads the body. +test("require('node:repl') is hollow until start() or REPLServer is used", async () => { + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + ` + const repl = require("node:repl"); + const shape = n => "value" in Object.getOwnPropertyDescriptor(repl, n) ? "data" : "accessor"; + console.log(JSON.stringify({ + keys: Object.keys(repl).sort(), + desc: { + start: shape("start"), + Recoverable: shape("Recoverable"), + REPL_MODE_SLOPPY: shape("REPL_MODE_SLOPPY"), + isValidSyntax: shape("isValidSyntax"), + REPLServer: shape("REPLServer"), + writer: shape("writer"), + }, + })); + // Reading the cheap five must not throw and must not require readline. + const {start, Recoverable, REPL_MODE_SLOPPY, REPL_MODE_STRICT, isValidSyntax} = repl; + console.log(JSON.stringify({ + start: typeof start, + Recoverable: typeof Recoverable, + REPL_MODE_SLOPPY: typeof REPL_MODE_SLOPPY, + isValidSyntax: typeof isValidSyntax, + })); + console.log("recoverable-is-error=" + (new Recoverable(new SyntaxError("m")) instanceof SyntaxError)); + // Now force the full load and check REPLServer is real. + console.log("REPLServer=" + typeof repl.REPLServer); + // Recoverable identity: the one exposed before load is the one the impl uses. + console.log("same-Recoverable=" + (repl.Recoverable === Recoverable)); + repl.repl = "x"; + console.log("repl.repl=" + repl.repl); + `, + ], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + const lines = stdout.trim().split("\n"); + expect(JSON.parse(lines[0])).toEqual({ + keys: [ + "REPLServer", + "REPL_MODE_SLOPPY", + "REPL_MODE_STRICT", + "Recoverable", + "isValidSyntax", + "repl", + "start", + "writer", + ], + desc: { + start: "data", + Recoverable: "data", + REPL_MODE_SLOPPY: "data", + isValidSyntax: "data", + REPLServer: "accessor", + writer: "accessor", + }, + }); + expect(JSON.parse(lines[1])).toEqual({ + start: "function", + Recoverable: "function", + REPL_MODE_SLOPPY: "symbol", + isValidSyntax: "function", + }); + expect(lines[2]).toBe("recoverable-is-error=true"); + expect(lines[3]).toBe("REPLServer=function"); + expect(lines[4]).toBe("same-Recoverable=true"); + expect(lines[5]).toBe("repl.repl=x"); + expect(exitCode).toBe(0); +}); + +describe.concurrent("node:repl process-global side effects", () => { + const env = { ...bunEnv, NO_COLOR: "1" }; + + // Known limitation until process.addUncaughtExceptionCaptureCallback is + // implemented natively: the shim occupies the exclusive capture slot for the + // process lifetime. It must NOT displace a user callback installed BEFORE the + // first repl.start(). + test( + "uncaught-exception capture shim defers to a pre-installed user callback", + async () => { + const script = ` + let userGot; + process.setUncaughtExceptionCaptureCallback(e => { userGot = e.message; }); + const repl = require("node:repl"); + const { PassThrough } = require("node:stream"); + const inp = new PassThrough(), out = new PassThrough(); out.resume(); + const r = repl.start({ input: inp, output: out, terminal: false, prompt: "" }); + r.close(); + setImmediate(() => { throw new Error("boom"); }); + setImmediate(() => setImmediate(() => { + console.log("userGot=" + userGot); + // The user callback owns the slot; REPL didn't displace it. + process.exit(0); + })); + `; + await using proc = Bun.spawn({ cmd: [bunExe(), "-e", script], env, stdout: "pipe", stderr: "pipe" }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stdout).toContain("userGot=boom"); + expect(stderr).not.toContain("ALREADY_SET"); + expect(exitCode).toBe(0); + }, + interactiveTimeout, + ); + + // Node filters slash-modules in addBuiltinLibsToObject (not + // getBuiltinLibs), so `fs/promises` etc. never land on the REPL context + // while repl.builtinModules and require-completion still list them. + test( + "addBuiltinLibsToObject does not install slash-modules on the REPL context", + async () => { + const script = ` + const repl = require("node:repl"); + const { PassThrough } = require("node:stream"); + const inp = new PassThrough(), out = new PassThrough(); out.resume(); + const r = repl.start({ input: inp, output: out, terminal: false, prompt: "" }); + const slash = Object.getOwnPropertyNames(r.context).filter(n => n.includes("/")); + const listed = repl.builtinModules.filter(n => n.includes("/")); + console.log("SLASH=" + JSON.stringify(slash) + " LISTED=" + (listed.includes("fs/promises"))); + r.close(); + `; + await using proc = Bun.spawn({ cmd: [bunExe(), "-e", script], env, stdout: "pipe", stderr: "pipe" }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stdout).toContain("SLASH=[] LISTED=true"); + expect(stderr).not.toContain("error"); + expect(exitCode).toBe(0); + }, + interactiveTimeout, + ); + + // decorateErrorStack runs after user code, so a tampered String.prototype.split + // must not stop the REPL from rendering the next error. + test( + "error rendering survives a tampered String.prototype.split", + async () => { + const script = ` + const repl = require("node:repl"); + const { PassThrough } = require("node:stream"); + const inp = new PassThrough(), out = new PassThrough(); + let buf = ""; out.on("data", d => buf += d); + const r = repl.start({ input: inp, output: out, terminal: false, prompt: "> " }); + r.on("exit", () => { console.log(buf); process.exit(0); }); + inp.write("String.prototype.split = () => { throw 0 }\\n"); + inp.write("oops\\n"); + inp.write("1+1\\n"); + inp.end(); + `; + await using proc = Bun.spawn({ cmd: [bunExe(), "-e", script], env, stdout: "pipe", stderr: "pipe" }); + const [stdout, , exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stdout).toContain("Uncaught ReferenceError"); + expect(stdout).toContain("> 2"); + expect(exitCode).toBe(0); + }, + interactiveTimeout, + ); + + test( + "REPL survives a tampered RegExp.prototype[Symbol.split]", + async () => { + const script = ` + const repl = require("node:repl"); + const { PassThrough } = require("node:stream"); + const inp = new PassThrough(), out = new PassThrough(); + let buf = ""; out.on("data", d => buf += d); + const r = repl.start({ input: inp, output: out, terminal: false, prompt: "> " }); + r.on("exit", () => { console.log(buf); process.exit(0); }); + inp.write("RegExp.prototype[Symbol.split] = () => { throw 0 }\\n"); + inp.write("oops\\n"); + inp.write("1+1\\n"); + inp.end(); + `; + await using proc = Bun.spawn({ cmd: [bunExe(), "-e", script], env, stdout: "pipe", stderr: "pipe" }); + const [stdout, , exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stdout).toContain("Uncaught ReferenceError"); + expect(stdout).toContain("> 2"); + expect(exitCode).toBe(0); + }, + interactiveTimeout, + ); + + test( + "REPL survives a tampered RegExp.prototype[Symbol.replace]", + async () => { + const script = ` + const repl = require("node:repl"); + const { PassThrough } = require("node:stream"); + const inp = new PassThrough(), out = new PassThrough(); + let buf = ""; out.on("data", d => buf += d); + const r = repl.start({ input: inp, output: out, terminal: false, prompt: "> " }); + r.on("exit", () => { console.log(buf); process.exit(0); }); + inp.write("RegExp.prototype[Symbol.replace] = () => { throw 0 }\\n"); + inp.write("oops\\n"); + inp.write("1+1\\n"); + inp.end(); + `; + await using proc = Bun.spawn({ cmd: [bunExe(), "-e", script], env, stdout: "pipe", stderr: "pipe" }); + const [stdout, , exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stdout).toContain("Uncaught ReferenceError"); + expect(stdout).toContain("> 2"); + expect(exitCode).toBe(0); + }, + interactiveTimeout, + ); +}); + +// JSC's Error#stack is an own data property (V8's is an accessor), so a frozen +// error makes _handleError's strict-mode `e.stack = …` rewrites throw; the Bun +// port guards those writes so the REPL prints the error and continues like Node. +describe.concurrent("node:repl prints a frozen thrown error and continues", () => { + test.each([ + ["Error in sloppy mode", "SLOPPY", "throw Object.freeze(new Error('boom'))", "Uncaught Error: boom"], + ["SyntaxError", "SLOPPY", "throw Object.freeze(new SyntaxError('boom'))", "Uncaught SyntaxError: boom"], + ["Error in strict mode", "STRICT", "throw Object.freeze(new Error('boom'))", "Uncaught Error: boom"], + ])( + "%s", + async (_name, mode, line, expectedFirstLine) => { + const script = ` + const repl = require("repl"); + const { PassThrough } = require("stream"); + const inp = new PassThrough(), out = new PassThrough(); + let buf = ""; + out.on("data", d => buf += d); + const r = repl.start({ + input: inp, + output: out, + terminal: false, + prompt: "", + useGlobal: true, + replMode: repl.REPL_MODE_${mode}, + }); + r.on("exit", () => process.stdout.write(buf)); + inp.write(${JSON.stringify(line + "\n")}); + inp.end(); + `; + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", script], + env: { ...bunEnv, NO_COLOR: "1" }, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + // The frozen .stack can't be trimmed under JSC (eager materialization), so + // assert only on the first line and that the REPL printed the next prompt. + expect(stdout.split("\n")[0]).toBe(expectedFirstLine); + expect(stdout).not.toContain("Attempted to assign to readonly property"); + expect(stderr).not.toContain("Attempted to assign to readonly property"); + expect(exitCode).toBe(0); + }, + interactiveTimeout, + ); +}); diff --git a/test/js/node/async_hooks/AsyncLocalStorage.test.ts b/test/js/node/async_hooks/AsyncLocalStorage.test.ts index 48e49dbdb44a..b6dc51aa113e 100644 --- a/test/js/node/async_hooks/AsyncLocalStorage.test.ts +++ b/test/js/node/async_hooks/AsyncLocalStorage.test.ts @@ -1132,6 +1132,34 @@ describe("async context passes through", () => { expect(stderr).not.toContain("AssertionError"); }); + // run()'s same-value short-circuit must not spread its rest args, or a + // tampered Array.prototype[Symbol.iterator] breaks it. The main path is + // already covered by test-repl-array-prototype-tempering.js. + test("run() short-circuit survives a deleted Array.prototype[Symbol.iterator]", async () => { + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + ` + const { AsyncLocalStorage } = require("async_hooks"); + require("node:util"); + const als = new AsyncLocalStorage(); + als.enterWith("v"); + delete Array.prototype[Symbol.iterator]; + console.log(als.run("v", (a, b) => a + "/" + b, "x", "y")); + `, + ], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).not.toContain("Spread syntax"); + expect(stderr).not.toContain("is not iterable"); + expect(stdout.trim()).toBe("x/y"); + expect(exitCode).toBe(0); + }); + test("Bun.build plugin", async () => { const s = new AsyncLocalStorage(); let a = undefined; diff --git a/test/js/node/readline/readline.node.test.ts b/test/js/node/readline/readline.node.test.ts index 32a07080a60d..26b97be82a69 100644 --- a/test/js/node/readline/readline.node.test.ts +++ b/test/js/node/readline/readline.node.test.ts @@ -2063,6 +2063,21 @@ describe("readline.createInterface()", () => { assert.strictEqual(closed, true); }); + it("Symbol.dispose method is named '[Symbol.dispose]' (a string, as Node's assignFunctionName produces)", () => { + const fn = readline.Interface.prototype[Symbol.dispose]; + // Node names it via assignFunctionName(SymbolDispose, fn), which stringifies the + // Symbol to `[${description}]`. A raw Symbol here throws on any coercion of .name. + assert.strictEqual(typeof fn.name, "string"); + assert.strictEqual(fn.name, "[Symbol.dispose]"); + assert.strictEqual(`${fn.name}`, "[Symbol.dispose]"); + assert.deepStrictEqual(Object.getOwnPropertyDescriptor(fn, "name"), { + value: "[Symbol.dispose]", + writable: false, + enumerable: false, + configurable: true, + }); + }); + it("should support Symbol.dispose as alias for close()", () => { const input = new PassThrough(); const output = new PassThrough(); diff --git a/test/js/node/readline/readline_never_unrefs.test.ts b/test/js/node/readline/readline_never_unrefs.test.ts index 01dd43659dee..36ff28146063 100644 --- a/test/js/node/readline/readline_never_unrefs.test.ts +++ b/test/js/node/readline/readline_never_unrefs.test.ts @@ -5,7 +5,8 @@ test("readline should unref", () => { cmd: [bunExe(), import.meta.dir + "/readline_never_unrefs.js"], env: bunEnv, stdio: ["inherit", "pipe", "pipe"], - timeout: 1000, + // Loading the v26 readline stack alone is ~3s under debug+asan. + timeout: 10_000, }); expect(res.exitCode).toBe(0); }); diff --git a/test/js/node/test/fixtures/repl-load-multiline-no-trailing-newline.js b/test/js/node/test/fixtures/repl-load-multiline-no-trailing-newline.js new file mode 100644 index 000000000000..605d49e2d051 --- /dev/null +++ b/test/js/node/test/fixtures/repl-load-multiline-no-trailing-newline.js @@ -0,0 +1,7 @@ +// The lack of a newline at the end of this file is intentional. +const getLunch = () => + placeOrder('tacos') + .then(eat); + +const placeOrder = (order) => Promise.resolve(order); +const eat = (food) => ''; \ No newline at end of file diff --git a/test/js/node/test/fixtures/repl-load-multiline.js b/test/js/node/test/fixtures/repl-load-multiline.js new file mode 100644 index 000000000000..faedf4ee07d1 --- /dev/null +++ b/test/js/node/test/fixtures/repl-load-multiline.js @@ -0,0 +1,6 @@ +const getLunch = () => + placeOrder('tacos') + .then(eat); + +const placeOrder = (order) => Promise.resolve(order); +const eat = (food) => ''; diff --git a/test/js/node/test/fixtures/repl-tab-completion-nested-repls.js b/test/js/node/test/fixtures/repl-tab-completion-nested-repls.js new file mode 100644 index 000000000000..79677491eca5 --- /dev/null +++ b/test/js/node/test/fixtures/repl-tab-completion-nested-repls.js @@ -0,0 +1,44 @@ +// Tab completion sometimes uses a separate REPL instance under the hood. +// Make sure errors in completion callbacks are properly thrown. +// +// Ref: https://github.com/nodejs/node/issues/21586 + +'use strict'; + +const { Stream } = require('stream'); +function noop() {} + +// A stream to push an array into a REPL +function ArrayStream() { + this.run = function(data) { + data.forEach((line) => { + this.emit('data', `${line}\n`); + }); + }; +} + +Object.setPrototypeOf(ArrayStream.prototype, Stream.prototype); +Object.setPrototypeOf(ArrayStream, Stream); +ArrayStream.prototype.readable = true; +ArrayStream.prototype.writable = true; +ArrayStream.prototype.pause = noop; +ArrayStream.prototype.resume = noop; +ArrayStream.prototype.write = noop; + +const repl = require('repl'); + +const putIn = new ArrayStream(); +const testMe = repl.start('', putIn); + +// Nesting of structures causes REPL to use a nested REPL for completion. +putIn.run([ + 'var top = function() {', + 'r = function test (', + ' one, two) {', + 'var inner = {', + ' one:1', + '};' +]); + +// In Node.js 10.11.0, this next line will terminate the repl silently... +testMe.complete('inner.o', () => { throw new Error('fhqwhgads'); }); \ No newline at end of file diff --git a/test/js/node/test/parallel/test-readline-line-separators.js b/test/js/node/test/parallel/test-readline-line-separators.js new file mode 100644 index 000000000000..7591f50c7fa1 --- /dev/null +++ b/test/js/node/test/parallel/test-readline-line-separators.js @@ -0,0 +1,18 @@ +'use strict'; +const common = require('../common'); +const assert = require('node:assert'); +const readline = require('node:readline'); +const { Readable } = require('node:stream'); + +const str = '012\n345\r67\r\n89\u{2028}ABC\u{2029}DEF'; + +const rli = new readline.Interface({ + input: Readable.from(str), +}); + +const linesRead = []; +rli.on('line', (line) => linesRead.push(line)); + +rli.on('close', common.mustCall(() => { + assert.deepStrictEqual(linesRead, ['012', '345', '67', '89', 'ABC', 'DEF']); +})); diff --git a/test/js/node/test/parallel/test-readline-promises-tab-complete.js b/test/js/node/test/parallel/test-readline-promises-tab-complete.js index 3d1c3e419c0c..954673f93440 100644 --- a/test/js/node/test/parallel/test-readline-promises-tab-complete.js +++ b/test/js/node/test/parallel/test-readline-promises-tab-complete.js @@ -71,14 +71,20 @@ common.skipIfDumbTerminal(); const expectations = [char, '', last]; rli.on('line', common.mustNotCall()); - for (const character of `${char}\t\t`) { - fi.emit('data', character); - queueMicrotask(() => { + // bun: upstream interleaves per-emit queueMicrotask asserts and ends + // with fi.end(); JSC settles the await chain across more microtask + // turns than V8, so wait a macrotask per emit instead. rli.close() + // here would race the in-flight completion (ERR_USE_AFTER_CLOSE) - + // upstream's fi.end() is a no-op on the fake input. + common.mustCall(async () => { + for (const character of `${char}\t\t`) { + fi.emit('data', character); + await new Promise((resolve) => setImmediate(resolve)); assert.strictEqual(output, expectations.shift()); output = ''; - }); - } - rli.close(); + } + fi.end(); + })(); }); }); }); @@ -108,9 +114,9 @@ common.skipIfDumbTerminal(); rli.on('line', common.mustNotCall()); fi.emit('data', '\t'); - queueMicrotask(() => { + setImmediate(common.mustCall(() => { assert.match(output, /^Tab completion error:[^]+Error: message/i); output = ''; - }); - rli.close(); + fi.end(); + })); } diff --git a/test/js/node/test/parallel/test-readline-tab-complete.js b/test/js/node/test/parallel/test-readline-tab-complete.js index fb5c410368da..d8b4c80584c0 100644 --- a/test/js/node/test/parallel/test-readline-tab-complete.js +++ b/test/js/node/test/parallel/test-readline-tab-complete.js @@ -95,7 +95,7 @@ common.skipIfDumbTerminal(); rli.on('line', common.mustNotCall()); fi.emit('data', '\t'); queueMicrotask(() => { - assert.match(output, /^Tab completion error:[^]+error: message/); // modified to match bun's error message + assert.match(output, /^Tab completion error: Error: message/); output = ''; }); rli.close(); diff --git a/test/js/node/test/parallel/test-repl-array-prototype-tempering.js b/test/js/node/test/parallel/test-repl-array-prototype-tempering.js new file mode 100644 index 000000000000..907a6396ebea --- /dev/null +++ b/test/js/node/test/parallel/test-repl-array-prototype-tempering.js @@ -0,0 +1,66 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { spawn } = require('child_process'); + +const replProcess = spawn(process.argv0, ['--interactive'], { + stdio: ['pipe', 'pipe', 'inherit'], + windowsHide: true, +}); + +replProcess.on('error', common.mustNotCall()); + +const replReadyState = (async function* () { + let ready; + const SPACE = ' '.charCodeAt(); + const BRACKET = '>'.charCodeAt(); + const DOT = '.'.charCodeAt(); + replProcess.stdout.on('data', (data) => { + ready = data[data.length - 1] === SPACE && ( + data[data.length - 2] === BRACKET || ( + data[data.length - 2] === DOT && + data[data.length - 3] === DOT && + data[data.length - 4] === DOT + )); + }); + + const processCrashed = new Promise((resolve, reject) => + replProcess.on('exit', reject) + ); + while (true) { + await Promise.race([new Promise(setImmediate), processCrashed]); + if (ready) { + ready = false; + yield; + } + } +})(); +async function writeLn(data, expectedOutput) { + await replReadyState.next(); + if (expectedOutput) { + replProcess.stdout.once('data', common.mustCall((data) => + assert.match(data.toString('utf8'), expectedOutput) + )); + } + await new Promise((resolve, reject) => replProcess.stdin.write( + `${data}\n`, + (err) => (err ? reject(err) : resolve()) + )); +} + +async function main() { + await writeLn( + 'Object.defineProperty(Array.prototype, "-1", ' + + '{ get() { return this[this.length - 1]; } });' + ); + + await writeLn( + '[3, 2, 1][-1];', + /^1\n(>\s)?$/ + ); + await writeLn('.exit'); + + assert(!replProcess.connected); +} + +main().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-repl-async-iife.js b/test/js/node/test/parallel/test-repl-async-iife.js new file mode 100644 index 000000000000..e1f5ca78128a --- /dev/null +++ b/test/js/node/test/parallel/test-repl-async-iife.js @@ -0,0 +1,10 @@ +'use strict'; +require('../common'); + +// Note: This test ensures that async IIFE doesn't crash +// Ref: https://github.com/nodejs/node/issues/38685 + +const repl = require('repl').start({ terminal: true }); + +repl.write('(async() => { })()\n'); +repl.write('.exit\n'); diff --git a/test/js/node/test/parallel/test-repl-autocomplete.js b/test/js/node/test/parallel/test-repl-autocomplete.js new file mode 100644 index 000000000000..568ea7b9ab4c --- /dev/null +++ b/test/js/node/test/parallel/test-repl-autocomplete.js @@ -0,0 +1,219 @@ +'use strict'; + +// Flags: --expose-internals + +const common = require('../common'); +const stream = require('stream'); +const REPL = require('internal/repl'); +const assert = require('assert'); +const fs = require('fs'); +const { inspect } = require('util'); + +if (process.env.TERM === 'dumb') { + common.skip('skipping - dumb terminal'); +} + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +process.throwDeprecation = true; + +const defaultHistoryPath = tmpdir.resolve('.node_repl_history'); + +// Create an input stream specialized for testing an array of actions +class ActionStream extends stream.Stream { + run(data) { + const _iter = data[Symbol.iterator](); + const doAction = () => { + const next = _iter.next(); + if (next.done) { + // Close the repl. Note that it must have a clean prompt to do so. + this.emit('keypress', '', { ctrl: true, name: 'd' }); + return; + } + const action = next.value; + + if (typeof action === 'object') { + this.emit('keypress', '', action); + } else { + this.emit('data', `${action}`); + } + setImmediate(doAction); + }; + doAction(); + } + resume() {} + pause() {} +} +ActionStream.prototype.readable = true; + +// Mock keys +const ENTER = { name: 'enter' }; +const UP = { name: 'up' }; +const DOWN = { name: 'down' }; +const LEFT = { name: 'left' }; +const RIGHT = { name: 'right' }; +const BACKSPACE = { name: 'backspace' }; +const TABULATION = { name: 'tab' }; +const WORD_LEFT = { name: 'left', ctrl: true }; +const WORD_RIGHT = { name: 'right', ctrl: true }; +const GO_TO_END = { name: 'end' }; +const SIGINT = { name: 'c', ctrl: true }; +const ESCAPE = { name: 'escape', meta: true }; + +const prompt = '> '; + +const tests = [ + { + env: { NODE_REPL_HISTORY: defaultHistoryPath }, + test: (function*() { + // Deleting Array iterator should not break history feature. + // + // Using a generator function instead of an object to allow the test to + // keep iterating even when Array.prototype[Symbol.iterator] has been + // deleted. + yield 'const ArrayIteratorPrototype ='; + yield ' Object.getPrototypeOf(Array.prototype[Symbol.iterator]());'; + yield ENTER; + yield 'const {next} = ArrayIteratorPrototype;'; + yield ENTER; + yield 'const realArrayIterator = Array.prototype[Symbol.iterator];'; + yield ENTER; + yield 'delete Array.prototype[Symbol.iterator];'; + yield ENTER; + yield 'delete ArrayIteratorPrototype.next;'; + yield ENTER; + yield UP; + yield UP; + yield DOWN; + yield DOWN; + yield 'fu'; + yield 'n'; + yield RIGHT; + yield BACKSPACE; + yield LEFT; + yield LEFT; + yield 'A'; + yield BACKSPACE; + yield GO_TO_END; + yield BACKSPACE; + yield WORD_LEFT; + yield WORD_RIGHT; + yield ESCAPE; + yield ENTER; + yield 'require("./'; + yield TABULATION; + yield SIGINT; + yield 'import("./'; + yield TABULATION; + yield SIGINT; + yield 'Array.proto'; + yield RIGHT; + yield '.pu'; + yield ENTER; + yield 'ArrayIteratorPrototype.next = next;'; + yield ENTER; + yield 'Array.prototype[Symbol.iterator] = realArrayIterator;'; + yield ENTER; + })(), + expected: [], + clean: false + }, +]; +const numtests = tests.length; + +const runTestWrap = common.mustCall(runTest, numtests); + +function cleanupTmpFile() { + try { + // Write over the file, clearing any history + fs.writeFileSync(defaultHistoryPath, ''); + } catch (err) { + if (err.code === 'ENOENT') return true; + throw err; + } + return true; +} + +function runTest() { + const opts = tests.shift(); + if (!opts) return; // All done + + const { expected, skip } = opts; + + // Test unsupported on platform. + if (skip) { + setImmediate(runTestWrap, true); + return; + } + const lastChunks = []; + let i = 0; + + REPL.createInternalRepl(opts.env, { + input: new ActionStream(), + output: new stream.Writable({ + write: common.mustCallAtLeast((chunk, _, next) => { + const output = chunk.toString(); + + if (!opts.showEscapeCodes && + (output[0] === '\x1B' || /^[\r\n]+$/.test(output))) { + return next(); + } + + lastChunks.push(output); + + if (expected.length && !opts.checkTotal) { + try { + assert.strictEqual(output, expected[i]); + } catch (e) { + console.error(`Failed test # ${numtests - tests.length}`); + console.error('Last outputs: ' + inspect(lastChunks, { + breakLength: 5, colors: true + })); + throw e; + } + // bun: upstream-todo(BridgeAR): Auto close on last chunk! + i++; + } + + next(); + }), + }), + allowBlockingCompletions: true, + completer: opts.completer, + prompt, + useColors: false, + preview: opts.preview, + terminal: true + }, common.mustCall((err, repl) => { + if (err) { + console.error(`Failed test # ${numtests - tests.length}`); + throw err; + } + + repl.once('close', common.mustCall(() => { + if (opts.clean) + cleanupTmpFile(); + + if (opts.checkTotal) { + assert.deepStrictEqual(lastChunks, expected); + } else if (expected.length !== i) { + console.error(tests[numtests - tests.length - 1]); + throw new Error(`Failed test # ${numtests - tests.length}`); + } + + setImmediate(runTestWrap, true); + })); + + if (opts.columns) { + Object.defineProperty(repl, 'columns', { + value: opts.columns, + enumerable: true + }); + } + repl.input.run(opts.test); + })); +} + +// run the tests +runTest(); diff --git a/test/js/node/test/parallel/test-repl-autolibs.js b/test/js/node/test/parallel/test-repl-autolibs.js new file mode 100644 index 000000000000..d3ab60a1b1d0 --- /dev/null +++ b/test/js/node/test/parallel/test-repl-autolibs.js @@ -0,0 +1,70 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const ArrayStream = require('../common/arraystream'); +const { startNewREPLServer } = require('../common/repl'); +const assert = require('assert'); +const util = require('util'); + +const putIn = new ArrayStream(); +startNewREPLServer({ input: putIn, output: putIn, useGlobal: true, terminal: false }); + +test1(); + +function test1() { + let gotWrite = false; + putIn.write = common.mustCall(function(data) { + gotWrite = true; + if (data.length) { + + // Inspect output matches repl output + assert.strictEqual(data, + `${util.inspect(require('fs'), null, 2, false)}\n`); + // Globally added lib matches required lib + assert.strictEqual(globalThis.fs, require('fs')); + test2(); + } + }); + assert(!gotWrite); + putIn.run(['fs']); + assert(gotWrite); +} + +function test2() { + let gotWrite = false; + putIn.write = common.mustCallAtLeast(function(data) { + gotWrite = true; + if (data.length) { + // REPL response error message + assert.strictEqual(data, '{}\n'); + // Original value wasn't overwritten + assert.strictEqual(val, globalThis.url); + } + }); + const val = {}; + globalThis.url = val; + common.allowGlobals(val); + assert(!gotWrite); + putIn.run(['url']); + assert(gotWrite); +} diff --git a/test/js/node/test/parallel/test-repl-clear-immediate-crash.js b/test/js/node/test/parallel/test-repl-clear-immediate-crash.js index ce8aaf48e7fa..252efa99e655 100644 --- a/test/js/node/test/parallel/test-repl-clear-immediate-crash.js +++ b/test/js/node/test/parallel/test-repl-clear-immediate-crash.js @@ -4,7 +4,9 @@ const child_process = require('child_process'); const assert = require('assert'); // Regression test for https://github.com/nodejs/node/issues/37806: -const proc = child_process.spawn(process.execPath, ['-i']); +// bun: upstream spawns with '-i'; in bun that short flag is already taken by +// --install=fallback, so the REPL is reached through the long form. +const proc = child_process.spawn(process.execPath, ['--interactive']); proc.on('error', common.mustNotCall()); proc.on('exit', common.mustCall((code) => { assert.strictEqual(code, 0); diff --git a/test/js/node/test/parallel/test-repl-cli-eval.js b/test/js/node/test/parallel/test-repl-cli-eval.js new file mode 100644 index 000000000000..6069a20957bd --- /dev/null +++ b/test/js/node/test/parallel/test-repl-cli-eval.js @@ -0,0 +1,22 @@ +'use strict'; +const common = require('../common'); +const child_process = require('child_process'); +const assert = require('assert'); + +// Regression test for https://github.com/nodejs/node/issues/27575: +// module.id === '' in the REPL. + +for (const extraFlags of [[], ['-e', '42']]) { + const flags = ['--interactive', ...extraFlags]; + const proc = child_process.spawn(process.execPath, flags, { + stdio: ['pipe', 'pipe', 'inherit'] + }); + proc.stdin.write('module.id\n.exit\n'); + + let stdout = ''; + proc.stdout.setEncoding('utf8'); + proc.stdout.on('data', (chunk) => stdout += chunk); + proc.stdout.on('end', common.mustCall(() => { + assert(stdout.includes(''), `stdout: ${stdout}`); + })); +} diff --git a/test/js/node/test/parallel/test-repl-colors.js b/test/js/node/test/parallel/test-repl-colors.js new file mode 100644 index 000000000000..6d46c6ef908f --- /dev/null +++ b/test/js/node/test/parallel/test-repl-colors.js @@ -0,0 +1,36 @@ +'use strict'; +// bun: ci sets process.env["FORCE_COLOR"], which makes the test fail in both node and bun +delete process.env["FORCE_COLOR"]; + +require('../common'); +const { Duplex } = require('stream'); +const { inspect } = require('util'); +const assert = require('assert'); +const { REPLServer } = require('repl'); + +let output = ''; + +const inout = new Duplex({ decodeStrings: false }); +inout._read = function() { + this.push('util.inspect("string")\n'); + this.push(null); +}; +inout._write = function(s, _, cb) { + output += s; + cb(); +}; + +const repl = new REPLServer({ input: inout, output: inout, useColors: true }); +inout.isTTY = true; +const repl2 = new REPLServer({ input: inout, output: inout }); + +process.on('exit', function() { + // https://github.com/nodejs/node/pull/16485#issuecomment-350428638 + // The color setting of the REPL should not have leaked over into + // the color setting of `util.inspect.defaultOptions`. + assert.strictEqual(output.includes(`"'string'"`), true); + assert.strictEqual(output.includes(`'\u001b[32m\\'string\\'\u001b[39m'`), false); + assert.strictEqual(inspect.defaultOptions.colors, false); + assert.strictEqual(repl.writer.options.colors, true); + assert.strictEqual(repl2.writer.options.colors, true); +}); diff --git a/test/js/node/test/parallel/test-repl-completion-on-getters-disabled.js b/test/js/node/test/parallel/test-repl-completion-on-getters-disabled.js new file mode 100644 index 000000000000..a2516f0ed00e --- /dev/null +++ b/test/js/node/test/parallel/test-repl-completion-on-getters-disabled.js @@ -0,0 +1,188 @@ +'use strict'; + +const common = require('../common'); +const assert = require('node:assert'); +const { describe, test } = require('node:test'); + +const { startNewREPLServer } = require('../common/repl'); + +function runCompletionTests(replInit, tests) { + const { replServer: testRepl, input } = startNewREPLServer(); + input.run([replInit]); + + tests.forEach(([query, expectedCompletions]) => { + testRepl.complete(query, common.mustCall((error, data) => { + const actualCompletions = data[0]; + if (expectedCompletions.length === 0) { + assert.deepStrictEqual(actualCompletions, []); + } else { + expectedCompletions.forEach((expectedCompletion) => + assert(actualCompletions.includes(expectedCompletion), `completion '${expectedCompletion}' not found`) + ); + } + })); + }); +} + +describe('REPL completion in relation of getters', () => { + describe('standard behavior without proxies/getters', () => { + test('completion of nested properties of an undeclared objects', () => { + runCompletionTests('', [ + ['nonExisting.', []], + ['nonExisting.f', []], + ['nonExisting.foo', []], + ['nonExisting.foo.', []], + ['nonExisting.foo.bar.b', []], + ]); + }); + + test('completion of nested properties on plain objects', () => { + runCompletionTests('const plainObj = { foo: { bar: { baz: {} } } };', [ + ['plainObj.', ['plainObj.foo']], + ['plainObj.f', ['plainObj.foo']], + ['plainObj.foo', ['plainObj.foo']], + ['plainObj.foo.', ['plainObj.foo.bar']], + ['plainObj.foo.bar.b', ['plainObj.foo.bar.baz']], + ['plainObj.fooBar.', []], + ['plainObj.fooBar.baz', []], + ]); + }); + }); + + describe('completions on an object with getters', () => { + test(`completions are generated for properties that don't trigger getters`, () => { + runCompletionTests( + ` + const fooKey = "foo"; + + const keys = { + "foo key": "foo", + }; + + const objWithGetters = { + foo: { bar: { baz: { buz: {} } }, get gBar() { return { baz: {} } } }, + get gFoo() { return { bar: { baz: {} } }; } + }; + `, [ + ['objWithGetters.', ['objWithGetters.foo']], + ['objWithGetters.f', ['objWithGetters.foo']], + ['objWithGetters.foo', ['objWithGetters.foo']], + ['objWithGetters["foo"].b', ['objWithGetters["foo"].bar']], + ['objWithGetters.foo.', ['objWithGetters.foo.bar']], + ['objWithGetters.foo.bar.b', ['objWithGetters.foo.bar.baz']], + ['objWithGetters.gFo', ['objWithGetters.gFoo']], + ['objWithGetters.foo.gB', ['objWithGetters.foo.gBar']], + ["objWithGetters.foo['bar'].b", ["objWithGetters.foo['bar'].baz"]], + ["objWithGetters['foo']['bar'].b", ["objWithGetters['foo']['bar'].baz"]], + ["objWithGetters['foo']['bar']['baz'].b", ["objWithGetters['foo']['bar']['baz'].buz"]], + ["objWithGetters[keys['foo key']].b", ["objWithGetters[keys['foo key']].bar"]], + ['objWithGetters[fooKey].b', ['objWithGetters[fooKey].bar']], + ["objWithGetters['f' + 'oo'].b", ["objWithGetters['f' + 'oo'].bar"]], + ]); + }); + + test('no completions are generated for properties that trigger getters', () => { + runCompletionTests( + ` + function getGFooKey() { + return "g" + "Foo"; + } + + const gFooKey = "gFoo"; + + const keys = { + "g-foo key": "gFoo", + }; + + const objWithGetters = { + foo: { bar: { baz: {} }, get gBar() { return { baz: {}, get gBuz() { return 5; } } } }, + get gFoo() { return { bar: { baz: {} } }; } + }; + `, + [ + ['objWithGetters.gFoo.', []], + ['objWithGetters.gFoo.b', []], + ['objWithGetters["gFoo"].b', []], + ['objWithGetters.gFoo.bar.b', []], + ['objWithGetters.foo.gBar.', []], + ['objWithGetters.foo.gBar.b', []], + ["objWithGetters.foo['gBar'].b", []], + ["objWithGetters['foo']['gBar'].b", []], + ["objWithGetters['foo']['gBar']['gBuz'].", []], + ["objWithGetters[keys['g-foo key']].b", []], + ['objWithGetters[gFooKey].b', []], + ["objWithGetters['g' + 'Foo'].b", []], + ['objWithGetters[getGFooKey()].b', []], + ]); + }); + + test('no side effects are triggered for getters during completion', async () => { + const { replServer } = startNewREPLServer(); + + await new Promise((resolve, reject) => { + replServer.eval('const foo = { get name() { globalThis.nameGetterRun = true; throw new Error(); } };', + replServer.context, '', (err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + + ['foo.name.', 'foo["name"].'].forEach((test) => { + replServer.complete( + test, + common.mustCall((error, data) => { + // The context's nameGetterRun variable hasn't been set + assert.strictEqual(replServer.context.nameGetterRun, undefined); + // No errors has been thrown + assert.strictEqual(error, null); + }) + ); + }); + }); + }); + + describe('completions on proxies', () => { + test('no completions are generated for a proxy object', () => { + runCompletionTests( + ` + function getFooKey() { + return "foo"; + } + + const fooKey = "foo"; + + const keys = { + "foo key": "foo", + }; + + const proxyObj = new Proxy({ foo: { bar: { baz: {} } } }, {}); + `, [ + ['proxyObj.', []], + ['proxyObj.f', []], + ['proxyObj.foo', []], + ['proxyObj.foo.', []], + ['proxyObj.["foo"].', []], + ['proxyObj.["f" + "oo"].', []], + ['proxyObj.[fooKey].', []], + ['proxyObj.[getFooKey()].', []], + ['proxyObj.[keys["foo key"]].', []], + ['proxyObj.foo.bar.b', []], + ]); + }); + + test('no completions are generated for a proxy present in a standard object', () => { + runCompletionTests( + 'const objWithProxy = { foo: { bar: new Proxy({ baz: {} }, {}) } };', [ + ['objWithProxy.', ['objWithProxy.foo']], + ['objWithProxy.foo', ['objWithProxy.foo']], + ['objWithProxy.foo.', ['objWithProxy.foo.bar']], + ['objWithProxy.foo.b', ['objWithProxy.foo.bar']], + ['objWithProxy.foo.bar.', []], + ['objWithProxy.foo["b" + "ar"].', []], + ]); + }); + }); +}); diff --git a/test/js/node/test/parallel/test-repl-context.js b/test/js/node/test/parallel/test-repl-context.js new file mode 100644 index 000000000000..97847aac6db3 --- /dev/null +++ b/test/js/node/test/parallel/test-repl-context.js @@ -0,0 +1,76 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const vm = require('vm'); +const { startNewREPLServer } = require('../common/repl'); + +// Test context when useGlobal is false. +{ + const { replServer, output } = startNewREPLServer({ + terminal: false, + useGlobal: false + }); + + // Ensure that the repl context gets its own "console" instance. + assert(replServer.context.console); + + // Ensure that the repl console instance is not the global one. + assert.notStrictEqual(replServer.context.console, console); + assert.notStrictEqual(replServer.context.Object, Object); + + replServer.write('({} instanceof Object)\n'); + + assert.strictEqual(output.accumulator, 'true\n'); + + const context = replServer.createContext(); + // Ensure that the repl context gets its own "console" instance. + assert(context.console instanceof require('console').Console); + + // Ensure that the repl's global property is the context. + assert.strictEqual(context.global, context); + + // Ensure that the repl console instance is writable. + context.console = 'foo'; + replServer.close(); +} + +// Test for context side effects. +{ + const { replServer } = startNewREPLServer({ + useGlobal: false + }); + + assert.ok(!replServer.underscoreAssigned); + assert.strictEqual(replServer.lines.length, 0); + + // An assignment to '_' in the repl server + replServer.write('_ = 500;\n'); + assert.ok(replServer.underscoreAssigned); + assert.strictEqual(replServer.lines.length, 1); + assert.strictEqual(replServer.lines[0], '_ = 500;'); + assert.strictEqual(replServer.last, 500); + + // Use the server to create a new context + const context = replServer.createContext(); + + // Ensure that creating a new context does not + // have side effects on the server + assert.ok(replServer.underscoreAssigned); + assert.strictEqual(replServer.lines.length, 1); + assert.strictEqual(replServer.lines[0], '_ = 500;'); + assert.strictEqual(replServer.last, 500); + + // Reset the server context + replServer.resetContext(); + assert.ok(!replServer.underscoreAssigned); + assert.strictEqual(replServer.lines.length, 0); + + // Ensure that assigning to '_' in the new context + // does not change the value in our server. + assert.ok(!replServer.underscoreAssigned); + vm.runInContext('_ = 1000;\n', context); + + assert.ok(!replServer.underscoreAssigned); + assert.strictEqual(replServer.lines.length, 0); + replServer.close(); +} diff --git a/test/js/node/test/parallel/test-repl-custom-eval.js b/test/js/node/test/parallel/test-repl-custom-eval.js new file mode 100644 index 000000000000..190e1c573aa1 --- /dev/null +++ b/test/js/node/test/parallel/test-repl-custom-eval.js @@ -0,0 +1,111 @@ +'use strict'; + +require('../common'); +const { startNewREPLServer } = require('../common/repl'); +const assert = require('assert'); +const { describe, it } = require('node:test'); + +const testingReplPrompt = '_REPL_TESTING_PROMPT_>'; + +// Processes some input in a REPL instance and returns a promise that +// resolves to the produced output (as a string). +function getReplRunOutput(inputStr, replOptions) { + return new Promise((resolve) => { + const { replServer, input, output } = startNewREPLServer({ prompt: testingReplPrompt, ...replOptions }); + + output.accumulator = ''; + + output.write = (chunk) => { + output.accumulator += chunk; + // The prompt appears after the input has been processed + if (output.accumulator.includes(testingReplPrompt)) { + replServer.close(); + resolve(output.accumulator); + } + }; + + input.emit('data', inputStr); + + input.run(['']); + }); +} + +describe('repl with custom eval', { concurrency: true }, () => { + it('uses the custom eval function as expected', async () => { + const output = await getReplRunOutput('Convert this to upper case', { + terminal: true, + eval: (code, _ctx, _replRes, cb) => cb(null, code.toUpperCase()), + }); + assert.match( + output, + /Convert this to upper case\r\n'CONVERT THIS TO UPPER CASE\\n'/ + ); + }); + + it('surfaces errors as expected', async () => { + const output = await getReplRunOutput('Convert this to upper case', { + terminal: true, + eval: (_code, _ctx, _replRes, cb) => cb(new Error('Testing Error')), + }); + assert.match(output, /Uncaught Error: Testing Error\n/); + }); + + it('provides a repl context to the eval callback', async () => { + const context = await new Promise((resolve) => { + const { replServer } = startNewREPLServer({ + eval: (_cmd, context) => resolve(context), + }); + replServer.context = { foo: 'bar' }; + replServer.write('\n.exit\n'); + }); + assert.strictEqual(context.foo, 'bar'); + }); + + it('provides the global context to the eval callback', async () => { + const context = await new Promise((resolve) => { + const { replServer } = startNewREPLServer({ + eval: (_cmd, context) => resolve(context), + useGlobal: true + }); + global.foo = 'global_foo'; + replServer.write('\n.exit\n'); + }); + + assert.strictEqual(context.foo, 'global_foo'); + delete global.foo; + }); + + it('inherits variables from the global context but does not use it afterwords if `useGlobal` is false', async () => { + global.bar = 'global_bar'; + const context = await new Promise((resolve) => { + const { replServer } = startNewREPLServer({ + useGlobal: false, + eval: (_cmd, context) => resolve(context), + }); + global.baz = 'global_baz'; + replServer.write('\n.exit\n'); + }); + + assert.strictEqual(context.bar, 'global_bar'); + assert.notStrictEqual(context.baz, 'global_baz'); + delete global.bar; + delete global.baz; + }); + + /** + * Default preprocessor transforms + * function f() {} to + * var f = function f() {} + * This test ensures that original input is preserved. + * Reference: https://github.com/nodejs/node/issues/9743 + */ + it('preserves the original input', async () => { + const cmd = await new Promise((resolve) => { + const { replServer } = startNewREPLServer({ + eval: (cmd) => resolve(cmd), + }); + replServer.write('function f() {}\n.exit\n'); + }); + assert.strictEqual(cmd, 'function f() {}\n'); + }); +}); diff --git a/test/js/node/test/parallel/test-repl-definecommand.js b/test/js/node/test/parallel/test-repl-definecommand.js new file mode 100644 index 000000000000..b54f99a5511f --- /dev/null +++ b/test/js/node/test/parallel/test-repl-definecommand.js @@ -0,0 +1,44 @@ +'use strict'; + +require('../common'); +const { startNewREPLServer } = require('../common/repl'); + +const stream = require('stream'); +const assert = require('assert'); + +let output = ''; +const outputStream = new stream.PassThrough(); +outputStream.on('data', function(d) { + output += d; +}); + +const { replServer: replServer, input } = startNewREPLServer({ prompt: '> ', terminal: true, output: outputStream }); + +replServer.defineCommand('say1', { + help: 'help for say1', + action: function(thing) { + output = ''; + this.output.write(`hello ${thing}\n`); + this.displayPrompt(); + } +}); + +replServer.defineCommand('say2', function() { + output = ''; + this.output.write('hello from say2\n'); + this.displayPrompt(); +}); + +input.run(['.help\n']); +assert.match(output, /\n\.say1 {5}help for say1\n/); +assert.match(output, /\n\.say2\n/); +input.run(['.say1 node developer\n']); +assert.ok(output.startsWith('hello node developer\n'), + `say1 output starts incorrectly: "${output}"`); +assert.ok(output.includes('> '), + `say1 output does not include prompt: "${output}"`); +input.run(['.say2 node developer\n']); +assert.ok(output.startsWith('hello from say2\n'), + `say2 output starts incorrectly: "${output}"`); +assert.ok(output.includes('> '), + `say2 output does not include prompt: "${output}"`); diff --git a/test/js/node/test/parallel/test-repl-editor.js b/test/js/node/test/parallel/test-repl-editor.js new file mode 100644 index 000000000000..8f41b3d4e704 --- /dev/null +++ b/test/js/node/test/parallel/test-repl-editor.js @@ -0,0 +1,115 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { startNewREPLServer } = require('../common/repl'); + +if (process.env.TERM === 'dumb') { + common.skip('skipping - dumb terminal'); +} + +// \u001b[nG - Moves the cursor to n st column +// \u001b[0J - Clear screen +// \u001b[0K - Clear to line end +const terminalCode = '\u001b[1G\u001b[0J> \u001b[3G'; +const terminalCodeRegex = new RegExp(terminalCode.replace(/\[/g, '\\['), 'g'); + +function run({ input: inputStr, output: outputStr, event, checkTerminalCodes = true }) { + let expected = + `${terminalCode}.editor\n` + + '// Entering editor mode (Ctrl+D to finish, Ctrl+C to cancel)\n' + + `${inputStr}${outputStr}\n${terminalCode}`; + + const { replServer, input, output } = startNewREPLServer({ + prompt: '> ', + terminal: true, + useColors: false + }); + + input.emit('data', '.editor\n'); + input.emit('data', inputStr); + replServer.write('', event); + replServer.close(); + + let found = output.accumulator; + if (!checkTerminalCodes) { + found = found.replace(terminalCodeRegex, '').replace(/\n/g, ''); + expected = expected.replace(terminalCodeRegex, '').replace(/\n/g, ''); + } + + assert.strictEqual(found, expected); +} + +const tests = [ + { + input: '', + output: '\n(To exit, press Ctrl+C again or Ctrl+D or type .exit)', + event: { ctrl: true, name: 'c' } + }, + { + input: 'let i = 1;', + output: '', + event: { ctrl: true, name: 'c' } + }, + { + input: 'let i = 1;\ni + 3', + output: '\n4', + event: { ctrl: true, name: 'd' } + }, + { + input: ' let i = 1;\ni + 3', + output: '\n4', + event: { ctrl: true, name: 'd' } + }, + { + input: '', + output: '', + checkTerminalCodes: false, + event: null, + }, +]; + +tests.forEach(run); + +// Auto code alignment for .editor mode +function testCodeAlignment({ input: inputStr, cursor = 0, line = '' }) { + const { replServer, input } = startNewREPLServer({ + prompt: '> ', + terminal: true, + useColors: false + }); + + input.emit('data', '.editor\n'); + inputStr.split('').forEach((ch) => input.emit('data', ch)); + // Test the content of current line and the cursor position + assert.strictEqual(line, replServer.line); + assert.strictEqual(cursor, replServer.cursor); + + replServer.write('', { ctrl: true, name: 'd' }); + replServer.close(); + // Ensure that empty lines are not saved in history + assert.notStrictEqual(replServer.history[0].trim(), ''); +} + +const codeAlignmentTests = [ + { + input: 'let i = 1;\n' + }, + { + input: ' let i = 1;\n', + cursor: 2, + line: ' ' + }, + { + input: ' let i = 1;\n', + cursor: 5, + line: ' ' + }, + { + input: ' let i = 1;\n let j = 2\n', + cursor: 2, + line: ' ' + }, +]; + +codeAlignmentTests.forEach(testCodeAlignment); diff --git a/test/js/node/test/parallel/test-repl-empty.js b/test/js/node/test/parallel/test-repl-empty.js new file mode 100644 index 000000000000..97d8c9bf05ce --- /dev/null +++ b/test/js/node/test/parallel/test-repl-empty.js @@ -0,0 +1,23 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { startNewREPLServer } = require('../common/repl'); + +let evalCalledWithExpectedArgs = false; + +const { replServer } = startNewREPLServer({ + eval: common.mustCall((cmd, context) => { + // Assertions here will not cause the test to exit with an error code + // so set a boolean that is checked later instead. + evalCalledWithExpectedArgs = (cmd === '\n'); + }) +}); + +try { + // Empty strings should be sent to the repl's eval function + replServer.write('\n'); +} finally { + replServer.write('.exit\n'); +} + +assert(evalCalledWithExpectedArgs); diff --git a/test/js/node/test/parallel/test-repl-end-emits-exit.js b/test/js/node/test/parallel/test-repl-end-emits-exit.js new file mode 100644 index 000000000000..1a869933dc5b --- /dev/null +++ b/test/js/node/test/parallel/test-repl-end-emits-exit.js @@ -0,0 +1,64 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const { startNewREPLServer } = require('../common/repl'); +const assert = require('assert'); +let terminalExit = 0; +let regularExit = 0; + +function testTerminalMode() { + const { replServer, input } = startNewREPLServer({ terminal: true }); + + process.nextTick(function() { + // Manually fire a ^D keypress + input.emit('data', '\u0004'); + }); + + replServer.on('exit', function() { + // Should be fired from the simulated ^D keypress + terminalExit++; + testRegularMode(); + }); +} + +function testRegularMode() { + const { replServer, input } = startNewREPLServer({ terminal: true }); + + process.nextTick(function() { + input.emit('end'); + }); + + replServer.on('exit', function() { + // Should be fired from the simulated 'end' event + regularExit++; + }); +} + +process.on('exit', function() { + assert.strictEqual(terminalExit, 1); + assert.strictEqual(regularExit, 1); +}); + + +// start +testTerminalMode(); diff --git a/test/js/node/test/parallel/test-repl-envvars.js b/test/js/node/test/parallel/test-repl-envvars.js new file mode 100644 index 000000000000..ba4de43b20f9 --- /dev/null +++ b/test/js/node/test/parallel/test-repl-envvars.js @@ -0,0 +1,87 @@ +'use strict'; + +// Flags: --expose-internals + +const common = require('../common'); +const stream = require('stream'); +const { describe, test } = require('node:test'); +const REPL = require('internal/repl'); +const assert = require('assert'); +const inspect = require('util').inspect; +const { REPL_MODE_SLOPPY, REPL_MODE_STRICT } = require('repl'); + +const tests = [ + { + env: {}, + expected: { terminal: true, useColors: false } + }, + { + env: { NODE_DISABLE_COLORS: '1' }, + expected: { terminal: true, useColors: false } + }, + { + env: { NODE_DISABLE_COLORS: '1', FORCE_COLOR: '1' }, + expected: { terminal: true, useColors: true } + }, + { + env: { NODE_NO_READLINE: '1' }, + expected: { terminal: false, useColors: false } + }, + { + env: { TERM: 'dumb' }, + expected: { terminal: true, useColors: false } + }, + { + env: { TERM: 'dumb', FORCE_COLOR: '1' }, + expected: { terminal: true, useColors: true } + }, + { + env: { NODE_NO_READLINE: '1', NODE_DISABLE_COLORS: '1' }, + expected: { terminal: false, useColors: false } + }, + { + env: { NODE_NO_READLINE: '0' }, + expected: { terminal: true, useColors: false } + }, + { + env: { NODE_REPL_MODE: 'sloppy' }, + expected: { terminal: true, useColors: false, replMode: REPL_MODE_SLOPPY } + }, + { + env: { NODE_REPL_MODE: 'strict' }, + expected: { terminal: true, useColors: false, replMode: REPL_MODE_STRICT } + }, +]; + +function run(test) { + const env = test.env; + const expected = test.expected; + + const opts = { + terminal: true, + input: new stream.Readable({ read() {} }), + output: new stream.Writable({ write() {} }) + }; + + Object.assign(process.env, env); + + return new Promise((resolve) => { + REPL.createInternalRepl(process.env, opts, common.mustSucceed((repl) => { + assert.strictEqual(repl.terminal, expected.terminal, + `Expected ${inspect(expected)} with ${inspect(env)}`); + assert.strictEqual(repl.useColors, expected.useColors, + `Expected ${inspect(expected)} with ${inspect(env)}`); + assert.strictEqual(repl.replMode, expected.replMode || REPL_MODE_SLOPPY, + `Expected ${inspect(expected)} with ${inspect(env)}`); + for (const key of Object.keys(env)) { + delete process.env[key]; + } + repl.close(); + resolve(); + })); + }); +} + +describe('REPL environment variables', { concurrency: 1 }, () => { + tests.forEach((testCase) => test(inspect(testCase.env), () => run(testCase))); +}); diff --git a/test/js/node/test/parallel/test-repl-eval-error-after-close.js b/test/js/node/test/parallel/test-repl-eval-error-after-close.js new file mode 100644 index 000000000000..0b4683fda4bf --- /dev/null +++ b/test/js/node/test/parallel/test-repl-eval-error-after-close.js @@ -0,0 +1,35 @@ +'use strict'; + +const common = require('../common'); +const { startNewREPLServer } = require('../common/repl'); +const assert = require('node:assert'); + +// This test checks that an eval function returning an error in its callback +// after the repl server has been closed doesn't cause an ERR_USE_AFTER_CLOSE +// error to be thrown (reference: https://github.com/nodejs/node/issues/58784) + +(async () => { + const close$ = Promise.withResolvers(); + const eval$ = Promise.withResolvers(); + + const { replServer, output } = startNewREPLServer({ + eval(_cmd, _context, _file, cb) { + // eslint-disable-next-line node-core/must-call-assert + close$.promise.then(() => { + cb(new Error('Error returned from the eval callback')); + eval$.resolve(); + }); + }, + }); + + replServer.write('\n'); + + replServer.close(); + close$.resolve(); + + process.on('uncaughtException', common.mustNotCall()); + + await eval$.promise; + + assert.match(output.accumulator, /Uncaught Error: Error returned from the eval callback/); +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-repl-function-definition-edge-case.js b/test/js/node/test/parallel/test-repl-function-definition-edge-case.js new file mode 100644 index 000000000000..4e73d9bcdd26 --- /dev/null +++ b/test/js/node/test/parallel/test-repl-function-definition-edge-case.js @@ -0,0 +1,19 @@ +// Reference: https://github.com/nodejs/node/pull/7624 +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { startNewREPLServer } = require('../common/repl'); + +const { replServer } = startNewREPLServer({ + useColors: false, + terminal: false +}); + +replServer.input.emit('data', 'function a() { return 42; } (1)\n'); +replServer.input.emit('data', 'a\n'); +replServer.input.emit('data', '.exit\n'); +replServer.once('exit', common.mustCall()); + +const expected = '1\n[Function: a]\n'; +const got = replServer.output.accumulator; +assert.strictEqual(got, expected); diff --git a/test/js/node/test/parallel/test-repl-harmony.js b/test/js/node/test/parallel/test-repl-harmony.js new file mode 100644 index 000000000000..b7c342114577 --- /dev/null +++ b/test/js/node/test/parallel/test-repl-harmony.js @@ -0,0 +1,52 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +const spawn = require('child_process').spawn; +// bun: upstream uses '-i'; that short flag is --install=fallback in bun, so the +// REPL is reached through the long form. +const args = ['--interactive']; +const child = spawn(process.execPath, args); + +const input = '(function(){"use strict"; const y=1;y=2})()\n'; +// This message will vary based on JavaScript engine, so don't check the message +// contents beyond confirming that the `Error` is a `TypeError`. +const expectOut = /> Uncaught TypeError: /; + +child.stderr.setEncoding('utf8'); +child.stderr.on('data', (d) => { + throw new Error('child.stderr be silent'); +}); + +child.stdout.setEncoding('utf8'); +let out = ''; +child.stdout.on('data', (d) => { + out += d; +}); +child.stdout.on('end', common.mustCall(() => { + assert.match(out, expectOut); + console.log('ok'); +})); + +child.stdin.end(input); diff --git a/test/js/node/test/parallel/test-repl-history-dedup-multiline.js b/test/js/node/test/parallel/test-repl-history-dedup-multiline.js new file mode 100644 index 000000000000..03dc089fa614 --- /dev/null +++ b/test/js/node/test/parallel/test-repl-history-dedup-multiline.js @@ -0,0 +1,44 @@ +'use strict'; + +const common = require('../common'); + +if (process.env.TERM === 'dumb') { + common.skip('skipping - dumb terminal'); +} + +const assert = require('assert'); +const readline = require('readline'); +const { EventEmitter } = require('events'); + +class FakeInput extends EventEmitter { + resume() {} + pause() {} + write() {} + end() {} +} +FakeInput.prototype.readable = true; + +{ + const fi = new FakeInput(); + const rli = new readline.Interface({ + input: fi, + output: fi, + terminal: true, + removeHistoryDuplicates: true, + }); + + function submitLine(line) { + rli.line = line; + fi.emit('keypress', '', { name: 'enter' }); + } + + submitLine('line1\nline2'); + submitLine('other'); + submitLine('line1\nline2'); + + assert.strictEqual(rli.history.length, 2); + assert.strictEqual(rli.history[0], 'line2\rline1'); + assert.strictEqual(rli.history[1], 'other'); + + rli.close(); +} diff --git a/test/js/node/test/parallel/test-repl-history-init-fail-leak.js b/test/js/node/test/parallel/test-repl-history-init-fail-leak.js new file mode 100644 index 000000000000..701a611ed2f3 --- /dev/null +++ b/test/js/node/test/parallel/test-repl-history-init-fail-leak.js @@ -0,0 +1,56 @@ +'use strict'; +// Flags: --expose-internals + +const common = require('../common'); +const fs = require('fs'); +const path = require('path'); +const { ReplHistory } = require('internal/repl/history'); +const assert = require('assert'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const historyPath = path.join(tmpdir.path, '.node_repl_history'); + +fs.writeFileSync(historyPath, 'dummy\n'); + +const originalOpen = fs.promises.open; +let closeCalled = false; + +fs.promises.open = async (filepath, flags, mode) => { + const handle = await originalOpen(filepath, flags, mode); + + if (flags === 'r+' && filepath === historyPath) { + handle.truncate = async (len) => { + throw new Error('Mock truncate error'); + }; + + const originalClose = handle.close; + handle.close = async () => { + closeCalled = true; + return originalClose.call(handle); + }; + } + + return handle; +}; + +const context = { + historySize: 30, + on: () => {}, + once: () => {}, + emit: () => {}, + pause: () => {}, + resume: () => {}, + off: () => {}, + line: '', + _historyPrev: () => {}, + _writeToOutput: () => {} +}; + +const history = new ReplHistory(context, { filePath: historyPath }); + +history.initialize(common.mustCall((err) => { + assert.strictEqual(err, null); + assert.strictEqual(closeCalled, true); +})); diff --git a/test/js/node/test/parallel/test-repl-history-perm.js b/test/js/node/test/parallel/test-repl-history-perm.js new file mode 100644 index 000000000000..1f33c2faf55a --- /dev/null +++ b/test/js/node/test/parallel/test-repl-history-perm.js @@ -0,0 +1,57 @@ +'use strict'; + +// Verifies that the REPL history file is created with mode 0600 + +// Flags: --expose-internals + +const common = require('../common'); + +if (common.isWindows) { + common.skip('Win32 uses ACLs for file permissions, ' + + 'modes are always 0666 and says nothing about group/other ' + + 'read access.'); +} + +const assert = require('assert'); +const fs = require('fs'); +const repl = require('internal/repl'); +const Duplex = require('stream').Duplex; +// Invoking the REPL should create a repl history file at the specified path +// and mode 600. + +const stream = new Duplex(); +stream.pause = stream.resume = () => {}; +// ends immediately +stream._read = function() { + this.push(null); +}; +stream._write = function(c, e, cb) { + cb(); +}; +stream.readable = stream.writable = true; + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); +const replHistoryPath = tmpdir.resolve('.node_repl_history'); + +const checkResults = common.mustSucceed((r) => { + const stat = fs.statSync(replHistoryPath); + const fileMode = stat.mode & 0o777; + assert.strictEqual( + fileMode, 0o600, + `REPL history file should be mode 0600 but was 0${fileMode.toString(8)}`); + + // Close the REPL + r.input.emit('keypress', '', { ctrl: true, name: 'd' }); + r.input.end(); +}); + +repl.createInternalRepl( + { NODE_REPL_HISTORY: replHistoryPath }, + { + terminal: true, + input: stream, + output: stream + }, + checkResults +); diff --git a/test/js/node/test/parallel/test-repl-inspect-defaults.js b/test/js/node/test/parallel/test-repl-inspect-defaults.js new file mode 100644 index 000000000000..966ee21ec09d --- /dev/null +++ b/test/js/node/test/parallel/test-repl-inspect-defaults.js @@ -0,0 +1,31 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const cp = require('child_process'); +// bun: upstream uses '-i'; that short flag is --install=fallback in bun, so the +// REPL is reached through the long form. +const child = cp.spawn(process.execPath, ['--interactive']); +let output = ''; + +child.stdout.setEncoding('utf8'); +child.stdout.on('data', (data) => { + output += data; +}); + +child.on('exit', common.mustCall(() => { + const results = output.replace(/^> /mg, '').split('\n').slice(2); + assert.deepStrictEqual( + results, + [ + '[ 42, 23 ]', + '1', + '[ 42, ... 1 more item ]', + '', + ] + ); +})); + +child.stdin.write('[ 42, 23 ]\n'); +child.stdin.write('util.inspect.replDefaults.maxArrayLength = 1\n'); +child.stdin.write('[ 42, 23 ]\n'); +child.stdin.end(); diff --git a/test/js/node/test/parallel/test-repl-let-process.js b/test/js/node/test/parallel/test-repl-let-process.js new file mode 100644 index 000000000000..22b57ab5bb97 --- /dev/null +++ b/test/js/node/test/parallel/test-repl-let-process.js @@ -0,0 +1,7 @@ +'use strict'; +require('../common'); +const { startNewREPLServer } = require('../common/repl'); + +// Regression test for https://github.com/nodejs/node/issues/6802 +const { input } = startNewREPLServer({ useGlobal: true }); +input.run(['let process']); diff --git a/test/js/node/test/parallel/test-repl-load-multiline-from-history.js b/test/js/node/test/parallel/test-repl-load-multiline-from-history.js new file mode 100644 index 000000000000..5814e3819da1 --- /dev/null +++ b/test/js/node/test/parallel/test-repl-load-multiline-from-history.js @@ -0,0 +1,96 @@ +'use strict'; + +// Flags: --expose-internals + +const common = require('../common'); + +const assert = require('assert'); +const repl = require('internal/repl'); +const stream = require('stream'); +const fixtures = require('../common/fixtures'); + +class ActionStream extends stream.Stream { + run(data) { + const _iter = data[Symbol.iterator](); + const doAction = () => { + const next = _iter.next(); + if (next.done) { + // Close the repl. Note that it must have a clean prompt to do so. + this.emit('keypress', '', { ctrl: true, name: 'd' }); + return; + } + const action = next.value; + + if (typeof action === 'object') { + this.emit('keypress', '', action); + } + setImmediate(doAction); + }; + doAction(); + } + resume() {} + pause() {} +} +ActionStream.prototype.readable = true; + +{ + // Testing multiline history loading + const tmpdir = require('../common/tmpdir'); + tmpdir.refresh(); + const replHistoryPath = fixtures.path('.node_repl_history_multiline'); + + const checkResults = common.mustSucceed((r) => { + assert.strictEqual(r.history.length, 4); + + r.input.run([{ name: 'up' }]); + assert.strictEqual(r.line, 'var d = [\n' + + ' {\n' + + ' a: 1,\n' + + ' b: 2,\n' + + ' },\n' + + ' {\n' + + ' a: 3,\n' + + ' b: 4,\n' + + ' c: [{ a: 1, b: 2 },\n' + + ' {\n' + + ' a: 3,\n' + + ' b: 4,\n' + + ' }\n' + + ' ]\n' + + ' }\n' + + ']' + ); + + // Move the cursor all lines up until the former entry is retrieved. + for (let i = 0; i < r.line.split('\n').length; i++) { + r.input.run([{ name: 'up' }]); + } + assert.strictEqual(r.line, 'const c = [\n {\n a: 1,\n b: 2,\n }\n]'); + + // Move the cursor all lines up until the former entry is retrieved. + for (let i = 0; i < r.line.split('\n').length; i++) { + r.input.run([{ name: 'up' }]); + } + assert.strictEqual(r.line, '`const b = [\n 1,\n 2,\n 3,\n 4,\n]`'); + + // Move the cursor all lines up until the former entry is retrieved. + for (let i = 0; i < r.line.split('\n').length; i++) { + r.input.run([{ name: 'up' }]); + } + assert.strictEqual(r.line, 'a = `\nI am a multiline string\nI can be as long as I want`'); + }); + + repl.createInternalRepl( + { NODE_REPL_HISTORY: replHistoryPath }, + { + terminal: true, + input: new ActionStream(), + output: new stream.Writable({ + write(chunk, _, next) { + next(); + } + }), + }, + checkResults + ); +} diff --git a/test/js/node/test/parallel/test-repl-load-multiline-no-trailing-newline.js b/test/js/node/test/parallel/test-repl-load-multiline-no-trailing-newline.js new file mode 100644 index 000000000000..b67139c1efca --- /dev/null +++ b/test/js/node/test/parallel/test-repl-load-multiline-no-trailing-newline.js @@ -0,0 +1,33 @@ +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const { startNewREPLServer } = require('../common/repl'); + +if (process.env.TERM === 'dumb') { + common.skip('skipping - dumb terminal'); +} + +const command = `.load ${fixtures.path('repl-load-multiline-no-trailing-newline.js')}`; +const terminalCode = '\u001b[1G\u001b[0J \u001b[1G'; +const terminalCodeRegex = new RegExp(terminalCode.replace(/\[/g, '\\['), 'g'); + +const expected = `${command} +// The lack of a newline at the end of this file is intentional. +const getLunch = () => + placeOrder('tacos') + .then(eat); + +const placeOrder = (order) => Promise.resolve(order); +const eat = (food) => ''; +undefined +`; + +const { replServer, output } = startNewREPLServer(); + +replServer.write(`${command}\n`); +assert.strictEqual( + output.accumulator.replace(terminalCodeRegex, ''), + expected +); +replServer.close(); diff --git a/test/js/node/test/parallel/test-repl-load-multiline.js b/test/js/node/test/parallel/test-repl-load-multiline.js new file mode 100644 index 000000000000..c2bf5635c61c --- /dev/null +++ b/test/js/node/test/parallel/test-repl-load-multiline.js @@ -0,0 +1,30 @@ +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const { startNewREPLServer } = require('../common/repl'); + +if (process.env.TERM === 'dumb') { + common.skip('skipping - dumb terminal'); +} + +const command = `.load ${fixtures.path('repl-load-multiline.js')}`; +const terminalCode = '\u001b[1G\u001b[0J \u001b[1G'; +const terminalCodeRegex = new RegExp(terminalCode.replace(/\[/g, '\\['), 'g'); + +const expected = `${command} +const getLunch = () => + placeOrder('tacos') + .then(eat); + +const placeOrder = (order) => Promise.resolve(order); +const eat = (food) => ''; + +undefined +`; + +const { replServer, output } = startNewREPLServer(); + +replServer.write(`${command}\n`); +assert.strictEqual(output.accumulator.replace(terminalCodeRegex, ''), expected); +replServer.close(); diff --git a/test/js/node/test/parallel/test-repl-multiline-navigation-while-adding.js b/test/js/node/test/parallel/test-repl-multiline-navigation-while-adding.js new file mode 100644 index 000000000000..33390749e40d --- /dev/null +++ b/test/js/node/test/parallel/test-repl-multiline-navigation-while-adding.js @@ -0,0 +1,312 @@ +'use strict'; + +// Flags: --expose-internals + +const common = require('../common'); + +const assert = require('assert'); +const repl = require('internal/repl'); +const stream = require('stream'); + +class ActionStream extends stream.Stream { + run(data) { + const _iter = data[Symbol.iterator](); + const doAction = () => { + const next = _iter.next(); + if (next.done) { + // Close the repl. Note that it must have a clean prompt to do so. + this.emit('keypress', '', { ctrl: true, name: 'd' }); + return; + } + const action = next.value; + + if (typeof action === 'object') { + this.emit('keypress', '', action); + } + setImmediate(doAction); + }; + doAction(); + } + write(chunk) { + const chunkLines = chunk.toString('utf8').split('\n'); + this.lines[this.lines.length - 1] += chunkLines[0]; + if (chunkLines.length > 1) { + this.lines.push(...chunkLines.slice(1)); + } + this.emit('line', this.lines[this.lines.length - 1]); + return true; + } + resume() {} + pause() {} +} +ActionStream.prototype.readable = true; + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +{ + const historyPath = tmpdir.resolve(`.${Math.floor(Math.random() * 10000)}`); + // Make sure the cursor is at the right places when pressing enter at the end of the first line. + const checkResults = common.mustSucceed((r) => { + r.write('let aaa = `I am a'); + r.input.run([{ name: 'enter' }]); + r.write('1111111111111'); + r.input.run([{ name: 'enter' }]); + r.write('22222222222222'); // The command is not complete yet. I can still edit it + r.input.run([{ name: 'up' }]); + r.input.run([{ name: 'up' }]); // I am on the first line + for (let i = 0; i < 3; i++) { + r.input.run([{ name: 'right' }]); + } // I am at the end of the first line + assert.strictEqual(r.cursor, 17); + + r.input.run([{ name: 'enter' }]); + assert.strictEqual(r.cursor, 18); + assert.strictEqual(r.line, 'let aaa = `I am a\n\n1111111111111\n22222222222222'); + r.write('000'); + r.input.run([{ name: 'down' }]); + r.input.run([{ name: 'down' }]); // I am in the last line + for (let i = 0; i < 5; i++) { + r.input.run([{ name: 'right' }]); + } // I am at the end of the last line + r.write('`'); // Making the command complete + r.input.run([{ name: 'enter' }]); // Issuing it + assert.strictEqual(r.history.length, 1); + assert.strictEqual(r.history[0], '22222222`222222\r1111111111111\r000\rlet aaa = `I am a'); + }); + + repl.createInternalRepl( + { NODE_REPL_HISTORY: historyPath }, + { + terminal: true, + input: new ActionStream(), + output: new stream.Writable({ + write(chunk, _, next) { + next(); + } + }), + }, + checkResults + ); +} + +{ + const historyPath = tmpdir.resolve(`.${Math.floor(Math.random() * 10000)}`); + // Make sure the cursor is at the right places when pressing enter in the middle of the first line. + const checkResults = common.mustSucceed((r) => { + r.write('let bbb = `I am a'); + r.input.run([{ name: 'enter' }]); + r.write('1111111111111'); + r.input.run([{ name: 'enter' }]); + r.write('22222222222222'); // The command is not complete yet. I can still edit it + r.input.run([{ name: 'up' }]); + r.input.run([{ name: 'up' }]); // I am on the first line + for (let i = 0; i < 3; i++) { + r.input.run([{ name: 'left' }]); + } // I am right after the string definition + assert.strictEqual(r.cursor, 11); + + r.input.run([{ name: 'enter' }]); + assert.strictEqual(r.cursor, 12); + assert.strictEqual(r.line, 'let bbb = `\nI am a\n1111111111111\n22222222222222'); + r.write('000'); + r.input.run([{ name: 'enter' }]); + r.input.run([{ name: 'down' }]); + r.input.run([{ name: 'down' }]); // I am in the last line + for (let i = 0; i < 14; i++) { + r.input.run([{ name: 'right' }]); + } // I am at the end of the last line + r.write('`'); // Making the command complete + r.input.run([{ name: 'enter' }]); // Issuing it + assert.strictEqual(r.history.length, 1); + assert.strictEqual(r.history[0], '22222222222222`\r1111111111111\rI am a\r000\rlet bbb = `'); + }); + + repl.createInternalRepl( + { NODE_REPL_HISTORY: historyPath }, + { + terminal: true, + input: new ActionStream(), + output: new stream.Writable({ + write(chunk, _, next) { + next(); + } + }), + }, + checkResults + ); +} + +{ + const historyPath = tmpdir.resolve(`.${Math.floor(Math.random() * 10000)}`); + // Make sure the cursor is at the right places when pressing enter at the end of the second line. + const checkResults = common.mustSucceed((r) => { + r.write('let ccc = `I am a'); + r.input.run([{ name: 'enter' }]); + r.write('1111111111111'); + r.input.run([{ name: 'enter' }]); + r.write('22222222222222'); // The command is not complete yet. I can still edit it + r.input.run([{ name: 'up' }]); // I am the end of second line + assert.strictEqual(r.cursor, 31); + + r.input.run([{ name: 'enter' }]); + assert.strictEqual(r.cursor, 32); + assert.strictEqual(r.line, 'let ccc = `I am a\n1111111111111\n\n22222222222222'); + r.write('000'); + r.input.run([{ name: 'down' }]); // I am in the last line + for (let i = 0; i < 11; i++) { + r.input.run([{ name: 'right' }]); + } // I am at the end of the last line + r.write('`'); // Making the command complete + r.input.run([{ name: 'enter' }]); // Issuing it + assert.strictEqual(r.history.length, 1); + assert.strictEqual(r.history[0], '22222222222222`\r000\r1111111111111\rlet ccc = `I am a'); + }); + + repl.createInternalRepl( + { NODE_REPL_HISTORY: historyPath }, + { + terminal: true, + input: new ActionStream(), + output: new stream.Writable({ + write(chunk, _, next) { + next(); + } + }), + }, + checkResults + ); +} + +{ + const historyPath = tmpdir.resolve(`.${Math.floor(Math.random() * 10000)}`); + // Make sure the cursor is at the right places when pressing enter in the middle of the second line. + const checkResults = common.mustSucceed((r) => { + r.write('let ddd = `I am a'); + r.input.run([{ name: 'enter' }]); + r.write('1111111111111'); + r.input.run([{ name: 'enter' }]); + r.write('22222222222222'); // The command is not complete yet. I can still edit it + r.input.run([{ name: 'up' }]); // I am the end of second line + assert.strictEqual(r.cursor, 31); + + for (let i = 0; i < 6; i++) { + r.input.run([{ name: 'left' }]); + } // I am in the middle of the second line + + r.input.run([{ name: 'enter' }]); + assert.strictEqual(r.cursor, 26); + assert.strictEqual(r.line, 'let ddd = `I am a\n1111111\n111111\n22222222222222'); + r.input.run([{ name: 'down' }]); // I am at the beginning of the last line + for (let i = 0; i < 14; i++) { + r.input.run([{ name: 'right' }]); + } // I am at the end of the last line + r.write('`'); // Making the command complete + r.input.run([{ name: 'enter' }]); // Issuing it + assert.strictEqual(r.history.length, 1); + assert.strictEqual(r.history[0], '22222222222222`\r111111\r1111111\rlet ddd = `I am a'); + }); + + repl.createInternalRepl( + { NODE_REPL_HISTORY: historyPath }, + { + terminal: true, + input: new ActionStream(), + output: new stream.Writable({ + write(chunk, _, next) { + next(); + } + }), + }, + checkResults + ); +} + +{ + const historyPath = tmpdir.resolve(`.${Math.floor(Math.random() * 10000)}`); + // Make sure the cursor is at the right places when pressing enter at the beginning of the third line. + const checkResults = common.mustSucceed((r) => { + r.write('let eee = `I am a'); + r.input.run([{ name: 'enter' }]); + r.write('1111111111111'); + r.input.run([{ name: 'enter' }]); + r.write('22222222222222'); // The command is not complete yet. I can still edit it + for (let i = 0; i < 14; i++) { + r.input.run([{ name: 'left' }]); + } // I am at the beginning of the last line + assert.strictEqual(r.cursor, 32); + r.input.run([{ name: 'enter' }]); + assert.strictEqual(r.cursor, 33); + assert.strictEqual(r.line, 'let eee = `I am a\n1111111111111\n\n22222222222222'); + r.input.run([{ name: 'up' }]); // I am the beginning of the new line + r.write('000'); + assert.strictEqual(r.cursor, 35); + r.input.run([{ name: 'down' }]); // I am in the last line + for (let i = 0; i < 11; i++) { + r.input.run([{ name: 'right' }]); + } // I am at the end of the last line + r.write('`'); // Making the command complete + r.input.run([{ name: 'enter' }]); // Issuing it + assert.strictEqual(r.history.length, 1); + assert.strictEqual(r.history[0], '22222222222222`\r000\r1111111111111\rlet eee = `I am a'); + }); + + repl.createInternalRepl( + { NODE_REPL_HISTORY: historyPath }, + { + terminal: true, + input: new ActionStream(), + output: new stream.Writable({ + write(chunk, _, next) { + next(); + } + }), + }, + checkResults + ); +} + +{ + const historyPath = tmpdir.resolve(`.${Math.floor(Math.random() * 10000)}`); + // Make sure the cursor is at the right places when pressing enter in the middle of the third line + // And executing the command while still in the middle of the multiline command + const checkResults = common.mustSucceed((r) => { + r.write('let fff = `I am a'); + r.input.run([{ name: 'enter' }]); + r.write('1111111111111'); + r.input.run([{ name: 'enter' }]); + r.write('22222222222222'); // The command is not complete yet. I can still edit it + assert.strictEqual(r.cursor, 46); + + for (let i = 0; i < 6; i++) { + r.input.run([{ name: 'left' }]); + } // I am in the middle of the third line + + r.input.run([{ name: 'enter' }]); + assert.strictEqual(r.cursor, 41); + assert.strictEqual(r.line, 'let fff = `I am a\n1111111111111\n22222222\n222222'); + r.input.run([{ name: 'down' }]); // I am at the beginning of the last line + for (let i = 0; i < 6; i++) { + r.input.run([{ name: 'right' }]); + } // I am at the end of the last line + r.write('`'); // Making the command complete + r.input.run([{ name: 'up' }]); // I am not at the end of the last line + r.input.run([{ name: 'enter' }]); // Issuing the command + assert.strictEqual(r.history.length, 1); + assert.strictEqual(r.history[0], '222222`\r22222222\r1111111111111\rlet fff = `I am a'); + }); + + repl.createInternalRepl( + { NODE_REPL_HISTORY: historyPath }, + { + terminal: true, + input: new ActionStream(), + output: new stream.Writable({ + write(chunk, _, next) { + next(); + } + }), + }, + checkResults + ); +} diff --git a/test/js/node/test/parallel/test-repl-multiline-navigation.js b/test/js/node/test/parallel/test-repl-multiline-navigation.js new file mode 100644 index 000000000000..7bcef8875975 --- /dev/null +++ b/test/js/node/test/parallel/test-repl-multiline-navigation.js @@ -0,0 +1,261 @@ +'use strict'; + +// Flags: --expose-internals + +const common = require('../common'); + +const assert = require('assert'); +const repl = require('internal/repl'); +const stream = require('stream'); + +class ActionStream extends stream.Stream { + run(data) { + const _iter = data[Symbol.iterator](); + const doAction = () => { + const next = _iter.next(); + if (next.done) { + // Close the repl. Note that it must have a clean prompt to do so. + this.emit('keypress', '', { ctrl: true, name: 'd' }); + return; + } + const action = next.value; + + if (typeof action === 'object') { + this.emit('keypress', '', action); + } + setImmediate(doAction); + }; + doAction(); + } + write(chunk) { + const chunkLines = chunk.toString('utf8').split('\n'); + this.lines[this.lines.length - 1] += chunkLines[0]; + if (chunkLines.length > 1) { + this.lines.push(...chunkLines.slice(1)); + } + this.emit('line', this.lines[this.lines.length - 1]); + return true; + } + resume() {} + pause() {} +} +ActionStream.prototype.readable = true; + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +{ + const historyPath = tmpdir.resolve(`.${Math.floor(Math.random() * 10000)}`); + // Make sure the cursor is at the right places. + // If the cursor is at the end of a long line and the down key is pressed, + // Move the cursor to the end of the next line, if shorter. + const checkResults = common.mustSucceed((r) => { + r.write('let str = `'); + r.input.run([{ name: 'enter' }]); + r.write('111'); + r.input.run([{ name: 'enter' }]); + r.write('22222222222222'); + r.input.run([{ name: 'enter' }]); + r.write('3`'); + r.input.run([{ name: 'enter' }]); + r.input.run([{ name: 'up' }]); + assert.strictEqual(r.cursor, 33); + + r.input.run([{ name: 'up' }]); + assert.strictEqual(r.cursor, 18); + + for (let i = 0; i < 5; i++) { + r.input.run([{ name: 'right' }]); + } + r.input.run([{ name: 'up' }]); + assert.strictEqual(r.cursor, 15); + r.input.run([{ name: 'up' }]); + + for (let i = 0; i < 4; i++) { + r.input.run([{ name: 'right' }]); + } + assert.strictEqual(r.cursor, 11); + + r.input.run([{ name: 'down' }]); + assert.strictEqual(r.cursor, 15); + + r.input.run([{ name: 'down' }]); + assert.strictEqual(r.cursor, 27); + + r.close(); + }); + + repl.createInternalRepl( + { NODE_REPL_HISTORY: historyPath }, + { + terminal: true, + input: new ActionStream(), + output: new stream.Writable({ + write(chunk, _, next) { + next(); + } + }), + }, + checkResults + ); +} + +{ + const historyPath = tmpdir.resolve(`.${Math.floor(Math.random() * 10000)}`); + // Make sure the cursor is at the right places. + // This is testing cursor clamping and restoring when moving up and down from long lines. + const checkResults = common.mustSucceed((r) => { + r.write('let ddd = `000'); + r.input.run([{ name: 'enter' }]); + r.write('1111111111111'); + r.input.run([{ name: 'enter' }]); + r.write('22222'); + r.input.run([{ name: 'enter' }]); + r.write('2222'); + r.input.run([{ name: 'enter' }]); + r.write('22222'); + r.input.run([{ name: 'enter' }]); + r.write('33333333`'); + r.input.run([{ name: 'up' }]); + assert.strictEqual(r.cursor, 45); + + r.input.run([{ name: 'up' }]); + assert.strictEqual(r.cursor, 39); + + r.input.run([{ name: 'up' }]); + assert.strictEqual(r.cursor, 34); + + r.input.run([{ name: 'up' }]); + assert.strictEqual(r.cursor, 24); + + r.input.run([{ name: 'right' }]); + // This is to reach a cursor pos which is much higher than the line we want to go to, + // So we can check that the cursor is clamped to the end of the line. + r.input.run([{ name: 'right' }]); + + r.input.run([{ name: 'down' }]); + assert.strictEqual(r.cursor, 34); + + r.input.run([{ name: 'down' }]); + assert.strictEqual(r.cursor, 39); + + r.input.run([{ name: 'down' }]); + assert.strictEqual(r.cursor, 45); + + r.input.run([{ name: 'down' }]); + assert.strictEqual(r.cursor, 55); + + r.close(); + }); + + repl.createInternalRepl( + { NODE_REPL_HISTORY: historyPath }, + { + terminal: true, + input: new ActionStream(), + output: new stream.Writable({ + write(chunk, _, next) { + next(); + } + }), + }, + checkResults + ); +} + +{ + const historyPath = tmpdir.resolve(`.${Math.floor(Math.random() * 10000)}`); + // If the last command errored and the user is trying to edit it, + // The errored line should be removed from history + const checkResults = common.mustSucceed((r) => { + r.write('let lineWithMistake = `I have some'); + r.input.run([{ name: 'enter' }]); + r.write('problem with` my syntax\''); + r.input.run([{ name: 'enter' }]); + r.input.run([{ name: 'up' }]); + r.input.run([{ name: 'backspace' }]); + r.write('`'); + for (let i = 0; i < 11; i++) { + r.input.run([{ name: 'left' }]); + } + r.input.run([{ name: 'backspace' }]); + r.input.run([{ name: 'enter' }]); + + assert.strictEqual(r.history.length, 1); + // Check that the line is properly set in the history structure + assert.strictEqual(r.history[0], 'problem with my syntax`\rlet lineWithMistake = `I have some'); + assert.strictEqual(r.line, ''); + + r.input.run([{ name: 'up' }]); + // Check that the line is properly displayed + assert.strictEqual(r.line, 'let lineWithMistake = `I have some\nproblem with my syntax`'); + + r.close(); + }); + + repl.createInternalRepl( + { NODE_REPL_HISTORY: historyPath }, + { + terminal: true, + input: new ActionStream(), + output: new stream.Writable({ + write(chunk, _, next) { + next(); + } + }), + }, + checkResults + ); +} + +{ + const historyPath = tmpdir.resolve(`.${Math.floor(Math.random() * 10000)}`); + const outputBuffer = []; + + // Test that the REPL preview is properly shown on multiline commands + // And deleted when enter is pressed + const checkResults = common.mustSucceed((r) => { + r.write('Array(100).fill('); + r.input.run([{ name: 'enter' }]); + r.write('123'); + r.input.run([{ name: 'enter' }]); + r.write(')'); + r.input.run([{ name: 'enter' }]); + r.input.run([{ name: 'up' }]); + r.input.run([{ name: 'up' }]); + + assert.deepStrictEqual(r.last, new Array(100).fill(123)); + r.input.run([{ name: 'enter' }]); + assert.strictEqual(outputBuffer.includes('[\n' + + ' 123, 123, 123, 123, 123, 123, 123, 123, 123, 123, 123,\n' + + ' 123, 123, 123, 123, 123, 123, 123, 123, 123, 123, 123,\n' + + ' 123, 123, 123, 123, 123, 123, 123, 123, 123, 123, 123,\n' + + ' 123, 123, 123, 123, 123, 123, 123, 123, 123, 123, 123,\n' + + ' 123, 123, 123, 123, 123, 123, 123, 123, 123, 123, 123,\n' + + ' 123, 123, 123, 123, 123, 123, 123, 123, 123, 123, 123,\n' + + ' 123, 123, 123, 123, 123, 123, 123, 123, 123, 123, 123,\n' + + ' 123, 123, 123, 123, 123, 123, 123, 123, 123, 123, 123,\n' + + ' 123, 123, 123, 123, 123, 123, 123, 123, 123, 123, 123,\n' + + ' 123\n' + + ']\n'), true); + + r.close(); + }); + + repl.createInternalRepl( + { NODE_REPL_HISTORY: historyPath }, + { + preview: true, + terminal: true, + input: new ActionStream(), + output: new stream.Writable({ + write(chunk, _, next) { + // Store each chunk in the buffer + outputBuffer.push(chunk.toString()); + next(); + } + }), + }, + checkResults + ); +} diff --git a/test/js/node/test/parallel/test-repl-multiline.js b/test/js/node/test/parallel/test-repl-multiline.js new file mode 100644 index 000000000000..6aecb6701144 --- /dev/null +++ b/test/js/node/test/parallel/test-repl-multiline.js @@ -0,0 +1,29 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { startNewREPLServer } = require('../common/repl'); + +const input = ['const foo = {', '};', 'foo']; + +function run({ useColors }) { + const { replServer, output } = startNewREPLServer({ useColors }); + + replServer.on('exit', common.mustCall(() => { + const actual = output.accumulator.split('\n'); + + // Validate the output, which contains terminal escape codes. + assert.strictEqual(actual.length, 6); + assert.ok(actual[0].endsWith(input[0])); + assert.ok(actual[1].includes('| ')); + assert.ok(actual[1].endsWith(input[1])); + assert.ok(actual[2].includes('undefined')); + assert.ok(actual[3].endsWith(input[2])); + assert.strictEqual(actual[4], '{}'); + })); + + input.forEach((line) => replServer.write(`${line}\n`)); + replServer.close(); +} + +run({ useColors: true }); +run({ useColors: false }); diff --git a/test/js/node/test/parallel/test-repl-multiple-instances-async-error.js b/test/js/node/test/parallel/test-repl-multiple-instances-async-error.js new file mode 100644 index 000000000000..ddc8a5eaccdc --- /dev/null +++ b/test/js/node/test/parallel/test-repl-multiple-instances-async-error.js @@ -0,0 +1,69 @@ +'use strict'; + +// This test verifies that when multiple REPL instances exist concurrently, +// async errors are correctly routed to the REPL instance that created them. + +const common = require('../common'); +const assert = require('assert'); +const repl = require('repl'); +const { Writable, PassThrough } = require('stream'); + +// Create two REPLs with separate inputs and outputs +let output1 = ''; +let output2 = ''; + +const input1 = new PassThrough(); +const input2 = new PassThrough(); + +const writable1 = new Writable({ + write(chunk, encoding, callback) { + output1 += chunk.toString(); + callback(); + } +}); + +const writable2 = new Writable({ + write(chunk, encoding, callback) { + output2 += chunk.toString(); + callback(); + } +}); + +const r1 = repl.start({ + input: input1, + output: writable1, + terminal: false, + prompt: 'R1> ', +}); + +const r2 = repl.start({ + input: input2, + output: writable2, + terminal: false, + prompt: 'R2> ', +}); + +// Create async error in REPL 1 +input1.write('setTimeout(() => { throw new Error("error from repl1") }, 10)\n'); + +// Create async error in REPL 2 +input2.write('setTimeout(() => { throw new Error("error from repl2") }, 20)\n'); + +setTimeout(common.mustCall(() => { + r1.close(); + r2.close(); + + // Verify error from REPL 1 went to REPL 1's output + assert.match(output1, /error from repl1/, + 'REPL 1 should have received its own async error'); + + // Verify error from REPL 2 went to REPL 2's output + assert.match(output2, /error from repl2/, + 'REPL 2 should have received its own async error'); + + // Verify errors did not cross over to wrong REPL + assert.doesNotMatch(output1, /error from repl2/, + 'REPL 1 should not have received REPL 2\'s error'); + assert.doesNotMatch(output2, /error from repl1/, + 'REPL 2 should not have received REPL 1\'s error'); +}), 100); diff --git a/test/js/node/test/parallel/test-repl-no-terminal-restore-process-listeners.js b/test/js/node/test/parallel/test-repl-no-terminal-restore-process-listeners.js new file mode 100644 index 000000000000..0cbe3de0bee5 --- /dev/null +++ b/test/js/node/test/parallel/test-repl-no-terminal-restore-process-listeners.js @@ -0,0 +1,17 @@ +'use strict'; +const common = require('../common'); +const { startNewREPLServer } = require('../common/repl'); +const assert = require('assert'); + +const originalProcessNewListenerCount = process.listenerCount('newListener'); +const { replServer } = startNewREPLServer(); + +const listenerCountBeforeClose = process.listenerCount('newListener'); +replServer.close(); +replServer.once('exit', common.mustCall(() => { + setImmediate(common.mustCall(() => { + const listenerCountAfterClose = process.listenerCount('newListener'); + assert.strictEqual(listenerCountAfterClose, listenerCountBeforeClose - 1); + assert.strictEqual(listenerCountAfterClose, originalProcessNewListenerCount); + })); +})); diff --git a/test/js/node/test/parallel/test-repl-no-terminal.js b/test/js/node/test/parallel/test-repl-no-terminal.js new file mode 100644 index 000000000000..803c8b519f69 --- /dev/null +++ b/test/js/node/test/parallel/test-repl-no-terminal.js @@ -0,0 +1,9 @@ +'use strict'; +const common = require('../common'); +const { startNewREPLServer } = require('../common/repl'); + +const { replServer } = startNewREPLServer(); + +replServer.setupHistory('/nonexistent/file', common.mustSucceed(() => { + replServer.close(); +})); diff --git a/test/js/node/test/parallel/test-repl-null-thrown.js b/test/js/node/test/parallel/test-repl-null-thrown.js new file mode 100644 index 000000000000..e62c3706e02b --- /dev/null +++ b/test/js/node/test/parallel/test-repl-null-thrown.js @@ -0,0 +1,13 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { startNewREPLServer } = require('../common/repl'); + +const { replServer, output } = startNewREPLServer(); + +replServer.emit('line', 'process.nextTick(() => { throw null; })'); +replServer.emit('line', '.exit'); + +setTimeout(common.mustCall(() => { + assert(output.accumulator.includes('Uncaught null')); +}), 0); diff --git a/test/js/node/test/parallel/test-repl-null.js b/test/js/node/test/parallel/test-repl-null.js new file mode 100644 index 000000000000..18009558eda4 --- /dev/null +++ b/test/js/node/test/parallel/test-repl-null.js @@ -0,0 +1,13 @@ +'use strict'; +require('../common'); +const repl = require('repl'); + +const replserver = new repl.REPLServer(); + +replserver._inTemplateLiteral = true; + +// `null` gets treated like an empty string. (Should it? You have to do some +// strange business to get it into the REPL. Maybe it should really throw?) + +replserver.emit('line', null); +replserver.emit('line', '.exit'); diff --git a/test/js/node/test/parallel/test-repl-options.js b/test/js/node/test/parallel/test-repl-options.js new file mode 100644 index 000000000000..0cea1d5be4ed --- /dev/null +++ b/test/js/node/test/parallel/test-repl-options.js @@ -0,0 +1,140 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +// Flags: --pending-deprecation + +'use strict'; +const common = require('../common'); +const ArrayStream = require('../common/arraystream'); +const assert = require('assert'); +const repl = require('repl'); +const cp = require('child_process'); + +assert.strictEqual(repl.repl, undefined); + +repl._builtinLibs; // eslint-disable-line no-unused-expressions +repl.builtinModules; // eslint-disable-line no-unused-expressions + +common.expectWarning({ + DeprecationWarning: { + DEP0142: + 'repl._builtinLibs is deprecated. Check module.builtinModules instead', + DEP0191: 'repl.builtinModules is deprecated. Check module.builtinModules instead', + DEP0141: 'repl.inputStream and repl.outputStream are deprecated. ' + + 'Use repl.input and repl.output instead', + } +}); + +// Create a dummy stream that does nothing +const stream = new ArrayStream(); + +// 1, mostly defaults +const r1 = repl.start({ + input: stream, + output: stream, + terminal: true +}); + +assert.strictEqual(r1.input, stream); +assert.strictEqual(r1.output, stream); +assert.strictEqual(r1.input, r1.inputStream); +assert.strictEqual(r1.output, r1.outputStream); +assert.strictEqual(r1.terminal, true); +assert.strictEqual(r1.useColors, false); +assert.strictEqual(r1.useGlobal, false); +assert.strictEqual(r1.ignoreUndefined, false); +assert.strictEqual(r1.replMode, repl.REPL_MODE_SLOPPY); +assert.strictEqual(r1.historySize, 30); + +// 2 +function writer() {} + +function evaler() {} +const r2 = repl.start({ + input: stream, + output: stream, + terminal: false, + useColors: true, + useGlobal: true, + ignoreUndefined: true, + eval: evaler, + writer: writer, + replMode: repl.REPL_MODE_STRICT, + historySize: 50 +}); +assert.strictEqual(r2.input, stream); +assert.strictEqual(r2.output, stream); +assert.strictEqual(r2.input, r2.inputStream); +assert.strictEqual(r2.output, r2.outputStream); +assert.strictEqual(r2.terminal, false); +assert.strictEqual(r2.useColors, true); +assert.strictEqual(r2.useGlobal, true); +assert.strictEqual(r2.ignoreUndefined, true); +assert.strictEqual(r2.writer, writer); +assert.strictEqual(r2.replMode, repl.REPL_MODE_STRICT); +assert.strictEqual(r2.historySize, 50); + +// 3, breakEvalOnSigint and eval supplied together should cause a throw +const r3 = () => repl.start({ + breakEvalOnSigint: true, + eval: true +}); + +assert.throws(r3, { + code: 'ERR_INVALID_REPL_EVAL_CONFIG', + name: 'TypeError', + message: 'Cannot specify both "breakEvalOnSigint" and "eval" for REPL' +}); + +// 4, Verify that defaults are used when no arguments are provided +const r4 = repl.start(); + +assert.strictEqual(r4.getPrompt(), '> '); +assert.strictEqual(r4.input, process.stdin); +assert.strictEqual(r4.output, process.stdout); +assert.strictEqual(r4.terminal, !!r4.output.isTTY); +assert.strictEqual(r4.useColors, r4.terminal); +assert.strictEqual(r4.useGlobal, false); +assert.strictEqual(r4.ignoreUndefined, false); +assert.strictEqual(r4.replMode, repl.REPL_MODE_SLOPPY); +assert.strictEqual(r4.historySize, 30); +r4.close(); + +// Check the standalone REPL +{ + const child = cp.spawn(process.execPath, ['--interactive']); + let output = ''; + + child.stdout.setEncoding('utf8'); + child.stdout.on('data', (data) => { + output += data; + }); + + child.on('exit', common.mustCall(() => { + const results = output.replace(/^> /mg, '').split('\n').slice(2); + assert.deepStrictEqual(results, ['undefined', '']); + })); + + child.stdin.write( + 'assert.ok(util.inspect(repl.repl, {depth: -1}).includes("REPLServer"));\n' + ); + child.stdin.write('.exit\n'); +} diff --git a/test/js/node/test/parallel/test-repl-permission-model.js b/test/js/node/test/parallel/test-repl-permission-model.js new file mode 100644 index 000000000000..167624f546aa --- /dev/null +++ b/test/js/node/test/parallel/test-repl-permission-model.js @@ -0,0 +1,137 @@ +'use strict'; + +// Flags: --expose-internals --permission --allow-fs-read=* + +const common = require('../common'); +const stream = require('stream'); +const REPL = require('internal/repl'); +const assert = require('assert'); +const { inspect } = require('util'); + +if (process.env.TERM === 'dumb') { + common.skip('skipping - dumb terminal'); +} + +// Create an input stream specialized for testing an array of actions +class ActionStream extends stream.Stream { + run(data) { + const _iter = data[Symbol.iterator](); + const doAction = () => { + const next = _iter.next(); + if (next.done) { + // Close the repl. Note that it must have a clean prompt to do so. + this.emit('keypress', '', { ctrl: true, name: 'd' }); + return; + } + const action = next.value; + + if (typeof action === 'object') { + this.emit('keypress', '', action); + } else { + this.emit('data', `${action}`); + } + setImmediate(doAction); + }; + doAction(); + } + resume() {} + pause() {} +} +ActionStream.prototype.readable = true; + +// Mock keys +const ENTER = { name: 'enter' }; +const TABULATION = { name: 'tab' }; + +const prompt = '> '; + +const tests = [ + { + test: (function*() { + yield 'f'; + yield TABULATION; + yield ENTER; + })(), + expected: [], + env: {} + }, +]; + +const numtests = tests.length; + +const runTestWrap = common.mustCall(runTest, numtests); + +function runTest() { + const opts = tests.shift(); + if (!opts) return; // All done + + const { expected, skip } = opts; + + // Test unsupported on platform. + if (skip) { + setImmediate(runTestWrap, true); + return; + } + const lastChunks = []; + let i = 0; + + REPL.createInternalRepl(opts.env, { + input: new ActionStream(), + output: new stream.Writable({ + write: common.mustCallAtLeast((chunk, _, next) => { + const output = chunk.toString(); + + if (!opts.showEscapeCodes && + (output[0] === '\x1B' || /^[\r\n]+$/.test(output))) { + return next(); + } + + lastChunks.push(output); + + if (expected.length && !opts.checkTotal) { + try { + assert.strictEqual(output, expected[i]); + } catch (e) { + console.error(`Failed test # ${numtests - tests.length}`); + console.error('Last outputs: ' + inspect(lastChunks, { + breakLength: 5, colors: true + })); + throw e; + } + // bun: upstream-todo(BridgeAR): Auto close on last chunk! + i++; + } + + next(); + }), + }), + allowBlockingCompletions: true, + completer: opts.completer, + prompt, + useColors: false, + preview: opts.preview, + terminal: true + }, common.mustCall((err, repl) => { + if (err) { + console.error(`Failed test # ${numtests - tests.length}`); + throw err; + } + + repl.once('close', common.mustCall(() => { + + if (opts.checkTotal) { + assert.deepStrictEqual(lastChunks, expected); + } else if (expected.length !== i) { + console.error(tests[numtests - tests.length - 1]); + throw new Error(`Failed test # ${numtests - tests.length}`); + } + + setImmediate(runTestWrap, true); + })); + + repl.input.run(opts.test); + })); +} + +// run the tests +runTest(); diff --git a/test/js/node/test/parallel/test-repl-persistent-history.js b/test/js/node/test/parallel/test-repl-persistent-history.js new file mode 100644 index 000000000000..0807a10a08a8 --- /dev/null +++ b/test/js/node/test/parallel/test-repl-persistent-history.js @@ -0,0 +1,266 @@ +'use strict'; + +// Flags: --expose-internals + +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const stream = require('stream'); +const REPL = require('internal/repl'); +const assert = require('assert'); +const fs = require('fs'); +const os = require('os'); + +if (process.env.TERM === 'dumb') { + common.skip('skipping - dumb terminal'); +} + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +// Mock os.homedir() +os.homedir = function() { + return tmpdir.path; +}; + +// Create an input stream specialized for testing an array of actions +class ActionStream extends stream.Stream { + run(data) { + const _iter = data[Symbol.iterator](); + const doAction = () => { + const next = _iter.next(); + if (next.done) { + // Close the repl. Note that it must have a clean prompt to do so. + setImmediate(() => { + this.emit('keypress', '', { ctrl: true, name: 'd' }); + }); + return; + } + const action = next.value; + + if (typeof action === 'object') { + this.emit('keypress', '', action); + } else { + this.emit('data', action); + } + setImmediate(doAction); + }; + doAction(); + } + resume() {} + pause() {} +} +ActionStream.prototype.readable = true; + + +// Mock keys +const UP = { name: 'up' }; +const DOWN = { name: 'down' }; +const ENTER = { name: 'enter' }; +const CLEAR = { ctrl: true, name: 'u' }; + +// File paths +const historyFixturePath = fixtures.path('.node_repl_history'); +const historyPath = tmpdir.resolve('.fixture_copy_repl_history'); +const historyPathFail = fixtures.path('nonexistent_folder', 'filename'); +const defaultHistoryPath = tmpdir.resolve('.node_repl_history'); +const emptyHiddenHistoryPath = fixtures.path('.empty-hidden-repl-history-file'); +const devNullHistoryPath = tmpdir.resolve('.dev-null-repl-history-file'); +// Common message bits +const prompt = '> '; +const replDisabled = '\nPersistent history support disabled. Set the ' + + 'NODE_REPL_HISTORY environment\nvariable to a valid, ' + + 'user-writable path to enable.\n'; +const homedirErr = '\nError: Could not get the home directory.\n' + + 'REPL session history will not be persisted.\n'; +const replFailedRead = '\nError: Could not open history file.\n' + + 'REPL session history will not be persisted.\n'; + +const tests = [ + { + env: { NODE_REPL_HISTORY: '' }, + test: [UP], + expected: [prompt, replDisabled, prompt] + }, + { + env: { NODE_REPL_HISTORY: ' ' }, + test: [UP], + expected: [prompt, replDisabled, prompt] + }, + { + env: { NODE_REPL_HISTORY: historyPath }, + test: [UP, CLEAR], + expected: [prompt, `${prompt}'you look fabulous today'`, prompt] + }, + { + env: {}, + test: [UP, '21', ENTER, "'42'", ENTER], + expected: [ + prompt, + '2', '1', '21\n', prompt, + "'", '4', '2', "'", "'42'\n", prompt, + ], + clean: false + }, + { // Requires the above test case + env: {}, + test: [UP, UP, CLEAR, ENTER, DOWN, CLEAR, ENTER, UP, ENTER], + expected: [ + prompt, + `${prompt}'42'`, + `${prompt}21`, + prompt, + prompt, + `${prompt}'42'`, + prompt, + prompt, + `${prompt}21`, + '21\n', + prompt, + ] + }, + { + env: { NODE_REPL_HISTORY: historyPath, + NODE_REPL_HISTORY_SIZE: 1 }, + test: [UP, UP, DOWN, CLEAR], + expected: [ + prompt, + `${prompt}'you look fabulous today'`, + prompt, + `${prompt}'you look fabulous today'`, + prompt, + ] + }, + { + env: { NODE_REPL_HISTORY: historyPathFail, + NODE_REPL_HISTORY_SIZE: 1 }, + test: [UP], + expected: [prompt, replFailedRead, prompt, replDisabled, prompt] + }, + { + before: common.mustCall(function before() { + if (common.isWindows) { + const execSync = require('child_process').execSync; + execSync(`ATTRIB +H "${emptyHiddenHistoryPath}"`); + } + }), + env: { NODE_REPL_HISTORY: emptyHiddenHistoryPath }, + test: [UP], + expected: [prompt] + }, + { + before: function before() { + if (!common.isWindows) + fs.symlinkSync('/dev/null', devNullHistoryPath); + }, + env: { NODE_REPL_HISTORY: devNullHistoryPath }, + test: [UP], + expected: [prompt] + }, + { // Make sure this is always the last test, since we change os.homedir() + before: function before() { + // Mock os.homedir() failure + os.homedir = function() { + throw new Error('os.homedir() failure'); + }; + }, + env: {}, + test: [UP], + expected: [prompt, homedirErr, prompt, replDisabled, prompt] + }, +]; +const numtests = tests.length; + + +function cleanupTmpFile() { + try { + // Write over the file, clearing any history + fs.writeFileSync(defaultHistoryPath, ''); + } catch (err) { + if (err.code === 'ENOENT') return true; + throw err; + } + return true; +} + +// Copy our fixture to the tmp directory +fs.createReadStream(historyFixturePath) + .pipe(fs.createWriteStream(historyPath)).on('unpipe', () => runTest()); + +const runTestWrap = common.mustCall(runTest, numtests); + +function runTest(assertCleaned) { + const opts = tests.shift(); + if (!opts) return; // All done + + if (assertCleaned) { + try { + assert.strictEqual(fs.readFileSync(defaultHistoryPath, 'utf8'), ''); + } catch (e) { + if (e.code !== 'ENOENT') { + console.error(`Failed test # ${numtests - tests.length}`); + throw e; + } + } + } + + const env = opts.env; + const test = opts.test; + const expected = opts.expected; + const clean = opts.clean; + const before = opts.before; + + if (before) before(); + + REPL.createInternalRepl(env, { + input: new ActionStream(), + output: new stream.Writable({ + write: common.mustCallAtLeast((chunk, _, next) => { + const output = chunk.toString(); + + // Ignore escapes and blank lines + if (output.charCodeAt(0) === 27 || /^[\r\n]+$/.test(output)) + return next(); + + try { + assert.strictEqual(output, expected.shift()); + } catch (err) { + console.error(`Failed test # ${numtests - tests.length}`); + throw err; + } + next(); + }), + }), + prompt, + useColors: false, + terminal: true + }, common.mustCall((err, repl) => { + if (err) { + console.error(`Failed test # ${numtests - tests.length}`); + throw err; + } + + repl.once('close', () => { + if (repl.historyManager.isFlushing) { + repl.once('flushHistory', onClose); + return; + } + + onClose(); + }); + + const onClose = common.mustCall(() => { + const cleaned = clean === false ? false : cleanupTmpFile(); + + try { + // Ensure everything that we expected was output + assert.strictEqual(expected.length, 0); + setImmediate(runTestWrap, cleaned); + } catch (err) { + console.error(`Failed test # ${numtests - tests.length}`); + throw err; + } + }); + + repl.inputStream.run(test); + })); +} diff --git a/test/js/node/test/parallel/test-repl-preprocess-top-level-await.js b/test/js/node/test/parallel/test-repl-preprocess-top-level-await.js new file mode 100644 index 000000000000..c49383e5773a --- /dev/null +++ b/test/js/node/test/parallel/test-repl-preprocess-top-level-await.js @@ -0,0 +1,154 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const { processTopLevelAwait } = require('internal/repl/await'); + +// Flags: --expose-internals + +// This test was created based on +// https://cs.chromium.org/chromium/src/third_party/WebKit/LayoutTests/http/tests/inspector-unit/preprocess-top-level-awaits.js?rcl=358caaba5e763e71c4abb9ada2d9cd8b1188cac9 + +const surrogate = ( + '"\u{1F601}\u{1f468}\u200d\u{1f469}\u200d\u{1f467}\u200d\u{1f466}"' +); + +const testCases = [ + [ '0', + null ], + [ 'await 0', + '(async () => { return { value: (await 0) } })()' ], + [ `await ${surrogate}`, + `(async () => { return { value: (await ${surrogate}) } })()` ], + [ 'await 0;', + '(async () => { return { value: (await 0) }; })()' ], + [ 'await 0;;;', + '(async () => { return { value: (await 0) };;; })()' ], + [ `await ${surrogate};`, + `(async () => { return { value: (await ${surrogate}) }; })()` ], + [ `await ${surrogate};`, + `(async () => { return { value: (await ${surrogate}) }; })()` ], + [ '(await 0)', + '(async () => { return ({ value: (await 0) }) })()' ], + [ `(await ${surrogate})`, + `(async () => { return ({ value: (await ${surrogate}) }) })()` ], + [ '(await 0);', + '(async () => { return ({ value: (await 0) }); })()' ], + [ `(await ${surrogate});`, + `(async () => { return ({ value: (await ${surrogate}) }); })()` ], + [ 'async function foo() { await 0; }', + null ], + [ 'async () => await 0', + null ], + [ 'class A { async method() { await 0 } }', + null ], + [ 'await 0; return 0;', + null ], + [ `await ${surrogate}; await ${surrogate};`, + `(async () => { await ${surrogate}; return { value: (await ${surrogate}) }; })()` ], + [ 'var a = await 1', + 'var a; (async () => { void (a = await 1) })()' ], + [ `var a = await ${surrogate}`, + `var a; (async () => { void (a = await ${surrogate}) })()` ], + [ 'let a = await 1', + 'let a; (async () => { void (a = await 1) })()' ], + [ 'const a = await 1', + 'let a; (async () => { void (a = await 1) })()' ], + [ 'for (var i = 0; i < 1; ++i) { await i }', + 'var i; (async () => { for (void (i = 0); i < 1; ++i) { await i } })()' ], + [ 'for (let i = 0; i < 1; ++i) { await i }', + '(async () => { for (let i = 0; i < 1; ++i) { await i } })()' ], + [ 'var {a} = {a:1}, [b] = [1], {c:{d}} = {c:{d: await 1}}', + 'var a, b, d; (async () => { void ( ({a} = {a:1}), ([b] = [1]), ' + + '({c:{d}} = {c:{d: await 1}})) })()' ], + [ 'let [a, b, c] = await ([1, 2, 3])', + 'let a, b, c; (async () => { void ([a, b, c] = await ([1, 2, 3])) })()'], + [ 'let {a,b,c} = await ({a: 1, b: 2, c: 3})', + 'let a, b, c; (async () => { void ({a,b,c} = ' + + 'await ({a: 1, b: 2, c: 3})) })()'], + [ 'let {a: [b]} = {a: [await 1]}, [{d}] = [{d: 3}]', + 'let b, d; (async () => { void ( ({a: [b]} = {a: [await 1]}),' + + ' ([{d}] = [{d: 3}])) })()'], + /* eslint-disable no-template-curly-in-string */ + [ 'console.log(`${(await { a: 1 }).a}`)', + '(async () => { return { value: (console.log(`${(await { a: 1 }).a}`)) } })()' ], + /* eslint-enable no-template-curly-in-string */ + [ 'await 0; function foo() {}', + 'var foo; (async () => { await 0; this.foo = foo; function foo() {} })()' ], + [ 'await 0; class Foo {}', + 'let Foo; (async () => { await 0; Foo=class Foo {} })()' ], + [ 'if (await true) { function foo() {} }', + 'var foo; (async () => { ' + + 'if (await true) { this.foo = foo; function foo() {} } })()' ], + [ 'if (await true) { class Foo{} }', + '(async () => { if (await true) { class Foo{} } })()' ], + [ 'if (await true) { var a = 1; }', + 'var a; (async () => { if (await true) { void (a = 1); } })()' ], + [ 'if (await true) { let a = 1; }', + '(async () => { if (await true) { let a = 1; } })()' ], + [ 'var a = await 1; let b = 2; const c = 3;', + 'var a; let b; let c; (async () => { void (a = await 1); void (b = 2);' + + ' void (c = 3); })()' ], + [ 'let o = await 1, p', + 'let o, p; (async () => { void ( (o = await 1), (p=undefined)) })()' ], + [ 'await (async () => { let p = await 1; return p; })()', + '(async () => { return { value: (await (async () => ' + + '{ let p = await 1; return p; })()) } })()' ], + [ '{ let p = await 1; }', + '(async () => { { let p = await 1; } })()' ], + [ 'var p = await 1', + 'var p; (async () => { void (p = await 1) })()' ], + [ 'await (async () => { var p = await 1; return p; })()', + '(async () => { return { value: (await (async () => ' + + '{ var p = await 1; return p; })()) } })()' ], + [ '{ var p = await 1; }', + 'var p; (async () => { { void (p = await 1); } })()' ], + [ 'for await (var i of asyncIterable) { i; }', + 'var i; (async () => { for await (i of asyncIterable) { i; } })()'], + [ 'for await (var [i] of asyncIterable) { i; }', + 'var i; (async () => { for await ([i] of asyncIterable) { i; } })()'], + [ 'for await (var {i} of asyncIterable) { i; }', + 'var i; (async () => { for await ({i} of asyncIterable) { i; } })()'], + [ 'for await (var [{i}, [j]] of asyncIterable) { i; }', + 'var i, j; (async () => { for await ([{i}, [j]] of asyncIterable)' + + ' { i; } })()'], + [ 'for await (let i of asyncIterable) { i; }', + '(async () => { for await (let i of asyncIterable) { i; } })()'], + [ 'for await (const i of asyncIterable) { i; }', + '(async () => { for await (const i of asyncIterable) { i; } })()'], + [ 'for (var i of [1,2,3]) { await 1; }', + 'var i; (async () => { for (i of [1,2,3]) { await 1; } })()'], + [ 'for (var [i] of [[1], [2]]) { await 1; }', + 'var i; (async () => { for ([i] of [[1], [2]]) { await 1; } })()'], + [ 'for (var {i} of [{i: 1}, {i: 2}]) { await 1; }', + 'var i; (async () => { for ({i} of [{i: 1}, {i: 2}]) { await 1; } })()'], + [ 'for (var [{i}, [j]] of [[{i: 1}, [2]]]) { await 1; }', + 'var i, j; (async () => { for ([{i}, [j]] of [[{i: 1}, [2]]])' + + ' { await 1; } })()'], + [ 'for (let i of [1,2,3]) { await 1; }', + '(async () => { for (let i of [1,2,3]) { await 1; } })()'], + [ 'for (const i of [1,2,3]) { await 1; }', + '(async () => { for (const i of [1,2,3]) { await 1; } })()'], + [ 'for (var i in {x:1}) { await 1 }', + 'var i; (async () => { for (i in {x:1}) { await 1 } })()'], + [ 'for (var [a,b] in {xy:1}) { await 1 }', + 'var a, b; (async () => { for ([a,b] in {xy:1}) { await 1 } })()'], + [ 'for (let i in {x:1}) { await 1 }', + '(async () => { for (let i in {x:1}) { await 1 } })()'], + [ 'for (const i in {x:1}) { await 1 }', + '(async () => { for (const i in {x:1}) { await 1 } })()'], + [ 'var x = await foo(); async function foo() { return Promise.resolve(1);}', + 'var x; var foo; (async () => { void (x = await foo()); this.foo = foo; ' + + 'async function foo() { return Promise.resolve(1);} })()'], + [ '(await x).y', + '(async () => { return { value: ((await x).y) } })()'], + [ 'await (await x).y', + '(async () => { return { value: (await (await x).y) } })()'], + [ 'var { ...rest } = await {}', + 'var rest; (async () => { void ({ ...rest } = await {}) })()', + ], +]; + +for (const [input, expected] of testCases) { + assert.strictEqual(processTopLevelAwait(input), expected); +} diff --git a/test/js/node/test/parallel/test-repl-pretty-stack-custom-writer.js b/test/js/node/test/parallel/test-repl-pretty-stack-custom-writer.js new file mode 100644 index 000000000000..e31460dbc93e --- /dev/null +++ b/test/js/node/test/parallel/test-repl-pretty-stack-custom-writer.js @@ -0,0 +1,15 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const { startNewREPLServer } = require('../common/repl'); + +const testingReplPrompt = '_REPL_TESTING_PROMPT_>'; + +const { replServer, output } = startNewREPLServer({ prompt: testingReplPrompt }); + +replServer.write('throw new Error("foo[a]")\n'); + +assert.strictEqual( + output.accumulator.split('\n').filter((line) => !line.includes(testingReplPrompt)).join(''), + 'Uncaught Error: foo[a]' +); diff --git a/test/js/node/test/parallel/test-repl-preview-timeout.js b/test/js/node/test/parallel/test-repl-preview-timeout.js new file mode 100644 index 000000000000..cf2f244c8147 --- /dev/null +++ b/test/js/node/test/parallel/test-repl-preview-timeout.js @@ -0,0 +1,17 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { startNewREPLServer } = require('../common/repl'); + +common.skipIfInspectorDisabled(); + +const { output, input } = startNewREPLServer(); + +output.accumulator = ''; + +// Input without '\n' triggering actual run. +const inputStr = 'while (true) {}'; +input.emit('data', inputStr); +// No preview available when timed out. +assert.strictEqual(output.accumulator, inputStr); diff --git a/test/js/node/test/parallel/test-repl-programmatic-history-setup-history.js b/test/js/node/test/parallel/test-repl-programmatic-history-setup-history.js new file mode 100644 index 000000000000..544f3994ef33 --- /dev/null +++ b/test/js/node/test/parallel/test-repl-programmatic-history-setup-history.js @@ -0,0 +1,279 @@ +'use strict'; + +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const stream = require('stream'); +const REPL = require('repl'); +const assert = require('assert'); +const fs = require('fs'); +const os = require('os'); + +if (process.env.TERM === 'dumb') { + common.skip('skipping - dumb terminal'); +} + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +// Mock os.homedir() +os.homedir = function() { + return tmpdir.path; +}; + +// Create an input stream specialized for testing an array of actions +class ActionStream extends stream.Stream { + run(data) { + const _iter = data[Symbol.iterator](); + const doAction = () => { + const next = _iter.next(); + if (next.done) { + // Close the repl. Note that it must have a clean prompt to do so. + setImmediate(() => { + this.emit('keypress', '', { ctrl: true, name: 'd' }); + }); + return; + } + const action = next.value; + + if (typeof action === 'object') { + this.emit('keypress', '', action); + } else { + this.emit('data', action); + } + setImmediate(doAction); + }; + doAction(); + } + resume() {} + pause() {} +} +ActionStream.prototype.readable = true; + + +// Mock keys +const UP = { name: 'up' }; +const DOWN = { name: 'down' }; +const ENTER = { name: 'enter' }; +const CLEAR = { ctrl: true, name: 'u' }; + +// File paths +const historyFixturePath = fixtures.path('.node_repl_history'); +const historyPath = tmpdir.resolve('.fixture_copy_repl_history'); +const historyPathFail = fixtures.path('nonexistent_folder', 'filename'); +const defaultHistoryPath = tmpdir.resolve('.node_repl_history'); +const emptyHiddenHistoryPath = fixtures.path('.empty-hidden-repl-history-file'); +const devNullHistoryPath = tmpdir.resolve('.dev-null-repl-history-file'); +// Common message bits +const prompt = '> '; +const replDisabled = '\nPersistent history support disabled. Set the ' + + 'NODE_REPL_HISTORY environment\nvariable to a valid, ' + + 'user-writable path to enable.\n'; +const homedirErr = '\nError: Could not get the home directory.\n' + + 'REPL session history will not be persisted.\n'; +const replFailedRead = '\nError: Could not open history file.\n' + + 'REPL session history will not be persisted.\n'; + +const tests = [ + // Makes sure that, if the history file is empty, the history is disabled + { + env: { NODE_REPL_HISTORY: '' }, + test: [UP], + expected: [prompt, replDisabled, prompt] + }, + // Makes sure that, if the history file is empty (when trimmed), the history is disabled + { + env: { NODE_REPL_HISTORY: ' ' }, + test: [UP], + expected: [prompt, replDisabled, prompt] + }, + // Properly loads the history file + { + env: { NODE_REPL_HISTORY: historyPath }, + test: [UP, CLEAR], + expected: [prompt, `${prompt}'you look fabulous today'`, prompt] + }, + // Properly navigates newly added history items + { + env: {}, + test: [UP, '21', ENTER, "'42'", ENTER], + expected: [ + prompt, + '2', '1', '21\n', prompt, + "'", '4', '2', "'", "'42'\n", prompt, + ], + clean: false + }, + { // Requires the above test case, because navigating old history + env: {}, + test: [UP, UP, UP, DOWN, ENTER], + expected: [ + prompt, + `${prompt}'42'`, + `${prompt}21`, + prompt, + `${prompt}21`, + '21\n', + prompt, + ] + }, + // Making sure that only the configured number of history items are kept + { + env: { NODE_REPL_HISTORY: historyPath, + NODE_REPL_HISTORY_SIZE: 1 }, + test: [UP, UP, DOWN, CLEAR], + expected: [ + prompt, + `${prompt}'you look fabulous today'`, + prompt, + `${prompt}'you look fabulous today'`, + prompt, + ] + }, + // Making sure that the history file is not written to if it is not writable + { + env: { NODE_REPL_HISTORY: historyPathFail, + NODE_REPL_HISTORY_SIZE: 1 }, + test: [UP], + expected: [prompt, replFailedRead, prompt, replDisabled, prompt] + }, + // Checking the history file permissions + { + before: common.mustCall(function before() { + if (common.isWindows) { + const execSync = require('child_process').execSync; + execSync(`ATTRIB +H "${emptyHiddenHistoryPath}"`); + } + }), + env: { NODE_REPL_HISTORY: emptyHiddenHistoryPath }, + test: [UP], + expected: [prompt] + }, + // Checking failures when os.homedir() fails + { + before: function before() { + // Mock os.homedir() failure + os.homedir = function() { + throw new Error('os.homedir() failure'); + }; + }, + env: {}, + test: [UP], + expected: [prompt, homedirErr, prompt, replDisabled, prompt] + }, + // Checking that the history file can be set to /dev/null + { + before: function before() { + if (!common.isWindows) + fs.symlinkSync('/dev/null', devNullHistoryPath); + }, + env: { NODE_REPL_HISTORY: devNullHistoryPath }, + test: [UP], + expected: [prompt] + }, +]; +const numtests = tests.length; + + +function cleanupTmpFile() { + try { + // Write over the file, clearing any history + fs.writeFileSync(defaultHistoryPath, ''); + } catch (err) { + if (err.code === 'ENOENT') return true; + throw err; + } + return true; +} + +// Copy our fixture to the tmp directory +fs.createReadStream(historyFixturePath) + .pipe(fs.createWriteStream(historyPath)).on('unpipe', () => runTest()); + +const runTestWrap = common.mustCall(runTest, numtests); + +function runTest(assertCleaned) { + const opts = tests.shift(); + if (!opts) return; // All done + + if (assertCleaned) { + try { + assert.strictEqual(fs.readFileSync(defaultHistoryPath, 'utf8'), ''); + } catch (e) { + if (e.code !== 'ENOENT') { + console.error(`Failed test # ${numtests - tests.length}`); + throw e; + } + } + } + + const test = opts.test; + const expected = opts.expected; + const clean = opts.clean; + const before = opts.before; + const size = opts.env.NODE_REPL_HISTORY_SIZE; + const filePath = opts.env.NODE_REPL_HISTORY; + + if (before) before(); + + const repl = REPL.start({ + input: new ActionStream(), + output: new stream.Writable({ + write: common.mustCallAtLeast((chunk, _, next) => { + const output = chunk.toString(); + + // Ignore escapes and blank lines + if (output.charCodeAt(0) === 27 || /^[\r\n]+$/.test(output)) + return next(); + + try { + assert.strictEqual(output, expected.shift()); + } catch (err) { + console.error(`Failed test # ${numtests - tests.length}`); + throw err; + } + next(); + }), + }), + prompt: prompt, + useColors: false, + terminal: true, + }); + + repl.setupHistory({ + size, + filePath, + onHistoryFileLoaded, + removeHistoryDuplicates: false + }); + + function onHistoryFileLoaded(err, repl) { + if (err) { + console.error(`Failed test # ${numtests - tests.length}`); + throw err; + } + + repl.once('close', () => { + if (repl.historyManager.isFlushing) { + repl.once('flushHistory', onClose); + return; + } + + onClose(); + }); + + function onClose() { + const cleaned = clean === false ? false : cleanupTmpFile(); + + try { + // Ensure everything that we expected was output + assert.strictEqual(expected.length, 0); + setImmediate(runTestWrap, cleaned); + } catch (err) { + console.error(`Failed test # ${numtests - tests.length}`); + throw err; + } + } + + repl.inputStream.run(test); + } +} diff --git a/test/js/node/test/parallel/test-repl-programmatic-history.js b/test/js/node/test/parallel/test-repl-programmatic-history.js new file mode 100644 index 000000000000..c2bb6c88e52e --- /dev/null +++ b/test/js/node/test/parallel/test-repl-programmatic-history.js @@ -0,0 +1,264 @@ +'use strict'; + +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const stream = require('stream'); +const REPL = require('repl'); +const assert = require('assert'); +const fs = require('fs'); +const os = require('os'); + +if (process.env.TERM === 'dumb') { + common.skip('skipping - dumb terminal'); +} + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +// Mock os.homedir() +os.homedir = function() { + return tmpdir.path; +}; + +// Create an input stream specialized for testing an array of actions +class ActionStream extends stream.Stream { + run(data) { + const _iter = data[Symbol.iterator](); + const doAction = () => { + const next = _iter.next(); + if (next.done) { + // Close the repl. Note that it must have a clean prompt to do so. + setImmediate(() => { + this.emit('keypress', '', { ctrl: true, name: 'd' }); + }); + return; + } + const action = next.value; + + if (typeof action === 'object') { + this.emit('keypress', '', action); + } else { + this.emit('data', action); + } + setImmediate(doAction); + }; + doAction(); + } + resume() {} + pause() {} +} +ActionStream.prototype.readable = true; + + +// Mock keys +const UP = { name: 'up' }; +const DOWN = { name: 'down' }; +const ENTER = { name: 'enter' }; +const CLEAR = { ctrl: true, name: 'u' }; + +// File paths +const historyFixturePath = fixtures.path('.node_repl_history'); +const historyPath = tmpdir.resolve('.fixture_copy_repl_history'); +const historyPathFail = fixtures.path('nonexistent_folder', 'filename'); +const defaultHistoryPath = tmpdir.resolve('.node_repl_history'); +const emptyHiddenHistoryPath = fixtures.path('.empty-hidden-repl-history-file'); +const devNullHistoryPath = tmpdir.resolve('.dev-null-repl-history-file'); +// Common message bits +const prompt = '> '; +const replDisabled = '\nPersistent history support disabled. Set the ' + + 'NODE_REPL_HISTORY environment\nvariable to a valid, ' + + 'user-writable path to enable.\n'; +const homedirErr = '\nError: Could not get the home directory.\n' + + 'REPL session history will not be persisted.\n'; +const replFailedRead = '\nError: Could not open history file.\n' + + 'REPL session history will not be persisted.\n'; + +const tests = [ + { + env: { NODE_REPL_HISTORY: '' }, + test: [UP], + expected: [prompt, replDisabled, prompt] + }, + { + env: { NODE_REPL_HISTORY: ' ' }, + test: [UP], + expected: [prompt, replDisabled, prompt] + }, + { + env: { NODE_REPL_HISTORY: historyPath }, + test: [UP, CLEAR], + expected: [prompt, `${prompt}'you look fabulous today'`, prompt] + }, + { + env: {}, + test: [UP, '21', ENTER, "'42'", ENTER], + expected: [ + prompt, + '2', '1', '21\n', prompt, + "'", '4', '2', "'", "'42'\n", prompt, + ], + clean: false + }, + { // Requires the above test case + env: {}, + test: [UP, UP, UP, DOWN, ENTER], + expected: [ + prompt, + `${prompt}'42'`, + `${prompt}21`, + prompt, + `${prompt}21`, + '21\n', + prompt, + ] + }, + { + env: { NODE_REPL_HISTORY: historyPath, + NODE_REPL_HISTORY_SIZE: 1 }, + test: [UP, UP, DOWN, CLEAR], + expected: [ + prompt, + `${prompt}'you look fabulous today'`, + prompt, + `${prompt}'you look fabulous today'`, + prompt, + ] + }, + { + env: { NODE_REPL_HISTORY: historyPathFail, + NODE_REPL_HISTORY_SIZE: 1 }, + test: [UP], + expected: [prompt, replFailedRead, prompt, replDisabled, prompt] + }, + { + before: common.mustCall(function before() { + if (common.isWindows) { + const execSync = require('child_process').execSync; + execSync(`ATTRIB +H "${emptyHiddenHistoryPath}"`); + } + }), + env: { NODE_REPL_HISTORY: emptyHiddenHistoryPath }, + test: [UP], + expected: [prompt] + }, + { + before: function before() { + if (!common.isWindows) + fs.symlinkSync('/dev/null', devNullHistoryPath); + }, + env: { NODE_REPL_HISTORY: devNullHistoryPath }, + test: [UP], + expected: [prompt] + }, + { // Make sure this is always the last test, since we change os.homedir() + before: function before() { + // Mock os.homedir() failure + os.homedir = function() { + throw new Error('os.homedir() failure'); + }; + }, + env: {}, + test: [UP], + expected: [prompt, homedirErr, prompt, replDisabled, prompt] + }, +]; +const numtests = tests.length; + + +function cleanupTmpFile() { + try { + // Write over the file, clearing any history + fs.writeFileSync(defaultHistoryPath, ''); + } catch (err) { + if (err.code === 'ENOENT') return true; + throw err; + } + return true; +} + +// Copy our fixture to the tmp directory +fs.createReadStream(historyFixturePath) + .pipe(fs.createWriteStream(historyPath)).on('unpipe', () => runTest()); + +const runTestWrap = common.mustCall(runTest, numtests); + +function runTest(assertCleaned) { + const opts = tests.shift(); + if (!opts) return; // All done + + if (assertCleaned) { + try { + assert.strictEqual(fs.readFileSync(defaultHistoryPath, 'utf8'), ''); + } catch (e) { + if (e.code !== 'ENOENT') { + console.error(`Failed test # ${numtests - tests.length}`); + throw e; + } + } + } + + const test = opts.test; + const expected = opts.expected; + const clean = opts.clean; + const before = opts.before; + const historySize = opts.env.NODE_REPL_HISTORY_SIZE; + const file = opts.env.NODE_REPL_HISTORY; + + if (before) before(); + + const repl = REPL.start({ + input: new ActionStream(), + output: new stream.Writable({ + write: common.mustCallAtLeast((chunk, _, next) => { + const output = chunk.toString(); + + // Ignore escapes and blank lines + if (output.charCodeAt(0) === 27 || /^[\r\n]+$/.test(output)) + return next(); + + try { + assert.strictEqual(output, expected.shift()); + } catch (err) { + console.error(`Failed test # ${numtests - tests.length}`); + throw err; + } + next(); + }), + }), + prompt: prompt, + useColors: false, + terminal: true, + historySize + }); + + repl.setupHistory(file, common.mustCall((err, repl) => { + if (err) { + console.error(`Failed test # ${numtests - tests.length}`); + throw err; + } + + repl.once('close', () => { + if (repl.historyManager.isFlushing) { + repl.once('flushHistory', onClose); + return; + } + + onClose(); + }); + + const onClose = common.mustCall(() => { + const cleaned = clean === false ? false : cleanupTmpFile(); + + try { + // Ensure everything that we expected was output + assert.strictEqual(expected.length, 0); + setImmediate(runTestWrap, cleaned); + } catch (err) { + console.error(`Failed test # ${numtests - tests.length}`); + throw err; + } + }); + + repl.inputStream.run(test); + })); +} diff --git a/test/js/node/test/parallel/test-repl-recoverable.js b/test/js/node/test/parallel/test-repl-recoverable.js new file mode 100644 index 000000000000..74dcd5dfbf58 --- /dev/null +++ b/test/js/node/test/parallel/test-repl-recoverable.js @@ -0,0 +1,41 @@ +'use strict'; + +require('../common'); +const ArrayStream = require('../common/arraystream'); +const assert = require('assert'); +const repl = require('repl'); + +let evalCount = 0; +let recovered = false; +let rendered = false; + +function customEval(code, context, file, cb) { + evalCount++; + + return cb(evalCount === 1 ? new repl.Recoverable() : null, true); +} + +const putIn = new ArrayStream(); + +putIn.write = function(msg) { + if (msg === '| ') { + recovered = true; + } + + if (msg === 'true\n') { + rendered = true; + } +}; + +repl.start('', putIn, customEval); + +// https://github.com/nodejs/node/issues/2939 +// Expose recoverable errors to the consumer. +putIn.emit('data', '1\n'); +putIn.emit('data', '2\n'); + +process.on('exit', function() { + assert(recovered, 'REPL never recovered'); + assert(rendered, 'REPL never rendered the result'); + assert.strictEqual(evalCount, 2); +}); diff --git a/test/js/node/test/parallel/test-repl-require-after-write.js b/test/js/node/test/parallel/test-repl-require-after-write.js new file mode 100644 index 000000000000..f41ba82e9df7 --- /dev/null +++ b/test/js/node/test/parallel/test-repl-require-after-write.js @@ -0,0 +1,31 @@ +'use strict'; + +const common = require('../common'); +const tmpdir = require('../common/tmpdir'); +const assert = require('assert'); +const spawn = require('child_process').spawn; + +tmpdir.refresh(); + +const requirePath = JSON.stringify(tmpdir.resolve('non-existent.json')); + +// Use -i to force node into interactive mode, despite stdout not being a TTY +// bun: '-i' is --install=fallback in bun, so the long form is used instead. +const child = spawn(process.execPath, ['--interactive']); + +let out = ''; +const input = `try { require(${requirePath}); } catch {} ` + + `require('fs').writeFileSync(${requirePath}, '1');` + + `require(${requirePath});`; + +child.stderr.on('data', common.mustNotCall()); + +child.stdout.setEncoding('utf8'); +child.stdout.on('data', (c) => { + out += c; +}); +child.stdout.on('end', common.mustCall(() => { + assert.ok(out.endsWith('> 1\n> ')); +})); + +child.stdin.end(input); diff --git a/test/js/node/test/parallel/test-repl-require-cache.js b/test/js/node/test/parallel/test-repl-require-cache.js new file mode 100644 index 000000000000..b8fe3a753759 --- /dev/null +++ b/test/js/node/test/parallel/test-repl-require-cache.js @@ -0,0 +1,34 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const repl = require('repl'); + +// https://github.com/joyent/node/issues/3226 + +require.cache.something = 1; +assert.strictEqual(require.cache.something, 1); + +repl.start({ useGlobal: false }).close(); + +assert.strictEqual(require.cache.something, 1); diff --git a/test/js/node/test/parallel/test-repl-require-context.js b/test/js/node/test/parallel/test-repl-require-context.js new file mode 100644 index 000000000000..070ec727537f --- /dev/null +++ b/test/js/node/test/parallel/test-repl-require-context.js @@ -0,0 +1,23 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const cp = require('child_process'); +const child = cp.spawn(process.execPath, ['--interactive']); +const fixtures = require('../common/fixtures'); +const fixture = fixtures.path('is-object.js').replace(/\\/g, '/'); +let output = ''; + +child.stdout.setEncoding('utf8'); +child.stdout.on('data', (data) => { + output += data; +}); + +child.on('exit', common.mustCall(() => { + const results = output.replace(/^> /mg, '').split('\n').slice(2); + assert.deepStrictEqual(results, ['undefined', 'true', 'true', '']); +})); + +child.stdin.write('const isObject = (obj) => obj.constructor === Object;\n'); +child.stdin.write('isObject({});\n'); +child.stdin.write(`require(${JSON.stringify(fixture)}).isObject({});\n`); +child.stdin.write('.exit\n'); diff --git a/test/js/node/test/parallel/test-repl-require-self-referential.js b/test/js/node/test/parallel/test-repl-require-self-referential.js new file mode 100644 index 000000000000..e22e2cfe883d --- /dev/null +++ b/test/js/node/test/parallel/test-repl-require-self-referential.js @@ -0,0 +1,26 @@ +'use strict'; + +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const { spawn } = require('child_process'); +const { isMainThread } = require('worker_threads'); + +if (!isMainThread) { + common.skip('process.chdir is not available in Workers'); +} + +const selfRefModule = fixtures.path('self_ref_module'); +const child = spawn(process.execPath, + ['--interactive'], + { cwd: selfRefModule } +); +let output = ''; +child.stdout.on('data', (chunk) => output += chunk); +child.on('exit', common.mustCall(() => { + const results = output.replace(/^> /mg, '').split('\n').slice(2); + assert.deepStrictEqual(results, [ "'Self resolution working'", '' ]); +})); + +child.stdin.write('require("self_ref");\n'); +child.stdin.write('.exit\n'); diff --git a/test/js/node/test/parallel/test-repl-reset-event.js b/test/js/node/test/parallel/test-repl-reset-event.js new file mode 100644 index 000000000000..195ff581fe78 --- /dev/null +++ b/test/js/node/test/parallel/test-repl-reset-event.js @@ -0,0 +1,63 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const util = require('util'); +const { startNewREPLServer } = require('../common/repl'); + +common.allowGlobals(42); + +function testReset(cb) { + const { replServer } = startNewREPLServer(); + replServer.context.foo = 42; + replServer.on('reset', common.mustCall(function(context) { + assert(!!context, 'REPL did not emit a context with reset event'); + assert.strictEqual(context, replServer.context, 'REPL emitted incorrect context. ' + + `context is ${util.inspect(context)}, expected ${util.inspect(replServer.context)}`); + assert.strictEqual( + context.foo, + undefined, + 'REPL emitted the previous context and is not using global as context. ' + + `context.foo is ${context.foo}, expected undefined.` + ); + context.foo = 42; + cb(); + })); + replServer.resetContext(); +} + +function testResetGlobal() { + const { replServer } = startNewREPLServer({ useGlobal: true }); + replServer.context.foo = 42; + replServer.on('reset', common.mustCall(function(context) { + assert.strictEqual( + context.foo, + 42, + '"foo" property is different from REPL using global as context. ' + + `context.foo is ${context.foo}, expected 42.` + ); + })); + replServer.resetContext(); +} + +testReset(common.mustCall(testResetGlobal)); diff --git a/test/js/node/test/parallel/test-repl-save-load-editor-mode.js b/test/js/node/test/parallel/test-repl-save-load-editor-mode.js new file mode 100644 index 000000000000..83a57cdaa55a --- /dev/null +++ b/test/js/node/test/parallel/test-repl-save-load-editor-mode.js @@ -0,0 +1,35 @@ +'use strict'; + +require('../common'); +const assert = require('node:assert'); +const fs = require('node:fs'); +const path = require('node:path'); +const { startNewREPLServer } = require('../common/repl'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +// Test for saving a REPL session in editor mode + +const { replServer, input } = startNewREPLServer(); + +input.run(['.editor']); + +const commands = [ + 'function testSave() {', + 'return "saved";', + '}', +]; + +input.run(commands); + +replServer.write('', { ctrl: true, name: 'd' }); + +const filePath = path.resolve(tmpdir.path, 'test.save.js'); + +input.run([`.save ${filePath}`]); + +assert.strictEqual(fs.readFileSync(filePath, 'utf8'), + `${commands.join('\n')}\n`); + +replServer.close(); diff --git a/test/js/node/test/parallel/test-repl-save-load-invalid-save.js b/test/js/node/test/parallel/test-repl-save-load-invalid-save.js new file mode 100644 index 000000000000..60039ee5b15a --- /dev/null +++ b/test/js/node/test/parallel/test-repl-save-load-invalid-save.js @@ -0,0 +1,25 @@ +'use strict'; + +const common = require('../common'); +const assert = require('node:assert'); +const { startNewREPLServer } = require('../common/repl'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +// Test for the appropriate handling of cases in which REPL saves fail + +const { replServer, input, output } = startNewREPLServer({ terminal: false }); + +// NUL (\0) is disallowed in filenames in UNIX-like operating systems and +// Windows so we can use that to test failed saves. +const invalidFilePath = tmpdir.resolve('\0\0\0\0\0'); + +output.write = common.mustCall(function(data) { + assert.strictEqual(data, `Failed to save: ${invalidFilePath}\n`); + output.write = () => {}; +}); + +input.run([`.save ${invalidFilePath}`]); + +replServer.close(); diff --git a/test/js/node/test/parallel/test-repl-save-load-load-dir.js b/test/js/node/test/parallel/test-repl-save-load-load-dir.js new file mode 100644 index 000000000000..20bfae436a9c --- /dev/null +++ b/test/js/node/test/parallel/test-repl-save-load-load-dir.js @@ -0,0 +1,23 @@ +'use strict'; + +const common = require('../common'); +const { startNewREPLServer } = require('../common/repl'); +const assert = require('node:assert'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +// Tests that an appropriate error is displayed if the user tries to load a directory instead of a file + +const { replServer, input, output } = startNewREPLServer({ terminal: false }); + +const dirPath = tmpdir.path; + +output.write = common.mustCall(function(data) { + assert.strictEqual(data, `Failed to load: ${dirPath} is not a valid file\n`); + output.write = () => {}; +}); + +input.run([`.load ${dirPath}`]); + +replServer.close(); diff --git a/test/js/node/test/parallel/test-repl-save-load-load-non-existent.js b/test/js/node/test/parallel/test-repl-save-load-load-non-existent.js new file mode 100644 index 000000000000..1456316ae447 --- /dev/null +++ b/test/js/node/test/parallel/test-repl-save-load-load-non-existent.js @@ -0,0 +1,23 @@ +'use strict'; + +const common = require('../common'); +const { startNewREPLServer } = require('../common/repl'); +const assert = require('node:assert'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +// Tests that an appropriate error is displayed if the user tries to load a non existent file + +const { replServer, input, output } = startNewREPLServer({ terminal: false }); + +const filePath = tmpdir.resolve('file.does.not.exist'); + +output.write = common.mustCall(function(data) { + assert.strictEqual(data, `Failed to load: ${filePath}\n`); + output.write = () => {}; +}); + +input.run([`.load ${filePath}`]); + +replServer.close(); diff --git a/test/js/node/test/parallel/test-repl-save-load-load-without-name.js b/test/js/node/test/parallel/test-repl-save-load-load-without-name.js new file mode 100644 index 000000000000..b52b7f37015a --- /dev/null +++ b/test/js/node/test/parallel/test-repl-save-load-load-without-name.js @@ -0,0 +1,21 @@ +'use strict'; + +const common = require('../common'); +const { startNewREPLServer } = require('../common/repl'); +const assert = require('node:assert'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +// Tests that an appropriate error is displayed if .load is called without a filename + +const { replServer, input, output } = startNewREPLServer({ terminal: false }); + +output.write = common.mustCall(function(data) { + assert.strictEqual(data, 'The "file" argument must be specified\n'); + output.write = () => {}; +}); + +input.run(['.load']); + +replServer.close(); diff --git a/test/js/node/test/parallel/test-repl-save-load-save-without-name.js b/test/js/node/test/parallel/test-repl-save-load-save-without-name.js new file mode 100644 index 000000000000..d12e5f2dd459 --- /dev/null +++ b/test/js/node/test/parallel/test-repl-save-load-save-without-name.js @@ -0,0 +1,21 @@ +'use strict'; + +const common = require('../common'); +const { startNewREPLServer } = require('../common/repl'); +const assert = require('node:assert'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +// Tests that an appropriate error is displayed if .save is called without a filename + +const { replServer, input, output } = startNewREPLServer({ terminal: false }); + +output.write = common.mustCall(function(data) { + assert.strictEqual(data, 'The "file" argument must be specified\n'); + output.write = () => {}; +}); + +input.run(['.save']); + +replServer.close(); diff --git a/test/js/node/test/parallel/test-repl-save-load.js b/test/js/node/test/parallel/test-repl-save-load.js new file mode 100644 index 000000000000..d8401c2d4278 --- /dev/null +++ b/test/js/node/test/parallel/test-repl-save-load.js @@ -0,0 +1,78 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; + +const common = require('../common'); +const { startNewREPLServer } = require('../common/repl'); +const assert = require('node:assert'); +const fs = require('node:fs'); +const path = require('node:path'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +// Tests that a REPL session data can be saved to and loaded from a file + +const { replServer, input } = startNewREPLServer({ terminal: false }); + +const filePath = path.resolve(tmpdir.path, 'test.save.js'); + +const testFileContents = [ + 'let inner = (function() {', + ' return {one:1};', + '})()', +]; + +input.run(testFileContents); +input.run([`.save ${filePath}`]); + +assert.strictEqual(fs.readFileSync(filePath, 'utf8'), + testFileContents.join('\n')); + +const innerOCompletions = [['inner.one'], 'inner.o']; + +// Double check that the data is still present in the repl after the save +replServer.completer('inner.o', common.mustSucceed((data) => { + assert.deepStrictEqual(data, innerOCompletions); +})); + +// Clear the repl context +input.run(['.clear']); + +// Double check that the data is no longer present in the repl +replServer.completer('inner.o', common.mustSucceed((data) => { + assert.deepStrictEqual(data, [[], 'inner.o']); +})); + +// Load the file back in. +input.run([`.load ${filePath}`]); + +// Make sure loading doesn't insert extra indentation +// https://github.com/nodejs/node/issues/47673 +assert.strictEqual(replServer.line, ''); + +// Make sure that the loaded data is present +replServer.complete('inner.o', common.mustSucceed((data) => { + assert.deepStrictEqual(data, innerOCompletions); +})); + +replServer.close(); diff --git a/test/js/node/test/parallel/test-repl-setprompt.js b/test/js/node/test/parallel/test-repl-setprompt.js new file mode 100644 index 000000000000..9901f8f974f6 --- /dev/null +++ b/test/js/node/test/parallel/test-repl-setprompt.js @@ -0,0 +1,49 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const spawn = require('child_process').spawn; +const os = require('os'); + +const args = [ + '-e', + 'var e = new (require("repl")).REPLServer("foo.. "); e.context.e = e;', +]; + +const p = 'bar.. '; + +const child = spawn(process.execPath, args); + +child.stdout.setEncoding('utf8'); + +let data = ''; +child.stdout.on('data', function(d) { data += d; }); + +child.stdin.end(`e.setPrompt("${p}");${os.EOL}`); + +child.on('close', common.mustCall((code, signal) => { + assert.strictEqual(code, 0); + assert.ok(!signal); + const lines = data.split('\n'); + assert.strictEqual(lines.pop(), p); +})); diff --git a/test/js/node/test/parallel/test-repl-sigint-nested-eval.js b/test/js/node/test/parallel/test-repl-sigint-nested-eval.js new file mode 100644 index 000000000000..d71324310f0b --- /dev/null +++ b/test/js/node/test/parallel/test-repl-sigint-nested-eval.js @@ -0,0 +1,55 @@ +'use strict'; +const common = require('../common'); +if (common.isWindows) { + // No way to send CTRL_C_EVENT to processes from JS right now. + common.skip('platform not supported'); +} + +const { isMainThread } = require('worker_threads'); + +if (!isMainThread) { + common.skip('No signal handling available in Workers'); +} + +const assert = require('assert'); +const spawn = require('child_process').spawn; + +// bun: upstream uses '-i'; that short flag is --install=fallback in bun, so the +// REPL is reached through the long form. +const child = spawn(process.execPath, [ '--interactive' ], { + stdio: [null, null, 2, 'ipc'] +}); + +let stdout = ''; +child.stdout.setEncoding('utf8'); +child.stdout.on('data', function(c) { + stdout += c; +}); + +child.stdout.once('data', common.mustCall(() => { + child.on('message', common.mustCall((msg) => { + assert.strictEqual(msg, 'repl is busy'); + process.kill(child.pid, 'SIGINT'); + child.stdout.once('data', common.mustCall(() => { + // Make sure REPL still works. + child.stdin.end('"foobar"\n'); + })); + })); + + child.stdin.write( + 'vm.runInThisContext("process.send(\'repl is busy\'); while(true){}", ' + + '{ breakOnSigint: true });\n' + ); +})); + +child.on('close', common.mustCall((code) => { + const expected = 'Script execution was interrupted by `SIGINT`'; + assert.ok( + stdout.includes(expected), + `Expected stdout to contain "${expected}", got ${stdout}` + ); + assert.ok( + stdout.includes('foobar'), + `Expected stdout to contain "foobar", got ${stdout}` + ); +})); diff --git a/test/js/node/test/parallel/test-repl-sigint.js b/test/js/node/test/parallel/test-repl-sigint.js new file mode 100644 index 000000000000..f50b467d2663 --- /dev/null +++ b/test/js/node/test/parallel/test-repl-sigint.js @@ -0,0 +1,55 @@ +'use strict'; +const common = require('../common'); +if (common.isWindows) { + // No way to send CTRL_C_EVENT to processes from JS right now. + common.skip('platform not supported'); +} + +const { isMainThread } = require('worker_threads'); + +if (!isMainThread) { + common.skip('No signal handling available in Workers'); +} + +const assert = require('assert'); +const spawn = require('child_process').spawn; + +process.env.REPL_TEST_PPID = process.pid; +// bun: upstream uses '-i'; that short flag is --install=fallback in bun, so the +// REPL is reached through the long form. +const child = spawn(process.execPath, [ '--interactive' ], { + stdio: [null, null, 2] +}); + +let stdout = ''; +child.stdout.setEncoding('utf8'); +child.stdout.on('data', function(c) { + stdout += c; +}); + +child.stdout.once('data', common.mustCall(() => { + process.on('SIGUSR2', common.mustCall(() => { + process.kill(child.pid, 'SIGINT'); + child.stdout.once('data', common.mustCall(() => { + // Make sure state from before the interruption is still available. + child.stdin.end('a*2*3*7\n'); + })); + })); + + child.stdin.write('a = 1001;' + + 'process.kill(+process.env.REPL_TEST_PPID, "SIGUSR2");' + + 'while(true){}\n'); +})); + +child.on('close', common.mustCall((code) => { + assert.strictEqual(code, 0); + const expected = 'Script execution was interrupted by `SIGINT`'; + assert.ok( + stdout.includes(expected), + `Expected stdout to contain "${expected}", got ${stdout}` + ); + assert.ok( + stdout.includes('42042\n'), + `Expected stdout to contain "42042", got ${stdout}` + ); +})); diff --git a/test/js/node/test/parallel/test-repl-stdin-push-null.js b/test/js/node/test/parallel/test-repl-stdin-push-null.js new file mode 100644 index 000000000000..53ba9ff7c331 --- /dev/null +++ b/test/js/node/test/parallel/test-repl-stdin-push-null.js @@ -0,0 +1,9 @@ +'use strict'; +const common = require('../common'); + +if (!process.stdin.isTTY) { + common.skip('does not apply on non-TTY stdin'); +} + +process.stdin.destroy(); +process.stdin.setRawMode(true); diff --git a/test/js/node/test/parallel/test-repl-syntax-error-stack.js b/test/js/node/test/parallel/test-repl-syntax-error-stack.js new file mode 100644 index 000000000000..16bf27d045bc --- /dev/null +++ b/test/js/node/test/parallel/test-repl-syntax-error-stack.js @@ -0,0 +1,32 @@ +'use strict'; + +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const { startNewREPLServer } = require('../common/repl'); + +let found = false; + +process.on('exit', () => { + assert.strictEqual(found, true); +}); + +const { input, output } = startNewREPLServer(); + +output.write = (data) => { + // Matching only on a minimal piece of the stack because the string will vary + // greatly depending on the JavaScript engine. V8 includes `;` because it + // displays the line of code (`var foo bar;`) that is causing a problem. + // ChakraCore does not display the line of code but includes `;` in the phrase + // `Expected ';' `. + if (/;/.test(data)) + found = true; +}; + +let file = fixtures.path('syntax', 'bad_syntax'); + +if (common.isWindows) + file = file.replace(/\\/g, '\\\\'); + +input.run(['.clear']); +input.run([`require('${file}');`]); diff --git a/test/js/node/test/parallel/test-repl-tab-complete-buffer.js b/test/js/node/test/parallel/test-repl-tab-complete-buffer.js new file mode 100644 index 000000000000..25a5dc6fe6c8 --- /dev/null +++ b/test/js/node/test/parallel/test-repl-tab-complete-buffer.js @@ -0,0 +1,62 @@ +'use strict'; + +const common = require('../common'); +const { hijackStderr, restoreStderr } = require('../common/hijackstdio'); +const assert = require('assert'); +const { startNewREPLServer } = require('../common/repl'); + +const { replServer, input } = startNewREPLServer(); + +for (const type of [ + Array, + Buffer, + + Uint8Array, + Uint16Array, + Uint32Array, + + Uint8ClampedArray, + Int8Array, + Int16Array, + Int32Array, + Float32Array, + Float64Array, +]) { + input.run(['.clear']); + + if (type === Array) { + input.run([ + 'var ele = [];', + 'for (let i = 0; i < 1e6 + 1; i++) ele[i] = 0;', + 'ele.biu = 1;', + ]); + } else if (type === Buffer) { + input.run(['var ele = Buffer.alloc(1e6 + 1); ele.biu = 1;']); + } else { + input.run([`var ele = new ${type.name}(1e6 + 1); ele.biu = 1;`]); + } + + hijackStderr(common.mustNotCall()); + replServer.complete( + 'ele.', + common.mustCall((err, data) => { + restoreStderr(); + assert.ifError(err); + + const ele = + type === Array ? [] : type === Buffer ? Buffer.alloc(0) : new type(0); + + assert.strictEqual(data[0].includes('ele.biu'), true); + + for (const key of data[0]) { + if (!key || key === 'ele.biu') return; + assert.notStrictEqual(ele[key.slice(4)], undefined); + } + }) + ); +} + +// check Buffer.prototype.length not crashing. +// Refs: https://github.com/nodejs/node/pull/11961 +input.run(['.clear']); +replServer.complete('Buffer.prototype.', common.mustCall()); diff --git a/test/js/node/test/parallel/test-repl-tab-complete-computed-props.js b/test/js/node/test/parallel/test-repl-tab-complete-computed-props.js new file mode 100644 index 000000000000..418dc5059e91 --- /dev/null +++ b/test/js/node/test/parallel/test-repl-tab-complete-computed-props.js @@ -0,0 +1,142 @@ +'use strict'; + +const common = require('../common'); +const { startNewREPLServer } = require('../common/repl'); +const { describe, it, before, after } = require('node:test'); +const assert = require('assert'); + +function testCompletion(replServer, { input, expectedCompletions }) { + replServer.complete( + input, + common.mustCall((_error, data) => { + assert.deepStrictEqual(data, [expectedCompletions, input]); + }), + ); +}; + +describe('REPL tab object completion on computed properties', () => { + describe('simple string cases', () => { + let replServer; + + before(() => { + const { replServer: server, input } = startNewREPLServer(); + replServer = server; + + input.run([ + ` + const obj = { + one: 1, + innerObj: { two: 2 }, + 'inner object': { three: 3 }, + }; + + const oneStr = 'one'; + `, + ]); + }); + + after(() => { + replServer.close(); + }); + + it('works with double quoted strings', () => testCompletion(replServer, { + input: 'obj["one"].toFi', + expectedCompletions: ['obj["one"].toFixed'], + })); + + it('works with single quoted strings', () => testCompletion(replServer, { + input: "obj['one'].toFi", + expectedCompletions: ["obj['one'].toFixed"], + })); + + it('works with template strings', () => testCompletion(replServer, { + input: 'obj[`one`].toFi', + expectedCompletions: ['obj[`one`].toFixed'], + })); + + it('works with nested objects', () => { + testCompletion(replServer, { + input: 'obj["innerObj"].tw', + expectedCompletions: ['obj["innerObj"].two'], + }); + testCompletion(replServer, { + input: 'obj["innerObj"].two.tofi', + expectedCompletions: ['obj["innerObj"].two.toFixed'], + }); + }); + + it('works with nested objects combining different type of strings', () => testCompletion(replServer, { + input: 'obj["innerObj"][`two`].tofi', + expectedCompletions: ['obj["innerObj"][`two`].toFixed'], + })); + + it('works with strings with spaces', () => testCompletion(replServer, { + input: 'obj["inner object"].th', + expectedCompletions: ['obj["inner object"].three'], + })); + }); + + describe('variables as indexes', () => { + let replServer; + + before(() => { + const { replServer: server, input } = startNewREPLServer(); + replServer = server; + + input.run([ + ` + const oneStr = 'One'; + const helloWorldStr = 'Hello' + ' ' + 'World'; + + const obj = { + [oneStr]: 1, + ['Hello World']: 'hello world!', + }; + + const lookupObj = { + stringLookup: helloWorldStr, + ['number lookup']: oneStr, + }; + `, + ]); + }); + + after(() => { + replServer.close(); + }); + + it('works with a simple variable', () => testCompletion(replServer, { + input: 'obj[oneStr].toFi', + expectedCompletions: ['obj[oneStr].toFixed'], + })); + + it('works with a computed variable', () => testCompletion(replServer, { + input: 'obj[helloWorldStr].tolocaleup', + expectedCompletions: ['obj[helloWorldStr].toLocaleUpperCase'], + })); + + it('works with a simple inlined computed property', () => testCompletion(replServer, { + input: 'obj["Hello " + "World"].tolocaleup', + expectedCompletions: ['obj["Hello " + "World"].toLocaleUpperCase'], + })); + + it('works with a ternary inlined computed property', () => testCompletion(replServer, { + input: 'obj[(1 + 2 > 5) ? oneStr : "Hello " + "World"].toLocaleUpperCase', + expectedCompletions: ['obj[(1 + 2 > 5) ? oneStr : "Hello " + "World"].toLocaleUpperCase'], + })); + + it('works with an inlined computed property with a nested property lookup', () => + testCompletion(replServer, { + input: 'obj[lookupObj.stringLookup].tolocaleupp', + expectedCompletions: ['obj[lookupObj.stringLookup].toLocaleUpperCase'], + }) + ); + + it('works with an inlined computed property with a nested inlined computer property lookup', () => + testCompletion(replServer, { + input: 'obj[lookupObj["number" + " lookup"]].toFi', + expectedCompletions: ['obj[lookupObj["number" + " lookup"]].toFixed'], + }) + ); + }); +}); diff --git a/test/js/node/test/parallel/test-repl-tab-complete-crash.js b/test/js/node/test/parallel/test-repl-tab-complete-crash.js new file mode 100644 index 000000000000..29f75028bdac --- /dev/null +++ b/test/js/node/test/parallel/test-repl-tab-complete-crash.js @@ -0,0 +1,21 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { startNewREPLServer } = require('../common/repl'); + +const { replServer, input } = startNewREPLServer(); + +// https://github.com/nodejs/node/issues/3346 +// Tab-completion should be empty +input.run(['.clear', 'function () {']); +replServer.complete('arguments.', common.mustCall((err, completions) => { + assert.strictEqual(err, null); + assert.deepStrictEqual(completions, [[], 'arguments.']); +})); + +input.run(['.clear', 'function () {', 'undef;']); +replServer.complete('undef.', common.mustCall((err, completions) => { + assert.strictEqual(err, null); + assert.deepStrictEqual(completions, [[], 'undef.']); +})); diff --git a/test/js/node/test/parallel/test-repl-tab-complete-custom-completer.js b/test/js/node/test/parallel/test-repl-tab-complete-custom-completer.js new file mode 100644 index 000000000000..1599331f2a2b --- /dev/null +++ b/test/js/node/test/parallel/test-repl-tab-complete-custom-completer.js @@ -0,0 +1,65 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { startNewREPLServer } = require('../common/repl'); + +// To test custom completer function. +// Sync mode. +{ + const customCompletions = 'aaa aa1 aa2 bbb bb1 bb2 bb3 ccc ddd eee'.split(' '); + const { replServer } = startNewREPLServer({ + completer: function completer(line) { + const hits = customCompletions.filter((c) => c.startsWith(line)); + // Show all completions if none found. + return [hits.length ? hits : customCompletions, line]; + } + }); + + // On empty line should output all the custom completions + // without complete anything. + replServer.complete('', common.mustCall((error, data) => { + assert.deepStrictEqual(data, [ + customCompletions, + '', + ]); + })); + + // On `a` should output `aaa aa1 aa2` and complete until `aa`. + replServer.complete('a', common.mustCall((error, data) => { + assert.deepStrictEqual(data, [ + 'aaa aa1 aa2'.split(' '), + 'a', + ]); + })); +} + +// To test custom completer function. +// Async mode. +{ + const customCompletions = 'aaa aa1 aa2 bbb bb1 bb2 bb3 ccc ddd eee'.split(' '); + const { replServer } = startNewREPLServer({ + completer: function completer(line, callback) { + const hits = customCompletions.filter((c) => c.startsWith(line)); + // Show all completions if none found. + callback(null, [hits.length ? hits : customCompletions, line]); + } + }); + + // On empty line should output all the custom completions + // without complete anything. + replServer.complete('', common.mustCall((error, data) => { + assert.deepStrictEqual(data, [ + customCompletions, + '', + ]); + })); + + // On `a` should output `aaa aa1 aa2` and complete until `aa`. + replServer.complete('a', common.mustCall((error, data) => { + assert.deepStrictEqual(data, [ + 'aaa aa1 aa2'.split(' '), + 'a', + ]); + })); +} diff --git a/test/js/node/test/parallel/test-repl-tab-complete-files.js b/test/js/node/test/parallel/test-repl-tab-complete-files.js new file mode 100644 index 000000000000..ddb3df07176b --- /dev/null +++ b/test/js/node/test/parallel/test-repl-tab-complete-files.js @@ -0,0 +1,70 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const path = require('path'); +const { startNewREPLServer } = require('../common/repl'); + +const { isMainThread } = require('worker_threads'); + +if (!isMainThread) { + common.skip('process.chdir is not available in Workers'); +} + +const { replServer } = startNewREPLServer(); + +// Tab completion for files/directories +{ + process.chdir(__dirname); + + const readFileSyncs = ['fs.readFileSync("', 'fs.promises.readFileSync("']; + if (!common.isWindows) { + readFileSyncs.forEach((readFileSync) => { + const fixturePath = `${readFileSync}../fixtures/test-repl-tab-completion`; + replServer.complete( + fixturePath, + common.mustCall((err, data) => { + assert.strictEqual(err, null); + assert.ok(data[0][0].includes('.hiddenfiles')); + assert.ok(data[0][1].includes('hellorandom.txt')); + assert.ok(data[0][2].includes('helloworld.js')); + }) + ); + + replServer.complete( + `${fixturePath}/hello`, + common.mustCall((err, data) => { + assert.strictEqual(err, null); + assert.ok(data[0][0].includes('hellorandom.txt')); + assert.ok(data[0][1].includes('helloworld.js')); + }) + ); + + replServer.complete( + `${fixturePath}/.h`, + common.mustCall((err, data) => { + assert.strictEqual(err, null); + assert.ok(data[0][0].includes('.hiddenfiles')); + }) + ); + + replServer.complete( + `${readFileSync}./xxxRandom/random`, + common.mustCall((err, data) => { + assert.strictEqual(err, null); + assert.strictEqual(data[0].length, 0); + }) + ); + + const testPath = fixturePath.slice(0, -1); + replServer.complete( + testPath, + common.mustCall((err, data) => { + assert.strictEqual(err, null); + assert.ok(data[0][0].includes('test-repl-tab-completion')); + assert.strictEqual(data[1], path.basename(testPath)); + }) + ); + }); + } +} diff --git a/test/js/node/test/parallel/test-repl-tab-complete-nested-repls.js b/test/js/node/test/parallel/test-repl-tab-complete-nested-repls.js new file mode 100644 index 000000000000..3cac02f20562 --- /dev/null +++ b/test/js/node/test/parallel/test-repl-tab-complete-nested-repls.js @@ -0,0 +1,23 @@ +// Tab completion sometimes uses a separate REPL instance under the hood. +// That REPL instance has its own domain. Make sure domain errors trickle back +// up to the main REPL. +// +// Ref: https://github.com/nodejs/node/issues/21586 + +'use strict'; + +require('../common'); +const fixtures = require('../common/fixtures'); + +const assert = require('assert'); +const { spawnSync } = require('child_process'); + +const testFile = fixtures.path('repl-tab-completion-nested-repls.js'); +const result = spawnSync(process.execPath, [testFile]); + +// The spawned process will fail. In Node.js 10.11.0, it will fail silently. The +// test here is to make sure that the error information bubbles up to the +// calling process. +assert.ok(result.status, 'testFile swallowed its error'); +const err = result.stderr.toString(); +assert.ok(err.includes('fhqwhgads'), err); diff --git a/test/js/node/test/parallel/test-repl-tab-complete-new-expression.js b/test/js/node/test/parallel/test-repl-tab-complete-new-expression.js new file mode 100644 index 000000000000..ef269f44b769 --- /dev/null +++ b/test/js/node/test/parallel/test-repl-tab-complete-new-expression.js @@ -0,0 +1,41 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { startNewREPLServer } = require('../common/repl'); +const { describe, it } = require('node:test'); + +// This test verifies that tab completion works correctly with `new` operator +// for a class. Property access has higher precedence than `new` so the properties +// should be displayed as autocompletion result. + +describe('REPL tab completion with new expressions', () => { + it('should output completion of class properties', () => { + const { replServer, input } = startNewREPLServer({ terminal: false }); + + input.run([ + ` + class X { x = 1 }; + X.Y = class Y { y = 2 }; + `, + ]); + + // Handle completion for property of root class. + replServer.complete( + 'new X.', + common.mustSucceed((completions) => { + assert.strictEqual(completions[1], 'X.'); + }) + ); + + // Handle completion for property with another class as value. + replServer.complete( + 'new X.Y.', + common.mustSucceed((completions) => { + assert.strictEqual(completions[1], 'X.Y.'); + }) + ); + + replServer.close(); + }); +}); diff --git a/test/js/node/test/parallel/test-repl-tab-complete-no-warn.js b/test/js/node/test/parallel/test-repl-tab-complete-no-warn.js new file mode 100644 index 000000000000..df995aba6a4e --- /dev/null +++ b/test/js/node/test/parallel/test-repl-tab-complete-no-warn.js @@ -0,0 +1,17 @@ +'use strict'; + +const common = require('../common'); +const { startNewREPLServer } = require('../common/repl'); +const DEFAULT_MAX_LISTENERS = require('events').defaultMaxListeners; + +const { replServer, input } = startNewREPLServer(); + +// https://github.com/nodejs/node/issues/18284 +// Tab-completion should not repeatedly add the +// `Runtime.executionContextCreated` listener +process.on('warning', common.mustNotCall()); + +input.run(['async function test() {']); +for (let i = 0; i < DEFAULT_MAX_LISTENERS; i++) { + replServer.complete('await Promise.resolve()', () => {}); +} diff --git a/test/js/node/test/parallel/test-repl-tab-complete-nosideeffects.js b/test/js/node/test/parallel/test-repl-tab-complete-nosideeffects.js new file mode 100644 index 000000000000..54562e2f1d29 --- /dev/null +++ b/test/js/node/test/parallel/test-repl-tab-complete-nosideeffects.js @@ -0,0 +1,39 @@ +'use strict'; + +const common = require('../common'); +const { describe, it } = require('node:test'); +const assert = require('assert'); +const { startNewREPLServer } = require('../common/repl'); + +function getNoResultsFunction() { + return common.mustSucceed((data) => { + assert.deepStrictEqual(data[0], []); + }); +} + +describe('REPL tab completion without side effects', () => { + const setup = [ + 'globalThis.counter = 0;', + 'function incCounter() { return counter++; }', + 'const arr = [{ bar: "baz" }];', + ]; + // None of these expressions should affect the value of `counter` + for (const code of [ + 'incCounter().', + 'a=(counter+=1).foo.', + 'a=(counter++).foo.', + 'for((counter)of[1])foo.', + 'for((counter)in{1:1})foo.', + 'arr[incCounter()].b', + ]) { + it(`does not evaluate with side effects (${code})`, async () => { + const { replServer, input } = startNewREPLServer(); + input.run(setup); + + replServer.complete(code, getNoResultsFunction()); + + assert.strictEqual(replServer.context.counter, 0); + replServer.close(); + }); + } +}); diff --git a/test/js/node/test/parallel/test-repl-tab-complete-on-editor-mode.js b/test/js/node/test/parallel/test-repl-tab-complete-on-editor-mode.js new file mode 100644 index 000000000000..6e2ef8b5670d --- /dev/null +++ b/test/js/node/test/parallel/test-repl-tab-complete-on-editor-mode.js @@ -0,0 +1,35 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { startNewREPLServer } = require('../common/repl'); + +// Tab completion in editor mode +{ + const { replServer, input } = startNewREPLServer(); + + input.run(['.clear', '.editor']); + + replServer.completer('Uin', common.mustCall((_error, data) => { + assert.deepStrictEqual(data, [['Uint'], 'Uin']); + })); + + input.run(['.clear', '.editor']); + + replServer.completer('var log = console.l', common.mustCall((_error, data) => { + assert.deepStrictEqual(data, [['console.log'], 'console.l']); + })); +} + +// Regression test for https://github.com/nodejs/node/issues/43528 +{ + const { replServer } = startNewREPLServer(); + + // Editor mode + replServer.write('.editor\n'); + + replServer.write('a'); + replServer.write(null, { name: 'tab' }); // Should not throw + + replServer.close(); +} diff --git a/test/js/node/test/parallel/test-repl-tab.js b/test/js/node/test/parallel/test-repl-tab.js new file mode 100644 index 000000000000..710fca9fae2d --- /dev/null +++ b/test/js/node/test/parallel/test-repl-tab.js @@ -0,0 +1,13 @@ +'use strict'; +const common = require('../common'); +const repl = require('repl'); +const zlib = require('zlib'); + +// Just use builtin stream inherited from Duplex +const putIn = zlib.createGzip(); +const testMe = repl.start('', putIn, function(cmd, context, filename, + callback) { + callback(null, cmd); +}); + +testMe.complete('', common.mustSucceed()); diff --git a/test/js/node/test/parallel/test-repl-throw-null-or-undefined.js b/test/js/node/test/parallel/test-repl-throw-null-or-undefined.js new file mode 100644 index 000000000000..3b4657ce98c0 --- /dev/null +++ b/test/js/node/test/parallel/test-repl-throw-null-or-undefined.js @@ -0,0 +1,13 @@ +'use strict'; +require('../common'); + +// This test ensures that the repl does not +// crash or emit error when throwing `null|undefined` +// ie `throw null` or `throw undefined`. + +const r = require('repl').start(); + +// Should not throw. +r.write('throw null\n'); +r.write('throw undefined\n'); +r.write('.exit\n'); diff --git a/test/js/node/test/parallel/test-repl-uncaught-exception-after-input-ended.js b/test/js/node/test/parallel/test-repl-uncaught-exception-after-input-ended.js new file mode 100644 index 000000000000..1e2ca86a9f07 --- /dev/null +++ b/test/js/node/test/parallel/test-repl-uncaught-exception-after-input-ended.js @@ -0,0 +1,23 @@ +'use strict'; +const common = require('../common'); +const { start } = require('node:repl'); +const { PassThrough } = require('node:stream'); +const assert = require('node:assert'); + +// This test verifies that uncaught exceptions in the REPL +// do not bring down the process, even if stdin may already +// have been ended at that point (and the REPL closed as +// a result of that). +const input = new PassThrough(); +const output = new PassThrough().setEncoding('utf8'); +start({ + input, + output, + terminal: false, +}); + +input.end('setImmediate(() => { throw new Error("test"); });\n'); + +setImmediate(common.mustCall(() => { + assert.match(output.read(), /Uncaught Error: test/); +})); diff --git a/test/js/node/test/parallel/test-repl-uncaught-exception-evalcallback.js b/test/js/node/test/parallel/test-repl-uncaught-exception-evalcallback.js new file mode 100644 index 000000000000..844fce6995aa --- /dev/null +++ b/test/js/node/test/parallel/test-repl-uncaught-exception-evalcallback.js @@ -0,0 +1,22 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { startNewREPLServer } = require('../common/repl'); + +const { replServer, output } = startNewREPLServer({ + prompt: '', + terminal: false, + useColors: false, + global: false, + eval: common.mustCall((code, context, filename, cb) => { + replServer.setPrompt('prompt! '); + cb(new Error('err')); + }) +}); + +replServer.write('foo\n'); + +// The output includes exactly one post-error prompt. +assert.match(output.accumulator, /prompt!/); +assert.doesNotMatch(output.accumulator, /prompt![\S\s]*prompt!/); +output.on('data', common.mustNotCall()); diff --git a/test/js/node/test/parallel/test-repl-uncaught-exception-standalone.js b/test/js/node/test/parallel/test-repl-uncaught-exception-standalone.js new file mode 100644 index 000000000000..bc53f9c60f9e --- /dev/null +++ b/test/js/node/test/parallel/test-repl-uncaught-exception-standalone.js @@ -0,0 +1,39 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const cp = require('child_process'); +// bun: upstream uses '-i'; that short flag is --install=fallback in bun, so the +// REPL is reached through the long form. +const child = cp.spawn(process.execPath, ['--interactive']); +let output = ''; + +child.stdout.setEncoding('utf8'); +child.stdout.on('data', (data) => { + output += data; +}); + +child.on('exit', common.mustCall(() => { + const results = output.split('\n'); + results.shift(); + assert.deepStrictEqual( + results, + [ + 'Type ".help" for more information.', + // x\n + '> Uncaught ReferenceError: x is not defined', + // Added `uncaughtException` listener. + '> short', + 'undefined', + // x\n + '> Foobar', + '> ', + ] + ); +})); + +child.stdin.write('x\n'); +child.stdin.write( + 'process.on("uncaughtException", () => console.log("Foobar"));' + + 'console.log("short")\n'); +child.stdin.write('x\n'); +child.stdin.end(); diff --git a/test/js/node/test/parallel/test-repl-uncaught-exception.js b/test/js/node/test/parallel/test-repl-uncaught-exception.js new file mode 100644 index 000000000000..012c7f59ebc8 --- /dev/null +++ b/test/js/node/test/parallel/test-repl-uncaught-exception.js @@ -0,0 +1,70 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const { startNewREPLServer } = require('../common/repl'); + +let count = 0; + +function run({ command, expected, useColors = false }) { + const { replServer, output } = startNewREPLServer({ + prompt: '', + terminal: false, + useColors, + }); + + replServer.write(`${command}\n`); + + if (typeof expected === 'string') { + assert.strictEqual(output.accumulator, expected); + } else { + assert.match(output.accumulator, expected); + } + + // Verify that the repl is still working as expected. + output.accumulator = ''; + replServer.write('1 + 1\n'); + // eslint-disable-next-line no-control-regex + assert.strictEqual(output.accumulator.replace(/\u001b\[[0-9]+m/g, ''), '2\n'); + replServer.close(); + count++; +} + +const tests = [ + { + useColors: true, + command: 'x', + expected: 'Uncaught ReferenceError: x is not defined\n' + }, + { + useColors: true, + command: 'throw { foo: "test" }', + expected: "Uncaught { foo: \x1B[32m'test'\x1B[39m }\n" + }, + { + command: 'process.on("uncaughtException", () => console.log("Foobar"));\n', + expected: /^Uncaught:\nTypeError \[ERR_INVALID_REPL_INPUT]: Listeners for `/ + }, + { + command: 'x;\n', + expected: 'Uncaught ReferenceError: x is not defined\n' + }, + { + command: 'process.on("uncaughtException", () => console.log("Foobar"));' + + 'console.log("Baz");\n', + expected: /^Uncaught:\nTypeError \[ERR_INVALID_REPL_INPUT]: Listeners for `/ + }, + { + command: 'console.log("Baz");' + + 'process.on("uncaughtException", () => console.log("Foobar"));\n', + expected: /^Baz\nUncaught:\nTypeError \[ERR_INVALID_REPL_INPUT]:.*uncaughtException/ + }, +]; + +process.on('exit', () => { + // To actually verify that the test passed we have to make sure no + // `uncaughtException` listeners exist anymore. + process.removeAllListeners('uncaughtException'); + assert.strictEqual(count, tests.length); +}); + +tests.forEach(run); diff --git a/test/js/node/test/parallel/test-repl-unexpected-token-recoverable.js b/test/js/node/test/parallel/test-repl-unexpected-token-recoverable.js new file mode 100644 index 000000000000..cc3229932ec4 --- /dev/null +++ b/test/js/node/test/parallel/test-repl-unexpected-token-recoverable.js @@ -0,0 +1,34 @@ +'use strict'; + +// This is a regression test for https://github.com/joyent/node/issues/8874. + +const common = require('../common'); +const assert = require('assert'); + +const spawn = require('child_process').spawn; +// Use -i to force node into interactive mode, despite stdout not being a TTY +// bun: '-i' is --install=fallback in bun, so the long form is used instead. +const args = [ '--interactive' ]; +const child = spawn(process.execPath, args); + +const input = 'const foo = "bar\\\nbaz"'; +// Match '|' as well since it marks a multi-line statement +const expectOut = /> \| undefined\n/; + +child.stderr.setEncoding('utf8'); +child.stderr.on('data', (d) => { + throw new Error('child.stderr be silent'); +}); + +child.stdout.setEncoding('utf8'); +let out = ''; +child.stdout.on('data', (d) => { + out += d; +}); + +child.stdout.on('end', common.mustCall(() => { + assert.match(out, expectOut); + console.log('ok'); +})); + +child.stdin.end(input); diff --git a/test/js/node/test/parallel/test-repl-use-global.js b/test/js/node/test/parallel/test-repl-use-global.js new file mode 100644 index 000000000000..06cda54f4d6f --- /dev/null +++ b/test/js/node/test/parallel/test-repl-use-global.js @@ -0,0 +1,81 @@ +'use strict'; + +// Flags: --expose-internals + +const common = require('../common'); +const stream = require('stream'); +const repl = require('internal/repl'); +const assert = require('assert'); + +// Array of [useGlobal, expectedResult] pairs +const globalTestCases = [ + [false, 'undefined'], + [true, '\'tacos\''], + [undefined, 'undefined'], +]; + +const globalTest = (useGlobal, cb, output) => (err, repl) => { + if (err) + return cb(err); + + let str = ''; + output.on('data', (data) => (str += data)); + globalThis.lunch = 'tacos'; + repl.write('globalThis.lunch;\n'); + repl.close(); + delete globalThis.lunch; + cb(null, str.trim()); +}; + +// Test how the global object behaves in each state for useGlobal +for (const [option, expected] of globalTestCases) { + runRepl(option, globalTest, common.mustSucceed((output) => { + assert.strictEqual(output, expected); + })); +} + +// Test how shadowing the process object via `let` +// behaves in each useGlobal state. Note: we can't +// actually test the state when useGlobal is true, +// because the exception that's generated is caught +// (see below), but errors are printed, and the test +// suite is aware of it, causing a failure to be flagged. +// +const processTestCases = [false, undefined]; +const processTest = (useGlobal, cb, output) => (err, repl) => { + if (err) + return cb(err); + + let str = ''; + output.on('data', (data) => (str += data)); + + // If useGlobal is false, then `let process` should work + repl.write('let process;\n'); + repl.write('21 * 2;\n'); + repl.close(); + cb(null, str.trim()); +}; + +for (const option of processTestCases) { + runRepl(option, processTest, common.mustSucceed((output) => { + assert.strictEqual(output, 'undefined\n42'); + })); +} + +function runRepl(useGlobal, testFunc, cb) { + const inputStream = new stream.PassThrough(); + const outputStream = new stream.PassThrough(); + const opts = { + input: inputStream, + output: outputStream, + useGlobal: useGlobal, + useColors: false, + terminal: false, + prompt: '' + }; + + repl.createInternalRepl( + process.env, + opts, + testFunc(useGlobal, cb, opts.output)); +} diff --git a/test/js/node/util/node-inspect-tests/parallel/util-inspect.test.js b/test/js/node/util/node-inspect-tests/parallel/util-inspect.test.js index 071828145b75..cc88c7ff7367 100644 --- a/test/js/node/util/node-inspect-tests/parallel/util-inspect.test.js +++ b/test/js/node/util/node-inspect-tests/parallel/util-inspect.test.js @@ -1879,9 +1879,10 @@ test("no assertion failures 3", () => { Object.setPrototypeOf(foo, null); assert( util.inspect(foo).startsWith( - // TODO: null prototypes - // `[${name}: null prototype] [WOW]${message ? `: ${message}` : '\n'}` - "[Object: null prototype] [WOW] {", + // Upstream expects `[${name}: null prototype] [WOW]...`; JSC reports + // the generic Error brand for null-prototype errors rather than the + // subclass name. + `[Error: null prototype] [WOW]${message ? `: ${message}` : ""}`, ), util.inspect(foo), ); @@ -1892,28 +1893,24 @@ test("no assertion failures 3", () => { tmp.startsWith( // TODO: null prototypes // `[${name}: null prototype]${message ? `: ${message}` : '\n'}`), - "[Error: null prototype] {", + "[Error: null prototype]", ) && tmp.includes("bar: true"), tmp, ); foo.stack = "This is a stack"; tmp = util.inspect(foo); assert( - tmp.startsWith( - // TODO: null prototypes - // '[[Error: null prototype]: This is a stack] { bar: true }' - "[Error: null prototype] {", - ) && tmp.includes("bar: true"), + // Restored to upstream: errors with a null prototype now format as + // errors, so the overridden stack renders bracketed like Node's. + tmp.startsWith("[[Error: null prototype]: This is a stack]") && tmp.includes("bar: true"), tmp, ); foo.stack = stack.split("\n")[0]; tmp = util.inspect(foo); assert( - tmp.startsWith( - // TODO: null prototypes - // `[[${name}: null prototype]${message ? `:\n ${message}` : ''}] { bar: true }` - "[Error: null prototype] {", - ) && tmp.includes("bar: true"), + // Bracketed error-form like upstream; JSC reports the generic Error + // brand and keeps the truncated stack on one line. + tmp.startsWith("[[Error: null prototype]") && tmp.includes("bar: true"), tmp, ); }); diff --git a/test/js/node/vm/vm.test.ts b/test/js/node/vm/vm.test.ts index 81e49e2e84b3..c4ac9cf7b07b 100644 --- a/test/js/node/vm/vm.test.ts +++ b/test/js/node/vm/vm.test.ts @@ -308,6 +308,159 @@ describe("Script", () => { message: "Class constructor Script cannot be invoked without 'new'", }); }); + + test("can specify displayErrors", () => { + const src = 'throw new Error("boom")'; + // displayErrors: false — no source-line/caret decoration on the stack. + try { + new Script(src, { filename: "t.vm" }).runInThisContext({ displayErrors: false }); + expect.unreachable(); + } catch (e: any) { + expect(e.message).toBe("boom"); + expect(e.stack).not.toMatch(/^t\.vm:1\n/); + } + // displayErrors: true (default) — stack is decorated with the source line. + try { + new Script(src, { filename: "t.vm" }).runInThisContext({ displayErrors: true }); + expect.unreachable(); + } catch (e: any) { + expect(e.stack).toMatch(/^t\.vm:1\nthrow new Error/); + } + // Same for runInContext. + try { + new Script(src, { filename: "t.vm" }).runInContext(createContext({}), { displayErrors: false }); + expect.unreachable(); + } catch (e: any) { + expect(e.stack).not.toMatch(/^t\.vm:1\n/); + } + }); + test("throws SyntaxError at construction like Node", () => { + // Node's vm.Script parses eagerly; the REPL depends on this. + expect(() => new Script("function {")).toThrow(SyntaxError); + expect(() => new Script("const x = ")).toThrow(SyntaxError); + }); + test("compile-time SyntaxError has arrow-decorated stack (Node DecorateErrorStack)", () => { + // Node prepends `:\n\n^\n\n` to compile-time SyntaxErrors + // from `new vm.Script`, unconditionally (independent of displayErrors). + for (const opts of [undefined, { displayErrors: true }, { displayErrors: false }]) { + let err: any; + try { + new Script("%%", opts); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect(err.stack.split("\n").slice(0, 4)).toEqual(["evalmachine.:1", "%%", "^", ""]); + } + + // Custom filename + lineOffset: reported line is offset-adjusted, source + // line and caret still come from the physical position. + let err: any; + try { + new Script("1;\n%%", { filename: "foo.js", lineOffset: 5 }); + } catch (e) { + err = e; + } + expect(err.stack.split("\n").slice(0, 4)).toEqual(["foo.js:7", "%%", "^", ""]); + + // Negative lineOffset: Node renders a signed line, still with source + caret. + // JSC clamps a negative provider start line to zero, so the offset is + // re-applied to the physical line when building the header. + err = undefined; + try { + new Script("1;\n%%", { lineOffset: -5 }); + } catch (e) { + err = e; + } + expect(err.stack.split("\n").slice(0, 4)).toEqual(["evalmachine.:-3", "%%", "^", ""]); + + // columnOffset on line 1 is subtracted from the caret; on later lines it + // is not (Node applies it only to the first physical line). + err = undefined; + try { + new Script(" %%", { columnOffset: 10 }); + } catch (e) { + err = e; + } + expect(err.stack.split("\n").slice(0, 4)).toEqual(["evalmachine.:1", " %%", " ^", ""]); + + err = undefined; + try { + new Script("1;\n %%", { columnOffset: 10 }); + } catch (e) { + err = e; + } + expect(err.stack.split("\n").slice(0, 4)).toEqual(["evalmachine.:2", " %%", " ^", ""]); + }); + + test("vm.compileFunction compile-time SyntaxError is arrow-decorated like new Script", () => { + // Node decorates both compile paths, but compileFunction defaults filename to + // "" where new Script defaults to "evalmachine.". An explicitly + // empty filename is honored by both and renders as ":". + const header = (fn: () => unknown) => { + let err: any; + try { + fn(); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + return err.stack.split("\n").slice(0, 4); + }; + + expect(header(() => compileFunction("%%"))).toEqual([":1", "%%", "^", ""]); + expect(header(() => compileFunction("%%", [], {}))).toEqual([":1", "%%", "^", ""]); + expect(header(() => compileFunction("%%", [], { filename: "" }))).toEqual([":1", "%%", "^", ""]); + expect(header(() => compileFunction("%%", [], { filename: "foo.js" }))).toEqual(["foo.js:1", "%%", "^", ""]); + expect(header(() => compileFunction("1;\n%%", [], { filename: "f.js", lineOffset: 5 }))).toEqual([ + "f.js:7", + "%%", + "^", + "", + ]); + expect(header(() => compileFunction("1;\n%%", [], { lineOffset: -5 }))).toEqual([":-3", "%%", "^", ""]); + + // An explicitly empty filename is not the same as an absent one. + expect(header(() => new Script("%%", { filename: "" }))).toEqual([":1", "%%", "^", ""]); + + // The string-options form counts as "provided" too, "" included. + expect(header(() => new Script("%%", "myfile.js"))).toEqual(["myfile.js:1", "%%", "^", ""]); + expect(header(() => new Script("%%", ""))).toEqual([":1", "%%", "^", ""]); + }); + + test("a throwing Error.prepareStackTrace does not escape the compile-time SyntaxError", () => { + // Building the error materializes its stack, running a user + // prepareStackTrace; if that throws, the SyntaxError must still be what is + // thrown (node does the same) and the arrow header must survive. + const prev = Error.prepareStackTrace; + Error.prepareStackTrace = () => { + throw new Error("boom-from-prepareStackTrace"); + }; + try { + let err: any; + try { + new Script("%%"); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect(err.message).toBe("Unexpected token '%'"); + expect(err.stack.split("\n").slice(0, 4)).toEqual(["evalmachine.:1", "%%", "^", ""]); + + // Same eager-materialization path via vm.compileFunction. + let fnErr: any; + try { + compileFunction("%%"); + } catch (e) { + fnErr = e; + } + expect(fnErr).toBeInstanceOf(SyntaxError); + expect(fnErr.message).toBe("Unexpected token '%'"); + expect(fnErr.stack.split("\n").slice(0, 4)).toEqual([":1", "%%", "^", ""]); + } finally { + Error.prepareStackTrace = prev; + } + }); }); type TestRunInContextArg = @@ -524,9 +677,6 @@ function testRunInContext({ fn, isIsolated, isNew }: TestRunInContextArg) { test.todo("can specify columnOffset", () => { // }); - test.todo("can specify displayErrors", () => { - // - }); test.todo("can specify timeout", () => { // }); @@ -698,15 +848,12 @@ resp.text().then((a) => { }); test("can't use export syntax in vm.Script", () => { - expect(() => { - const script = new Script("export default {};"); - script.runInThisContext(); - }).toThrow({ name: "SyntaxError", message: "Unexpected keyword 'export'" }); - - expect(() => { - const script = new Script("export default {};"); - script.createCachedData(); - }).toThrow({ message: "createCachedData failed" }); + // vm.Script now parses eagerly (like Node), so the SyntaxError surfaces at + // construction rather than at runInThisContext()/createCachedData(). + expect(() => new Script("export default {};")).toThrow({ + name: "SyntaxError", + message: "Unexpected keyword 'export'", + }); }); test("rejects invalid bytecode", () => { diff --git a/test/no-validate-exceptions.txt b/test/no-validate-exceptions.txt index 9dd0e440d42a..d22ba0b89be2 100644 --- a/test/no-validate-exceptions.txt +++ b/test/no-validate-exceptions.txt @@ -111,3 +111,8 @@ test/bundler/native-plugin.test.ts # `bun run build` in the react templates loads bun-plugin-tailwind's napi # addon, same Init() pattern. test/cli/init/init.test.ts + +# JSC JSONP fast path (Interpreter::executeProgram doGet lambda) does an +# unchecked getPropertySlot/getValue pair; only throwable through +# NodeVMGlobalObject's overridden getOwnPropertySlot. Needs a WebKit-side fix. +test/js/node/test/parallel/test-repl-inspect-defaults.js