/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
ext/node/polyfills/repl.ts
2 108 строк
64 KB
Kenta Moriuchi
chore: make prefer-primordials lint an internal plugin (#35978)
23 июл 2026, 11:53
Не верифицирован
23 июл 2026, 11:53
5b55d08
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. // Copyright Joyent and Node contributors. All rights reserved. MIT license. // deno-lint-ignore-file no-this-alias no-unused-vars no-explicit-any import { core, primordials } from "ext:core/mod.js"; const { ArrayIsArray, ArrayPrototypeFilter, ArrayPrototypeForEach, ArrayPrototypeIndexOf, ArrayPrototypeJoin, ArrayPrototypeMap, ArrayPrototypePop, ArrayPrototypePush, ArrayPrototypeShift, ArrayPrototypeSort, ArrayPrototypeSplice, ArrayPrototypeUnshift, ArrayBufferIsView, Boolean, ErrorPrototype, FunctionPrototypeBind, FunctionPrototypeCall, MathMax, MathMin, NumberIsNaN, NumberParseFloat, ObjectCreate, ObjectDefineProperty, ObjectGetOwnPropertyDescriptor, ObjectGetOwnPropertyNames, ObjectGetPrototypeOf, ObjectKeys, ObjectPrototypeHasOwnProperty, ObjectPrototypeIsPrototypeOf, ReflectApply, RegExpPrototypeExec, RegExpPrototypeTest, SafeArrayIterator, SafeRegExp, SafeSet, SetPrototypeAdd, SetPrototypeHas, String, StringPrototypeCharAt, StringPrototypeEndsWith, StringPrototypeIncludes, StringPrototypeIndexOf, StringPrototypeRepeat, StringPrototypeReplace, StringPrototypeSearch, StringPrototypeSlice, StringPrototypeSplit, StringPrototypeStartsWith, StringPrototypeToLowerCase, StringPrototypeTrim, Symbol, SyntaxError, } = primordials; import { Interface } from "ext:deno_node/_readline.mjs"; const { commonPrefix } = core.loadExtScript( "ext:deno_node/internal/readline/utils.mjs", ); const { inspect } = core.loadExtScript( "ext:deno_node/internal/util/inspect.mjs", ); const { ERR_INVALID_REPL_EVAL_CONFIG, ERR_INVALID_REPL_INPUT, ERR_MISSING_ARGS, } = core.loadExtScript("ext:deno_node/internal/errors.ts"); const { validateFunction } = core.loadExtScript( "ext:deno_node/internal/validators.mjs", ); const { shouldColorize } = core.loadExtScript( "ext:deno_node/internal/util.mjs", ); const vm = core.loadExtScript("ext:deno_node/vm.js").default; import process from "node:process"; import path from "node:path"; import fs from "node:fs"; import { Console } from "node:console"; import Module from "node:module"; const { EventEmitter } = core.loadExtScript("ext:deno_node/_events.mjs"); export const REPL_MODE_SLOPPY = Symbol("repl-sloppy"); export const REPL_MODE_STRICT = Symbol("repl-strict"); const kBufferedCommandSymbol = Symbol("bufferedCommand"); const kLoadingSymbol = Symbol("loading"); const kContextId = Symbol("contextId"); const kStandaloneREPL = Symbol("standaloneREPL"); // Multiline prompt indicator (matches Node's `| `). const kMultilinePrompt = "| "; // Per-instance unique resource name, matching Node's `REPL{n}` scheme so that // stack traces reproduce the resource ID expected by node_compat tests. let nextREPLResourceNumber = 1; function getREPLResourceName(): string { return `REPL${nextREPLResourceNumber++}`; } // REPLs that are "active" route async (uncaught) errors through their // `_handleError`. We use `process.setUncaughtExceptionCaptureCallback` so // user code inspecting `process.listenerCount('uncaughtException')` sees 0 // (matching Node, which uses `domain` and never installs a process // listener). The capture callback is a single slot - we save the previous // value when adding the first REPL hook and restore it when the last // REPL exits. const _activeREPLs: REPLServer[] = []; let _captureHookInstalled = false; let _prevCaptureCallback: any = null; function _replCaptureCallback(err: any) { const repl = _activeREPLs[_activeREPLs.length - 1]; if (repl) { // Always print the error via the REPL output. Node's REPL installs a // default `_domain.on('error', e => repl._handleError(e))` listener so // that domain-routed errors still surface in the REPL. try { repl._handleError(err); } catch { // ignore inspect failures } // Mirror Node's domain integration: also emit on `_domain` so user // listeners (e.g. test-repl-tab-complete-nested-repls's // `_domain.on('error', err => { throw err; })`) run. If a listener // re-throws, the throw escapes from emit so callers like // `child_process.spawn` see a non-zero exit code. const dom = (repl as any)._domain; if ( dom && typeof dom.listenerCount === "function" && dom.listenerCount("error") > 0 ) { dom.emit("error", err); } return; } if (typeof _prevCaptureCallback === "function") { _prevCaptureCallback(err); } else { // Re-throw so deno_core's default uncaught handler runs. throw err; } } function _installCaptureHook() { if (_captureHookInstalled) return; _captureHookInstalled = true; const p = process as any; if ( typeof p.hasUncaughtExceptionCaptureCallback === "function" && p.hasUncaughtExceptionCaptureCallback() ) { _prevCaptureCallback = (p as any)._uncaughtExceptionCaptureFn ?? null; // Clear before re-setting (the API throws if already set). p.setUncaughtExceptionCaptureCallback(null); } p.setUncaughtExceptionCaptureCallback(_replCaptureCallback); } function _uninstallCaptureHookIfIdle() { if (_activeREPLs.length !== 0 || !_captureHookInstalled) return; _captureHookInstalled = false; const p = process as any; p.setUncaughtExceptionCaptureCallback(null); if (typeof _prevCaptureCallback === "function") { p.setUncaughtExceptionCaptureCallback(_prevCaptureCallback); } _prevCaptureCallback = null; } // Node's REPL refuses user-installed `uncaughtException` listeners so the // REPL's own handler isn't shadowed. Mirror that with a counted // `newListener` guard installed once and removed when the last non- // standalone REPL exits. The REPL's own handler is named // `_replUncaughtHandler`, matching the only allowed function name. let _newListenerGuardCount = 0; function _newListenerGuard(event: string, listener: any) { if ( event === "uncaughtException" && listener && listener.name !== "_replUncaughtHandler" ) { throw new ERR_INVALID_REPL_INPUT( "Listeners for `uncaughtException` cannot be used in the REPL", ); } } function _addNewListenerGuard() { if (_newListenerGuardCount++ === 0) { process.prependListener("newListener", _newListenerGuard); } } function _removeNewListenerGuard() { if (--_newListenerGuardCount === 0) { process.removeListener("newListener", _newListenerGuard); } } const reAtFrame = new SafeRegExp(/^\s+at\s/); const reExtFrame = new SafeRegExp(/\bext:[a-z_]+\//); const reNodeParenFrame = new SafeRegExp(/\(node:[a-z_]+:/); const reNodeFrame = new SafeRegExp(/\bnode:[a-z_]+:\d+:\d+\)?$/); const reOpenParen = new SafeRegExp(/\(/); // Drop frames that originate from the REPL polyfill / deno_core internals, // plus the trailing top-level eval frame, so the stack matches Node's // `overrideStackTrace`-filtered output. function filterReplStack(stack: string): string { const rawLines = StringPrototypeSplit(stack, "\n"); const filtered: string[] = []; for (const line of new SafeArrayIterator(rawLines)) { if (!RegExpPrototypeTest(reAtFrame, line)) { ArrayPrototypePush(filtered, line); continue; } if (RegExpPrototypeTest(reExtFrame, line)) continue; if (RegExpPrototypeTest(reNodeParenFrame, line)) continue; if (RegExpPrototypeTest(reNodeFrame, line)) continue; ArrayPrototypePush(filtered, line); } // Mirror Node: from the bottom up, drop the last "anonymous" frame // (one without a function name, e.g. " at FILE:line:col") and // everything below it. That removes the synthetic top-level eval frame // (Node uses overrideStackTrace + ArrayPrototypeFindLastIndex). The // calling code is responsible for dealing with the bracketed inspect // form when this leaves zero frames. let lastAnon = -1; for (let i = filtered.length - 1; i >= 0; i--) { const ln = filtered[i]; if (!RegExpPrototypeTest(reAtFrame, ln)) continue; if (!RegExpPrototypeTest(reOpenParen, ln)) { lastAnon = i; break; } } if (lastAnon !== -1) { filtered.length = lastAnon; } return ArrayPrototypeJoin(filtered, "\n"); } // This is the default "writer" value const writer = (obj: unknown) => inspect(obj, writer.options); writer.options = { ...inspect.defaultOptions, showProxy: true }; // ANSI cursor control helpers for preview function _cursorTo(stream: any, x: number) { stream.write(`\x1b[${x + 1}G`); } function _moveCursor(stream: any, dx: number, dy: number) { let data = ""; if (dx < 0) data += `\x1b[${-dx}D`; else if (dx > 0) data += `\x1b[${dx}C`; if (dy < 0) data += `\x1b[${-dy}A`; else if (dy > 0) data += `\x1b[${dy}B`; if (data) stream.write(data); } function _clearLine(stream: any, dir?: number) { if (dir !== undefined && dir < 0) stream.write("\x1b[1K"); else if (dir !== undefined && dir > 0) stream.write("\x1b[0K"); else stream.write("\x1b[2K"); } // Match Node's REPL: hide the legacy `__define*Getter|Setter__` and // `__lookup*Getter|Setter__` prototype helpers from completion. `__proto__` // is intentionally kept since it is widely used. const _replHiddenProtoNames = new SafeSet([ "__defineGetter__", "__defineSetter__", "__lookupGetter__", "__lookupSetter__", ]); const reDigits = new SafeRegExp(/^\d+$/); const reIdentifier = new SafeRegExp(/^[a-zA-Z_$][\w$]*$/); function _getPropertyNames(obj: any): string[] { if (!obj) return []; try { // Skip getOwnPropertyNames for huge indexed collections; arrays and // typed arrays with millions of elements would otherwise allocate // millions of name strings and trip a v8 internal assertion. const len = (obj as any)?.length; const tooBigIndexed = typeof len === "number" && len > 100_000 && (ArrayIsArray(obj) || ArrayBufferIsView(obj)); if (tooBigIndexed) { const symbolKeyed = ArrayPrototypeFilter( ObjectKeys(obj), (k: string) => !RegExpPrototypeTest(reDigits, k), ); return ArrayPrototypeFilter(symbolKeyed, (name: string) => { if (SetPrototypeHas(_replHiddenProtoNames, name)) return false; return RegExpPrototypeTest(reIdentifier, name); }); } return ArrayPrototypeFilter( ObjectGetOwnPropertyNames(obj), (name: string) => { if (SetPrototypeHas(_replHiddenProtoNames, name)) return false; return RegExpPrototypeTest(reIdentifier, name); }, ); } catch { return []; } } export class Recoverable extends SyntaxError { err: Error; constructor(err: Error) { super(); this.err = err; } } /** * Check if code looks like it might be an object literal * that needs to be wrapped in parens to eval as an expression. */ function isObjectLiteral(code: string): boolean { const trimmed = StringPrototypeTrim(code); return StringPrototypeStartsWith(trimmed, "{") && !StringPrototypeStartsWith(trimmed, "{\\"); } interface PreviewSession { isSafe(expression: string): boolean; close(): void; } // The actual implementation lives in an IIFE-loaded script so it can // see the snapshot-time `core.ops` capture -- ES-module polyfills like // this file only see the post-`removeImportedOps` view, which doesn't // include `op_node_repl_inspector_connect`. See `_repl_preview.js`. const { createPreviewSession: _createPreviewSession } = core.loadExtScript( "ext:deno_node/_repl_preview.js", ) as { createPreviewSession(): PreviewSession | null }; const reUnexpectedEndOfInput = new SafeRegExp(/Unexpected end of input/); const reUnexpectedToken = new SafeRegExp(/Unexpected token/); const reUnterminatedString = new SafeRegExp(/Unterminated string/); const reUnterminatedTemplate = new SafeRegExp(/Unterminated template/); const reTrailingSemicolon = new SafeRegExp(/;\s*$/); const reBlankString = new SafeRegExp(/^\s*$/); const reTrailingNewline = new SafeRegExp(/\n$/); const reReplCommand = new SafeRegExp(/^\s*\.(\w*)$/); const reVarDeclaration = new SafeRegExp( /^\s*(?:let|const|var)\s+[a-zA-Z_$][\w$]*$/, ); const reRequireImport = new SafeRegExp( /(?:^|[\s;({,])(require|import)\s*\(\s*(['"`])((?:(?!\2|\\).)*)$/, ); const reExprChain = new SafeRegExp( /(?:^|[\s;({\[,!~+\-*/&|^%<>?:=])((?:[a-zA-Z_$][\w$]*\??\.)*(?:[a-zA-Z_$][\w$]*)?)\.?$/, ); const reNewlineChars = new SafeRegExp(/[\r\n\v]/); const reLeadingWhitespace = new SafeRegExp(/^\s+/); const reReplKeyword = new SafeRegExp(/^\.([^\s]+)\s*(.*)$/); const reStackFrames = new SafeRegExp(/^\s+at\s.*\n?/gm); const reReplFilename = new SafeRegExp(/^REPL\d+:\d+\r?\n/); const reReplLowerFilename = new SafeRegExp(/^repl:\d+\r?\n/); const reUnexpectedTokenName = new SafeRegExp( /Unexpected (?:token|identifier|string|number|keyword|reserved\s+word) '(.+?)'/, ); const reErrorBraces = new SafeRegExp(/^\[(.*)\](?: (\{[\s\S]*\}))?$/); const reLineSplit = new SafeRegExp(/(?<=\n)/); const reErrorName = new SafeRegExp(/^\[?([A-Z][a-z0-9_]*)*Error/); /** * Check if a syntax error is recoverable (i.e., the user might continue typing). */ function isRecoverableError(e: unknown, code: string): boolean { if (!e || typeof e !== "object" || (e as Error).name !== "SyntaxError") { return false; } const message = (e as Error).message; // Check for common recoverable patterns if (RegExpPrototypeTest(reUnexpectedEndOfInput, message)) return true; if (RegExpPrototypeTest(reUnexpectedToken, message)) { // Check if the code has unbalanced braces/brackets/parens let depth = 0; for (let i = 0; i < code.length; i++) { const c = code[i]; if (c === "{" || c === "(" || c === "[") depth++; else if (c === "}" || c === ")" || c === "]") depth--; } if (depth > 0) return true; } if (RegExpPrototypeTest(reUnterminatedString, message)) return true; if (RegExpPrototypeTest(reUnterminatedTemplate, message)) return true; return false; } function _turnOnEditorMode(repl: REPLServer) { repl.editorMode = true; FunctionPrototypeCall(Interface.prototype.setPrompt, repl, ""); } function _turnOffEditorMode(repl: REPLServer) { repl.editorMode = false; repl.setPrompt(repl._initialPrompt); } function defineDefaultCommands(repl: REPLServer) { repl.defineCommand("break", { help: "Sometimes you get stuck, this gets you out", action: function (this: REPLServer) { this.clearBufferedCommand(); this.displayPrompt(); }, }); let clearMessage: string; if (repl.useGlobal) { clearMessage = "Alias for .break"; } else { clearMessage = "Break, and also clear the local context"; } repl.defineCommand("clear", { help: clearMessage, action: function (this: REPLServer) { 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: REPLServer) { this.close(); }, }); repl.defineCommand("help", { help: "Print this help message", action: function (this: REPLServer) { const names = ArrayPrototypeSort(ObjectKeys(this.commands)); const longestNameLength = MathMax( ...new SafeArrayIterator( 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 (this: REPLServer, file: string) { 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 as { code?: string })?.code === "ERR_MISSING_ARGS") { this.output.write(`${(error as 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 (this: REPLServer, file: string) { 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 as { code?: string })?.code === "ERR_MISSING_ARGS") { this.output.write(`${(error as Error).message}\n`); } else { this.output.write(`Failed to load: ${file}\n`); } } this.displayPrompt(); }, }); repl.defineCommand("editor", { help: "Enter editor mode", action: function (this: REPLServer) { if (!this.terminal) { this.displayPrompt(); return; } _turnOnEditorMode(this); this.output.write( "// Entering editor mode (Ctrl+D to finish, Ctrl+C to cancel)\n", ); }, }); } function _memory(this: REPLServer, cmd: string) { this.lines = this.lines || []; this.lines.level = this.lines.level || []; if (cmd) { const len = this.lines.level.length ? this.lines.level.length - 1 : 0; ArrayPrototypePush(this.lines, StringPrototypeRepeat(" ", len) + cmd); } else { ArrayPrototypePush(this.lines, ""); } if (!cmd) { this.lines.level = []; return; } const countMatches = (regex: RegExp, str: string) => { let count = 0; while (RegExpPrototypeExec(regex, str) !== null) count++; return count; }; const dw = countMatches(new SafeRegExp(/[{(]/g), cmd); const up = countMatches(new SafeRegExp(/[})]/g), cmd); let depth = dw - up; if (depth) { FunctionPrototypeCall(function workIt() { const self = this as REPLServer; if (depth > 0) { ArrayPrototypePush(self.lines.level, { line: self.lines.length - 1, depth: depth, }); } else if (depth < 0) { const curr = ArrayPrototypePop(self.lines.level); if (curr) { const tmp = curr.depth + depth; if (tmp < 0) { depth += curr.depth; FunctionPrototypeCall(workIt, self); } else if (tmp > 0) { curr.depth += depth; ArrayPrototypePush(self.lines.level, curr); } } } }, this); } } type REPLCommand = { help?: string; action: (this: REPLServer, ...args: any[]) => void; }; export class REPLServer extends (Interface as any) { constructor( prompt?: any, stream?: any, eval_?: any, useGlobal?: boolean, ignoreUndefined?: boolean, replMode?: symbol, ) { let options: Record<string, unknown>; if (prompt !== null && typeof prompt === "object") { options = { ...prompt }; stream = options.stream || options.socket; eval_ = options.eval; useGlobal = options.useGlobal as boolean | undefined; ignoreUndefined = options.ignoreUndefined as boolean | undefined; prompt = options.prompt; replMode = options.replMode as symbol | undefined; } else { options = {}; } if (!options.input && !options.output) { stream = stream || process; options.input = stream.stdin || stream; options.output = stream.stdout || stream; } if (options.terminal === undefined) { options.terminal = !!(options.output as { isTTY?: boolean })?.isTTY; } options.terminal = !!options.terminal; // The preview re-runs the typed input on every keystroke. To match // Node's behavior and avoid visibly firing side effects (e.g. typing // the closing paren of `console.log("hi")` printing "hi" before // Enter), the preview path is gated by the V8 inspector's // `Runtime.evaluate({ throwOnSideEffect: true })` -- see // `_createPreviewSession`. If the inspector probe isn't available // (`previewSession` ends up null below), preview stays off rather // than falling back to an unprotected `vm.Script` eval. // See: https://github.com/denoland/deno/issues/34360 // // The `preview` default matches Node: off when a custom `eval` is // supplied (consumers like `@babel/node` get clean output), on // otherwise. A consumer that wants previews with a custom eval can // pass `preview: true` explicitly. const previewRequested = !!options.terminal && (options.preview !== undefined ? !!options.preview : !eval_); const previewSession = previewRequested ? _createPreviewSession() : null; const usePreview = previewSession !== null; if (options.terminal && options.useColors === undefined) { options.useColors = shouldColorize(options.output); } // Default prompt if (prompt === undefined) { prompt = "> "; } super({ input: options.input, output: options.output, completer: options.completer || completer, terminal: options.terminal, historySize: options.historySize, prompt, }); // Create a fake domain for compatibility. Node uses domains for error // handling, but we use a simpler EventEmitter-based approach. this._domain = new EventEmitter(); // Deprecated inputStream/outputStream properties (DEP0141) ObjectDefineProperty(this, "inputStream", { __proto__: null, get: () => this.input, set: (val: any) => { this.input = val; }, enumerable: false, configurable: true, }); ObjectDefineProperty(this, "outputStream", { __proto__: null, get: () => this.output, set: (val: any) => { this.output = val; }, enumerable: false, configurable: true, }); this.allowBlockingCompletions = !!options.allowBlockingCompletions; this.useColors = !!options.useColors; this._isStandalone = !!(options as Record<symbol, unknown>)[kStandaloneREPL]; this.useGlobal = !!useGlobal; this.ignoreUndefined = !!ignoreUndefined; this.replMode = replMode || REPL_MODE_SLOPPY; this.underscoreAssigned = false; this.last = undefined; this.underscoreErrAssigned = false; this.lastError = undefined; this.breakEvalOnSigint = !!options.breakEvalOnSigint; this.editorMode = false; this[kContextId] = undefined; this._initialPrompt = prompt as string; // Stable resource name like Node's "REPL1"/"REPL2"/... - used as the // filename for compiled scripts so stack traces match Node's output. this._resourceName = getREPLResourceName(); if (this.breakEvalOnSigint && eval_) { throw new ERR_INVALID_REPL_EVAL_CONFIG(); } if ((options as Record<symbol, unknown>)[kStandaloneREPL]) { _module.exports.repl = this; } else { // Non-standalone REPLs install the user-listener guard and route // async errors via `process._fatalException`. The fatal-exception // hook is global; the guard is refcounted so multiple concurrent // REPLs co-exist and the process is left clean once the last exits. // We use `_fatalException` (rather than process.on) so user code // inspecting `process.listenerCount('uncaughtException')` sees 0, // matching Node's domain-based REPL. _installCaptureHook(); _addNewListenerGuard(); ArrayPrototypePush(_activeREPLs, this); this._processListenersAttached = true; this.once("exit", () => { const idx = ArrayPrototypeIndexOf(_activeREPLs, this); if (idx !== -1) ArrayPrototypeSplice(_activeREPLs, idx, 1); if (this._processListenersAttached) { this._processListenersAttached = false; _removeNewListenerGuard(); } _uninstallCaptureHookIfIdle(); }); } eval_ = eval_ || defaultEval; const self = this; // Pause taking in new input, and store the keys in a buffer. const pausedBuffer: any[] = []; let paused = false; function pause() { paused = true; } function unpause() { if (!paused) return; paused = false; let entry: any; const tmpCompletionEnabled = self.isCompletionEnabled; while ((entry = ArrayPrototypeShift(pausedBuffer)) !== undefined) { const type = entry[0]; const payload = entry[1]; const isCompletionEnabled = entry[2]; switch (type) { case "key": { const d = payload[0]; const key = payload[1]; self.isCompletionEnabled = isCompletionEnabled; self._ttyWrite(d, key); break; } case "close": self.emit("exit"); break; } if (paused) { break; } } self.isCompletionEnabled = tmpCompletionEnabled; } function defaultEval( code: string, context: any, _file: string, cb: (err: Error | null, result?: any) => void, ) { let result; let err: Error | null = null; let wrappedCmd = false; const input = code; // Empty input if (code === "\n") return cb(null); // If it looks like an object literal (starts with { and no trailing ;), // try wrapping in parens first to treat as expression. // This matches Node.js behavior: wrap first, fallback to unwrapped. if ( isObjectLiteral(code) && !RegExpPrototypeTest(reTrailingSemicolon, StringPrototypeTrim(code)) ) { code = `(${StringPrototypeTrim(code)})\n`; wrappedCmd = true; } if (err === null) { let wrappedErr: Error | undefined; while (true) { try { if ( self.replMode === REPL_MODE_STRICT && !RegExpPrototypeTest(reBlankString, code) ) { code = `'use strict'; void 0;\n${code}`; } const script = new vm.Script(code, { filename: _file }); if (self.useGlobal) { result = script.runInThisContext({ displayErrors: false }); } else { result = script.runInContext(context, { displayErrors: false, }); } } catch (e) { if (wrappedCmd) { // Wrapped version failed, try original wrappedCmd = false; code = input; wrappedErr = e as Error; continue; } // Use the unwrapped error unless it's a SyntaxError and the // wrapped version also had a SyntaxError (in which case we // prefer the unwrapped SyntaxError for better messaging). const error = e as Error; if (isRecoverableError(error, code)) { err = new Recoverable(error); } else { // Attach source context for SyntaxErrors so _handleError // can display source lines like Node.js does. if ( error != null && typeof error === "object" && error.name === "SyntaxError" ) { (error as any)._replSourceCode = StringPrototypeReplace( input, reTrailingNewline, "", ); } err = error; } } break; } } cb(err, result); } self.eval = function REPLEval( code: string, context: any, file: string, cb: (err: Error | null, result?: any) => void, ) { eval_(code, context, file, cb); }; self.clearBufferedCommand(); function completer(text: string, cb: any) { const callback = self.editorMode ? self.completeOnEditorMode(cb) : cb; _doComplete(text, callback); } // Wire the real completer into both `this.completer` (used by readline // and by user code via `replServer.completer(...)`) and the local // `completer` reference. `super(...)` set this to the outer stub. if (!options.completer) { self.completer = completer; } function _doComplete( line: string, callback: (err: Error | null, result: [string[], string]) => void, ) { const completionGroups: string[][] = []; let completeOn = ""; let filter = ""; // Handle REPL commands (.break, .clear, etc.). Node returns command // names without the leading dot and a completeOn matching the typed // suffix (also without the dot), so e.g. completing ".b" produces // [['break'], 'b']. const cmdMatch = RegExpPrototypeExec(reReplCommand, line); if (cmdMatch) { ArrayPrototypePush(completionGroups, ObjectKeys(self.commands)); completeOn = cmdMatch[1]; if (cmdMatch[1].length) { filter = cmdMatch[1]; } completionGroupsLoaded(); return; } // Variable declarations like `let a`, `const x`, `var foo` should // not produce identifier completions for the binding name. if (RegExpPrototypeTest(reVarDeclaration, line)) { callback(null, [[], line]); return; } // Module-name completions for `require('...` and `import('...`. // Returns the list of public builtin modules (and, for `import(...)`, // their `node:` URL forms). Third-party node_modules-on-disk lookup // is left to a future pass. const modMatch = RegExpPrototypeExec(reRequireImport, line); if (modMatch) { const subject = modMatch[3]; const fromModule: string[] = ArrayIsArray((Module as any).builtinModules) ? (Module as any).builtinModules : []; const fromModulePublic = ArrayPrototypeFilter( fromModule, (m: string) => !StringPrototypeStartsWith(m, "_"), ); const unprefixed = ArrayPrototypeFilter( fromModulePublic, (m: string) => !StringPrototypeStartsWith(m, "node:"), ); const alreadyPrefixed = ArrayPrototypeFilter( fromModulePublic, (m: string) => StringPrototypeStartsWith(m, "node:"), ); const knownInModule = new SafeSet(fromModule); const replExtras = ArrayPrototypeFilter( builtinModules as string[], (m: string) => !SetPrototypeHas(knownInModule, m) && !StringPrototypeStartsWith(m, "_"), ); // Group A: unprefixed builtins + user-pushed extras. Pushing a // single new entry must produce only one extra completion (no extra // group separator), so merge replExtras into this group. ArrayPrototypePush(completionGroups, [ ...new SafeArrayIterator(unprefixed), ...new SafeArrayIterator(replExtras), ]); // Group B: every `node:` URL form. Includes both the originally- // prefixed names from `Module.builtinModules` (e.g. `node:sqlite`) // and the `node:`-prefixed forms of the unprefixed builtins. ArrayPrototypePush(completionGroups, [ ...new SafeArrayIterator( ArrayPrototypeMap(unprefixed, (m: string) => `node:${m}`), ), ...new SafeArrayIterator(alreadyPrefixed), ]); completeOn = subject; filter = subject; completionGroupsLoaded(); return; } // Match identifier chains: foo, foo.bar, foo.bar.baz, etc. const exprMatch = RegExpPrototypeExec(reExprChain, line); let expr = ""; if (exprMatch) { const matchStr = exprMatch[1]; completeOn = matchStr; if (StringPrototypeEndsWith(matchStr, ".")) { expr = StringPrototypeSlice(matchStr, 0, -1); filter = ""; } else if (StringPrototypeIncludes(matchStr, ".")) { const bits = StringPrototypeSplit(matchStr, "."); filter = ArrayPrototypePop(bits)!; expr = ArrayPrototypeJoin(bits, "."); } else { filter = matchStr; } } else if (line.length === 0) { completeOn = ""; filter = ""; } else { callback(null, [[], line]); return; } if (expr) { // Member expression completion const chaining = "."; let evalExpr = expr; if (StringPrototypeEndsWith(expr, "?")) { // `expr` already contains the trailing `?`; strip it for eval // and keep the chaining as a plain `.` so we render // `console?.log` rather than `console??.log`. evalExpr = StringPrototypeSlice(expr, 0, -1); } const wrappedExpr = `try { ${evalExpr} } catch {}`; self.eval( wrappedExpr, self.context, "repl", (_e: any, obj: any) => { if (obj != null) { const memberGroups: string[][] = []; try { let p; if ( (typeof obj === "object" && obj !== null) || typeof obj === "function" ) { ArrayPrototypePush(memberGroups, _getPropertyNames(obj)); p = ObjectGetPrototypeOf(obj); } else { p = obj.constructor ? obj.constructor.prototype : null; } let sentinel = 5; while (p !== null && sentinel-- > 0) { ArrayPrototypePush(memberGroups, _getPropertyNames(p)); p = ObjectGetPrototypeOf(p); } } catch { // Proxy without getOwnPropertyNames } if (memberGroups.length) { const prefix = expr + chaining; for (const group of new SafeArrayIterator(memberGroups)) { ArrayPrototypePush( completionGroups, ArrayPrototypeMap(group, (m: string) => `${prefix}${m}`), ); } if (filter) { filter = `${prefix}${filter}`; } } } completionGroupsLoaded(); }, ); } else { // Global completion - walk context prototype chain if (self.context) { try { let obj = self.context; let sentinel = 5; while (obj !== null && sentinel-- > 0) { try { ArrayPrototypePush(completionGroups, _getPropertyNames(obj)); } catch { // ignore } obj = ObjectGetPrototypeOf(obj); } } catch { // ignore } } // Also include the JS-level globals (Object, Array, Uint8Array, // ...). When `useGlobal` is false, our `vm.createContext()` returns // an object whose prototype doesn't expose the built-in // constructors as own properties, so completion misses them. if (!self.useGlobal) { try { ArrayPrototypePush(completionGroups, _getPropertyNames(globalThis)); } catch { // ignore } } // JS keywords if (filter !== "") { 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", "undefined", "var", "void", "while", "with", "yield", ]); } completionGroupsLoaded(); } function completionGroupsLoaded() { // Filter by prefix if (completionGroups.length && filter) { const lowerFilter = StringPrototypeToLowerCase(filter); const filtered: string[][] = []; for (const group of new SafeArrayIterator(completionGroups)) { const fg = ArrayPrototypeFilter( group, (str) => StringPrototypeStartsWith( StringPrototypeToLowerCase(str), lowerFilter, ), ); if (fg.length) ArrayPrototypePush(filtered, fg); } completionGroups.length = 0; ArrayPrototypePush( completionGroups, ...new SafeArrayIterator(filtered), ); } // Deduplicate and collect const completions: string[] = []; const seen = new SafeSet<string>(); SetPrototypeAdd(seen, ""); for (const group of new SafeArrayIterator(completionGroups)) { ArrayPrototypeSort(group, (a, b) => (b > a ? 1 : -1)); const prevSize = seen.size; for (const item of new SafeArrayIterator(group)) { if (!SetPrototypeHas(seen, item)) { ArrayPrototypeUnshift(completions, item); SetPrototypeAdd(seen, item); } } if (seen.size !== prevSize) { ArrayPrototypeUnshift(completions, ""); } } if (completions.length > 0 && completions[0] === "") { ArrayPrototypeShift(completions); } callback(null, [completions, completeOn]); } } // Preview state let inputPreview: string | null = null; let completionPreview: string | null = null; let previewCompletionCounter = 0; let escaped: string | null = null; function _getDisplayPosHelper(str: string) { if (typeof self._getDisplayPos === "function") { return self._getDisplayPos(str); } return { rows: 0, cols: str.length }; } function getPreviewPos() { const displayPos = _getDisplayPosHelper( `${self.getPrompt()}${self.line}`, ); const cursorPos = self.line.length !== self.cursor ? (typeof self.getCursorPos === "function" ? self.getCursorPos() : { rows: 0, cols: self.getPrompt().length + self.cursor }) : displayPos; return { displayPos, cursorPos }; } function isCursorAtInputEnd() { return self.cursor === self.line.length; } function showCompletionPreview(line: string) { previewCompletionCounter++; const count = previewCompletionCounter; self.completer(line, (err: any, data: any) => { if (count !== previewCompletionCounter) return; if (err) return; const rawCompletions = data[0]; const completeOn = data[1]; if (!rawCompletions || rawCompletions.length === 0) return; const completions = ArrayPrototypeFilter(rawCompletions, Boolean); if (completions.length === 0) return; const prefix = commonPrefix(completions); if (prefix.length <= completeOn.length) return; const suffix = StringPrototypeSlice(prefix, completeOn.length); completionPreview = suffix; const result = self.useColors ? `\x1b[90m${suffix}\x1b[39m` : ` // ${suffix}`; const { cursorPos, displayPos } = getPreviewPos(); if (self.line.length !== self.cursor) { _cursorTo(self.output, displayPos.cols); _moveCursor(self.output, 0, displayPos.rows - cursorPos.rows); } self.output.write(result); _cursorTo(self.output, cursorPos.cols); const totalLine = `${self.getPrompt()}${self.line}${suffix}`; const newPos = _getDisplayPosHelper(totalLine); const rows = newPos.rows - cursorPos.rows - (newPos.cols === 0 ? 1 : 0); if (rows > 0) _moveCursor(self.output, 0, -rows); }); } function showPreview(showCompletion = true) { if (!usePreview) return; if (inputPreview !== null || !self.isCompletionEnabled) return; const line = StringPrototypeTrim(self.line); if (line === "") return; // Show completion preview (inline dim suffix) if (showCompletion) { showCompletionPreview(self.line); } // Don't show eval preview in multiline mode if (self[kBufferedCommandSymbol]) return; // Evaluate for input preview let previewLine = line; if ( completionPreview !== null && isCursorAtInputEnd() && escaped !== self.line ) { previewLine += completionPreview; } let previewCode = previewLine + "\n"; // Apply object literal wrapping for preview too if ( isObjectLiteral(previewCode) && !RegExpPrototypeTest( reTrailingSemicolon, StringPrototypeTrim(previewCode), ) ) { previewCode = `(${StringPrototypeTrim(previewCode)})\n`; } // Probe via the inspector first: if v8 reports a side effect // (or a syntax/reference error / runtime throw / timeout), don't // preview. This is what stops typed input like `console.log("hi")` // from printing "hi" before the user presses Enter. if (!previewSession!.isSafe(previewCode)) return; // Probe passed -- safe to evaluate normally for the actual value // so `inspect()` below sees the real JS object (with prototype, // class info, getters, etc.) rather than the CDP wire shape. let result; try { const script = new vm.Script(previewCode, { filename: "repl" }); if (self.useGlobal) { result = script.runInThisContext({ displayErrors: false, timeout: 500, }); } else { result = script.runInContext(self.context, { displayErrors: false, timeout: 500, }); } } catch { // Probe says safe but the actual eval threw -- e.g. the probe // ran against globalThis and the expression references a REPL // var that only exists in `self.context`. Silently skip. return; } if (result === undefined && self.ignoreUndefined) return; let inspected = inspect(result, { colors: false, showProxy: true, breakLength: Infinity, compact: true, maxArrayLength: 10, depth: 1, }); if (inspected === line) return; // Truncate at newline const nlIdx = StringPrototypeSearch(inspected, reNewlineChars); if (nlIdx !== -1) inspected = StringPrototypeSlice(inspected, 0, nlIdx); // Limit length const maxCols = MathMin( (self as any).columns || 80, 250, ); if (inspected.length > maxCols) { inspected = StringPrototypeSlice(inspected, 0, maxCols - 4) + "..."; } inputPreview = inspected; const preview = self.useColors ? `\x1b[90m${inspected}\x1b[39m` : `// ${inspected}`; const { cursorPos, displayPos } = getPreviewPos(); const rows = displayPos.rows - cursorPos.rows; if (rows > 0) _moveCursor(self.output, 0, rows); self.output.write(`\n${preview}`); _cursorTo(self.output, cursorPos.cols); _moveCursor(self.output, 0, -rows - 1); } function clearPreview(key: any) { if (!usePreview) return; // Clear input preview if (inputPreview !== null) { const { displayPos, cursorPos } = getPreviewPos(); const rows = displayPos.rows - cursorPos.rows + 1; _moveCursor(self.output, 0, rows); _clearLine(self.output); _moveCursor(self.output, 0, -rows); inputPreview = null; } // Clear completion preview if (completionPreview !== null) { const move = self.line.length !== self.cursor; let pos: any; let rows = 0; if (move) { pos = getPreviewPos(); _cursorTo(self.output, pos.displayPos.cols); rows = pos.displayPos.rows - pos.cursorPos.rows; _moveCursor(self.output, 0, rows); } const totalLine = `${self.getPrompt()}${self.line}${completionPreview}`; const newPos = _getDisplayPosHelper(totalLine); if ( newPos.rows === 0 || (move && pos.displayPos.rows === newPos.rows) ) { _clearLine(self.output, 1); } else { self.output.write("\x1b[0J"); } if (move) { _cursorTo(self.output, pos.cursorPos.cols); _moveCursor(self.output, 0, -rows); } // Auto-accept completion on Enter if (key && !key.ctrl && !key.shift) { if (key.name === "escape") { if (escaped === null && key.meta) { escaped = self.line; } } else if ( (key.name === "return" || key.name === "enter") && !key.meta && escaped !== self.line && isCursorAtInputEnd() ) { self._insertString(completionPreview); } } completionPreview = null; } if (escaped !== self.line) { escaped = null; } } self.resetContext(); this.commands = ObjectCreate(null); defineDefaultCommands(this); // Figure out which "writer" function to use self.writer = options.writer || writer; if (self.writer === writer) { writer.options.colors = self.useColors; } function _parseREPLKeyword( this: REPLServer, keyword: string, rest: string, ): boolean { 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; } if (previewSession !== null) { try { previewSession.close(); } catch { /* ignore */ } } self.emit("exit"); }); let sawSIGINT = false; let sawCtrlD = false; self.on("SIGINT", function onSigInt() { 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: string) { cmd = cmd || ""; sawSIGINT = false; if (self.editorMode) { self[kBufferedCommandSymbol] += cmd + "\n"; // code alignment const matches = self._sawKeyPress && !self[kLoadingSymbol] ? RegExpPrototypeExec(reLeadingWhitespace, 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); if (trimmedCmd) { if ( StringPrototypeCharAt(trimmedCmd, 0) === "." && StringPrototypeCharAt(trimmedCmd, 1) !== "." && NumberIsNaN(NumberParseFloat(trimmedCmd)) ) { const matches = RegExpPrototypeExec(reReplKeyword, trimmedCmd); const keyword = matches?.[1]; const rest = matches?.[2]; if ( keyword && 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"; self.eval(evalCmd, self.context, self._resourceName, finish); function finish(e: Error | null, ret?: any) { FunctionPrototypeCall(_memory, self, cmd); if ( e !== null && e !== undefined && !self[kBufferedCommandSymbol] && StringPrototypeStartsWith(StringPrototypeTrim(cmd), "npm ") && !ObjectPrototypeIsPrototypeOf(Recoverable.prototype, e) ) { 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 if ( ObjectPrototypeIsPrototypeOf(Recoverable.prototype, e) && !sawCtrlD ) { // Start multiline self[kBufferedCommandSymbol] += cmd + "\n"; self.displayPrompt(); return; } if (e !== null && e !== undefined) { self._handleError((e as Recoverable).err || e); } // Clear buffer if no SyntaxErrors self.clearBufferedCommand(); sawCtrlD = false; // If we got any output - print it (if no error) if ( (e === null || e === undefined) && arguments.length === 2 && (!self.ignoreUndefined || ret !== undefined) ) { if (!self.underscoreAssigned) { self.last = ret; } self.output.write(self.writer(ret) + "\n"); } if (!self.closed && !e) { 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); } }); // Wrap readline tty to enable editor mode and pausing. const ttyWrite = FunctionPrototypeBind(self._ttyWrite, self); self._ttyWrite = (d: any, key: any) => { 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); ttyWrite(d, key); const showCompletion = key.name !== "escape"; showPreview(showCompletion); return; } // Editor mode if (key.ctrl && !key.shift) { switch (key.name) { 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( arg1?: any, cb?: (err: Error | null, repl: REPLServer) => void, ) { // Newer API: setupHistory({ filePath, size, onHistoryFileLoaded }) let onLoaded = cb; if (typeof arg1 === "object" && arg1 !== null) { onLoaded = arg1.onHistoryFileLoaded; } if (typeof onLoaded === "function") { onLoaded(null, this); } } clearBufferedCommand() { this[kBufferedCommandSymbol] = ""; } _handleError(e: Error) { let errStack = ""; if (typeof e === "object" && e !== null) { const isError = ObjectPrototypeIsPrototypeOf(ErrorPrototype, e) || (typeof (e as any).name === "string" && typeof (e as any).stack === "string"); if (isError && (e as any).stack) { if (e.name === "SyntaxError") { // Remove stack trace errStack = StringPrototypeReplace( StringPrototypeReplace( StringPrototypeReplace(e.stack, reStackFrames, ""), reReplFilename, "", ), reReplLowerFilename, "", ); // Deno's V8 doesn't include source context in SyntaxError stacks // like Node.js does. Add it if we have the source code attached. if ((e as any)._replSourceCode) { const srcLine = (e as any)._replSourceCode; // Try to determine caret position from the error message. // Node.js uses V8's Message.GetStartColumn() which we don't have. let col = 0; const tokenMatch = RegExpPrototypeExec( reUnexpectedTokenName, e.message, ); if (tokenMatch) { const idx = StringPrototypeIndexOf(srcLine, tokenMatch[1]); if (idx !== -1) col = idx; } const caret = StringPrototypeRepeat(" ", col) + "^"; errStack = `${srcLine}\n${caret}\n\n${errStack}`; } } else { // For non-syntax errors, keep user stack frames but drop frames // that originate from the REPL/runtime internals, plus the trailing // top-level eval frame, so the trace matches Node's output // (which uses overrideStackTrace). Mutate `e.stack` so the // subsequent `writer(e)` call (inspect) renders the filtered stack // alongside any custom Error properties. (e as any).stack = filterReplStack(e.stack); } if (e.name !== "SyntaxError") { // Some node-style Errors (NodeTypeError etc.) install `toString` // as an own property; inspect would render that as a noisy // `[Function]` entry inside the `{ ... }` block AND wrap the // whole error in `[ ... ]`. Temporarily hide it (and any // similarly own-shadowed prototype methods) for the inspect call. const hidden: { key: string; desc: PropertyDescriptor }[] = []; for (const key of new SafeArrayIterator(["toString"])) { const desc = ObjectGetOwnPropertyDescriptor(e, key); if (desc) { ArrayPrototypePush(hidden, { key, desc }); delete (e as any)[key]; } } try { errStack = this.writer(e); } finally { for (const { key, desc } of new SafeArrayIterator(hidden)) { ObjectDefineProperty(e, key, desc); } } } } if (!errStack) { errStack = this.writer(e); } // Remove one line error braces to keep the old style in place. inspect // wraps an Error with no `at` frames in `[ErrorName: msg]`, optionally // followed by ` { extra: props }` when the Error has own enumerable // props. Node's REPL only strips the outer `[...]`, leaving the props // section intact. if (errStack[0] === "[") { const m = RegExpPrototypeExec(reErrorBraces, errStack); if (m) { errStack = m[2] ? `${m[1]} ${m[2]}` : m[1]; } } } if (!this.underscoreErrAssigned) { this.lastError = e; } if (errStack === "") { errStack = this.writer(e); } const lines = StringPrototypeSplit(errStack, reLineSplit); let matched = false; errStack = ""; ArrayPrototypeForEach(lines, (line: string) => { if ( !matched && RegExpPrototypeTest(reErrorName, line) ) { 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.closed || this._closing) return; this._closing = true; // Drop the `newListener` guard synchronously so user code can register // its own `uncaughtException` listener on the closed REPL (matches // Node, which removes the guard via 'exit'). The capture-callback // hook stays installed so async errors that were already pending in // the user's REPL eval still flow through `_handleError` to the REPL // output stream. if (this._processListenersAttached) { this._processListenersAttached = false; _removeNewListenerGuard(); } const self = this; process.nextTick(() => { try { // @ts-ignore - calling parent close FunctionPrototypeCall(Interface.prototype.close, self); } catch { // May fail if input stream already destroyed } self._closing = false; }); } createContext() { let context: any; if (this.useGlobal) { context = globalThis; } else { context = vm.createContext(); // Match Node: copy non-builtin own properties from globalThis into the // new context so user-defined globals are visible at REPL start. const builtinNames = new SafeSet( (vm as any).runInNewContext?.( "Object.getOwnPropertyNames(globalThis)", ) ?? [], ); for ( const name of new SafeArrayIterator( ObjectGetOwnPropertyNames(globalThis), ) ) { if (SetPrototypeHas(builtinNames, name)) continue; try { const desc = ObjectGetOwnPropertyDescriptor(globalThis, name); if (desc) ObjectDefineProperty(context, name, desc); } catch { // Non-configurable / non-writable props just get skipped. } } context.global = context; let _console; try { _console = new Console(this.output); } catch { _console = console; } ObjectDefineProperty(context, "console", { __proto__: null, configurable: true, writable: true, value: _console, }); } // Set up module and require in the context try { // `path` is the node:path module, not an Array; `path.join` is the // path joiner. // deno-lint-ignore deno-internal/prefer-primordials const replPath = path.join(process.cwd(), "repl"); const replRequire = Module.createRequire(replPath); ObjectDefineProperty(context, "require", { __proto__: null, configurable: true, writable: true, value: replRequire, }); } catch { // createRequire may fail in some environments } ObjectDefineProperty(context, "module", { __proto__: null, configurable: true, writable: true, value: { exports: {} }, }); // Mirror Node's addBuiltinLibsToObject: expose each non-underscored, // non-slashed builtin module as a lazy getter on the REPL context. This // is what makes `util.inspect(...)` work in the REPL without requiring // an explicit `require('util')`. const ctxRequire = (context as any).require; if (typeof ctxRequire === "function") { for (const name of new SafeArrayIterator(builtinModules)) { if ( name[0] === "_" || StringPrototypeIncludes(name, "/") || ObjectPrototypeHasOwnProperty(context, name) ) { continue; } const setReal = (val: unknown) => { delete (context as any)[name]; (context as any)[name] = val; }; try { ObjectDefineProperty(context, name, { __proto__: null, get: () => { try { const mod = ctxRequire(name); ObjectDefineProperty(context, name, { __proto__: null, configurable: true, writable: true, value: mod, }); return mod; } catch { return undefined; } }, set: setReal, configurable: true, enumerable: false, }); } catch { // Property may already be non-configurable; skip silently. } } } return context; } resetContext() { this.context = this.createContext(); this.underscoreAssigned = false; this.underscoreErrAssigned = false; 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?: boolean) { let prompt = this._initialPrompt; if (this[kBufferedCommandSymbol].length) { prompt = kMultilinePrompt; } super.setPrompt(prompt); this.prompt(preserveCursor); } setPrompt(prompt: string) { this._initialPrompt = prompt; super.setPrompt(prompt); } complete(...args: any[]) { ReflectApply(this.completer, this, args); } completeOnEditorMode(callback: any) { return (err: Error | null, results: any) => { if (err) return callback(err); const completions = results[0]; const completeOn = results[1] ?? ""; let result = ArrayPrototypeFilter(completions, Boolean); if (completeOn && result.length !== 0) { result = [commonPrefix(result)]; } callback(null, [result, completeOn]); }; } defineCommand( keyword: string, cmd: REPLCommand | ((this: REPLServer, ...args: unknown[]) => void), ) { if (typeof cmd === "function") { cmd = { action: cmd }; } else { validateFunction(cmd.action, "cmd.action"); } this.commands[keyword] = cmd; } } function completer(this: REPLServer, _text: string, cb: any) { cb(null, [[], _text]); } export function start( prompt?: any, source?: any, eval_?: any, useGlobal?: boolean, ignoreUndefined?: boolean, replMode?: symbol, ) { return new REPLServer( prompt, source, eval_, useGlobal, ignoreUndefined, replMode, ); } export const builtinModules = [ "assert", "async_hooks", "buffer", "child_process", "cluster", "console", "constants", "crypto", "dgram", "diagnostics_channel", "dns", "domain", "events", "fs", "http", "http2", "https", "inspector", "module", "net", "os", "path", "perf_hooks", "process", "punycode", "querystring", "readline", "repl", "stream", "string_decoder", "sys", "timers", "tls", "trace_events", "tty", "url", "util", "v8", "vm", "wasi", "worker_threads", "zlib", ]; export const _builtinLibs = builtinModules; // Module-level reference for standalone REPL tracking const _module = { exports: {} as Record<string, unknown> }; export default { REPLServer, builtinModules, _builtinLibs, start, writer, REPL_MODE_SLOPPY, REPL_MODE_STRICT, Recoverable, };