// 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("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"; */ 'use strict'; const { ArrayPrototypeAt, ArrayPrototypeFilter, ArrayPrototypeFindLastIndex, ArrayPrototypeForEach, ArrayPrototypeIncludes, ArrayPrototypeJoin, ArrayPrototypeMap, ArrayPrototypePop, ArrayPrototypePush, ArrayPrototypePushApply, ArrayPrototypeShift, ArrayPrototypeSlice, ArrayPrototypeSome, ArrayPrototypeSort, ArrayPrototypeUnshift, Boolean, Error: MainContextError, FunctionPrototypeBind, JSONStringify, MathMaxApply, NumberIsNaN, NumberParseFloat, ObjectAssign, ObjectDefineProperty, ObjectGetOwnPropertyDescriptor, ObjectGetOwnPropertyNames, ObjectGetPrototypeOf, ObjectKeys, ObjectSetPrototypeOf, Promise, ReflectApply, RegExp, RegExpPrototypeExec, SafePromiseRace, SafeSet, SafeWeakSet, StringPrototypeCharAt, StringPrototypeCodePointAt, StringPrototypeEndsWith, StringPrototypeIncludes, StringPrototypeRepeat, StringPrototypeSlice, StringPrototypeSplit, StringPrototypeStartsWith, StringPrototypeToLocaleLowerCase, StringPrototypeTrim, StringPrototypeTrimStart, Symbol, SyntaxError, SyntaxErrorPrototype, globalThis, } = primordials; const { BuiltinModule } = require('internal/bootstrap/realm'); const { makeRequireFunction, addBuiltinLibsToObject, } = require('internal/modules/helpers'); const { isIdentifierStart, isIdentifierChar, parse: acornParse, } = require('internal/deps/acorn/acorn/dist/acorn'); const acornWalk = require('internal/deps/acorn/acorn-walk/dist/walk'); const { decorateErrorStack, isError, deprecate, deprecateInstantiation, SideEffectFreeRegExpPrototypeSymbolReplace, SideEffectFreeRegExpPrototypeSymbolSplit, } = require('internal/util'); const { inspect } = require('internal/util/inspect'); const vm = require('vm'); const { runInThisContext, runInContext } = vm.Script.prototype; const path = require('path'); const fs = require('fs'); const { Interface } = require('readline'); const { commonPrefix, } = require('internal/readline/utils'); const { Console } = require('console'); const { shouldColorize } = require('internal/util/colors'); const CJSModule = require('internal/modules/cjs/loader').Module; let _builtinLibs = ArrayPrototypeFilter( CJSModule.builtinModules, (e) => e[0] !== '_', ); const nodeSchemeBuiltinLibs = ArrayPrototypeMap( _builtinLibs, (lib) => `node:${lib}`); ArrayPrototypeForEach( BuiltinModule.getSchemeOnlyModuleNames(), (lib) => ArrayPrototypePush(nodeSchemeBuiltinLibs, `node:${lib}`), ); const domain = require('domain'); let debug = require('internal/util/debuglog').debuglog('repl', (fn) => { debug = fn; }); const { ErrorPrepareStackTrace, codes: { ERR_CANNOT_WATCH_SIGINT, ERR_INVALID_REPL_EVAL_CONFIG, ERR_INVALID_REPL_INPUT, ERR_MISSING_ARGS, ERR_SCRIPT_EXECUTION_INTERRUPTED, }, isErrorStackTraceLimitWritable, overrideStackTrace, } = require('internal/errors'); const { sendInspectorCommand } = require('internal/util/inspector'); const { getOptionValue } = require('internal/options'); const { validateFunction, validateObject, } = require('internal/validators'); const experimentalREPLAwait = getOptionValue( '--experimental-repl-await', ); const pendingDeprecation = getOptionValue('--pending-deprecation'); const { REPL_MODE_SLOPPY, REPL_MODE_STRICT, isRecoverableError, kStandaloneREPL, setupPreview, setupReverseSearch, } = require('internal/repl/utils'); const { constants: { ALL_PROPERTIES, SKIP_SYMBOLS, }, getOwnNonIndexProperties, } = internalBinding('util'); const { startSigintWatchdog, stopSigintWatchdog, } = internalBinding('contextify'); const history = require('internal/repl/history'); const { extensionFormatMap, } = require('internal/modules/esm/formats'); const { makeContextifyScript, } = require('internal/vm'); 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++}`; } // Lazy-loaded. let processTopLevelAwait; const globalBuiltins = new SafeSet(vm.runInNewContext('Object.getOwnPropertyNames(globalThis)')); const parentModule = module; const domainSet = new SafeWeakSet(); const kBufferedCommandSymbol = Symbol('bufferedCommand'); const kContextId = Symbol('contextId'); const kLoadingSymbol = Symbol('loading'); let addedNewListener = false; try { // Hack for require.resolve("./relative") to work properly. module.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); module.filename = path.resolve(dirname, 'repl'); } // Hack for repl require to work properly with node_modules folders module.paths = CJSModule._nodeModulePaths(module.filename); // 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 = acornParse(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; }; function REPLServer(prompt, stream, eval_, useGlobal, ignoreUndefined, replMode) { if (!(this instanceof REPLServer)) { return deprecateInstantiation(REPLServer, 'DEP0185', 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; eval_ = options.eval; 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); } // TODO(devsnek): Add a test case for custom eval functions. const preview = options.terminal && (options.preview !== undefined ? !!options.preview : !eval_); 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._domain = options.domain || domain.create(); this.useGlobal = !!useGlobal; this.ignoreUndefined = !!ignoreUndefined; this.replMode = replMode || module.exports.REPL_MODE_SLOPPY; this.underscoreAssigned = false; this.last = undefined; this.underscoreErrAssigned = false; this.lastError = undefined; this.breakEvalOnSigint = !!options.breakEvalOnSigint; this.editorMode = false; // Context id for use with the inspector protocol. this[kContextId] = undefined; 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. module.exports.repl = this; } else if (!addedNewListener) { // 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', (event, listener) => { if (event === 'uncaughtException' && process.domain && listener.name !== 'domainUncaughtExceptionClear' && domainSet.has(process.domain)) { // Throw an error so that the event will not be added and the current // domain takes over. 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'); } }); addedNewListener = true; } domainSet.add(this._domain); 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; // It's confusing for `{ a : 1 }` to be interpreted as a block // statement rather than an object literal. So, we first try // to wrap it in parentheses, so that it will be interpreted as // an expression. Note that if the above condition changes, // lib/internal/repl/utils.js needs to be changed to match. if (RegExpPrototypeExec(/^\s*{/, code) !== null && RegExpPrototypeExec(/;\s*$/, code) === null) { code = `(${StringPrototypeTrim(code)})\n`; wrappedCmd = true; } const hostDefinedOptionId = Symbol(`eval:${file}`); let parentURL; try { const { pathToFileURL } = require('internal/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) { const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader(); return cascadedLoader.import(specifier, parentURL, importAttributes); } // `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 === module.exports.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 = ReflectApply(runInThisContext, script, [scriptOptions]); } else { result = ReflectApply(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 (process.domain) { debug('not recoverable, send to domain'); process.domain.emit('error', err); process.domain.exit(); 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 (err && process.domain) { debug('not recoverable, send to domain'); process.domain.emit('error', err); process.domain.exit(); return; } finishExecution(err); } finally { // Remove prioritized SIGINT listener if it was not called. prioritizedSigintQueue.delete(sigintListener); unpause(); } })(); } } if (!awaitPromise || err) { finishExecution(err, result); } } self.eval = self._domain.bind(eval_); self._domain.on('error', function debugDomainError(e) { debug('domain error'); let errStack = ''; if (typeof e === 'object' && e !== null) { overrideStackTrace.set(e, (error, stackFrames) => { let frames; if (typeof stackFrames === 'object') { // Search from the bottom of the call stack to // find the first frame with a null function name const idx = ArrayPrototypeFindLastIndex( stackFrames, (frame) => frame.getFunctionName() === null, ); // If found, get rid of it and everything below it frames = ArrayPrototypeSlice(stackFrames, 0, idx); } else { frames = stackFrames; } // FIXME(devsnek): this is inconsistent with the checks // that the real prepareStackTrace dispatch uses in // lib/internal/errors.js. if (typeof MainContextError.prepareStackTrace === 'function') { return MainContextError.prepareStackTrace(error, frames); } return ErrorPrepareStackTrace(error, frames); }); decorateErrorStack(e); if (e.domainThrown) { delete e.domain; delete e.domainThrown; } if (isError(e)) { 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(self.lines, -1)); e.stack = SideEffectFreeRegExpPrototypeSymbolReplace( /SyntaxError:.*\n/, e.stack, `SyntaxError: ${e.message}\n`); } } else if (self.replMode === module.exports.REPL_MODE_STRICT) { e.stack = SideEffectFreeRegExpPrototypeSymbolReplace( /(\s+at\s+REPL\d+:)(\d+)/, e.stack, (_, pre, line) => pre + (line - 1), ); } } errStack = self.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 (!self.underscoreErrAssigned) { self.lastError = e; } if (options[kStandaloneREPL] && process.listenerCount('uncaughtException') !== 0) { process.nextTick(() => { process.emit('uncaughtException', e); self.clearBufferedCommand(); self.lines.level = []; self.displayPrompt(); }); } else { if (errStack === '') { errStack = self.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'; self.output.write(errStack); self.clearBufferedCommand(); self.lines.level = []; self.displayPrompt(); } }); self.clearBufferedCommand(); function completer(text, cb) { ReflectApply(complete, self, [text, self.editorMode ? self.completeOnEditorMode(cb) : cb]); } ReflectApply(Interface, this, [{ input: options.input, output: options.output, completer: options.completer || completer, terminal: options.terminal, historySize: options.historySize, prompt, }]); self.resetContext(); this.commands = { __proto__: null }; defineDefaultCommands(this); // Figure out which "writer" function to use self.writer = options.writer || module.exports.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) { ReflectApply(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; } ReflectApply(_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 (ReflectApply(_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); self.eval(evalCmd, self.context, getREPLResourceName(), finish); function finish(e, ret) { debug('finish', e, ret); ReflectApply(_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 if (e) { if (e instanceof Recoverable && !sawCtrlD) { // Start buffering data like that: // { // ... x: 1 // ... } self[kBufferedCommandSymbol] += cmd + '\n'; self.displayPrompt(); return; } self._domain.emit('error', e.err || e); } // 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'); } // Display prompt again (unless we already did by emitting the 'error' // event on the domain instance). if (!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); } }); 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) { // 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(); } ObjectSetPrototypeOf(REPLServer.prototype, Interface.prototype); ObjectSetPrototypeOf(REPLServer, Interface); // 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); } REPLServer.prototype.setupHistory = function setupHistory(historyFile, cb) { history(this, historyFile, cb); }; REPLServer.prototype.clearBufferedCommand = function clearBufferedCommand() { this[kBufferedCommandSymbol] = ''; }; REPLServer.prototype.close = function close() { if (this.terminal && this._flushing && !this._closingOnFlush) { this._closingOnFlush = true; this.once('flushHistory', () => ReflectApply(Interface.prototype.close, this, []), ); return; } process.nextTick(() => ReflectApply(Interface.prototype.close, this, []), ); }; REPLServer.prototype.createContext = function() { 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; }; REPLServer.prototype.resetContext = function() { this.context = this.createContext(); this.underscoreAssigned = false; this.underscoreErrAssigned = false; // 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); }; REPLServer.prototype.displayPrompt = function(preserveCursor) { let prompt = this._initialPrompt; if (this[kBufferedCommandSymbol].length) { prompt = '...'; const len = this.lines.level.length ? this.lines.level.length - 1 : 0; const levelInd = StringPrototypeRepeat('..', len); prompt += levelInd + ' '; } // Do not overwrite `_initialPrompt` here ReflectApply(Interface.prototype.setPrompt, this, [prompt]); this.prompt(preserveCursor); }; // When invoked as an API method, overwrite _initialPrompt REPLServer.prototype.setPrompt = function setPrompt(prompt) { this._initialPrompt = prompt; ReflectApply(Interface.prototype.setPrompt, this, [prompt]); }; 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 simpleExpressionRE = /(?:[\w$'"`[{(](?:\w|\$|['"`\]})])*\??\.)*[a-zA-Z_$](?:\w|\$)*\??\.?$/; const versionedFileNamesRe = /-\d+\.\d+/; function isIdentifier(str) { if (str === '') { return false; } const first = StringPrototypeCodePointAt(str, 0); if (!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 (!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 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 getGlobalLexicalScopeNames(contextId) { return sendInspectorCommand((session) => { let names = []; session.post('Runtime.globalLexicalScopeNames', { executionContextId: contextId, }, (error, result) => { if (!error) names = result.names; }); return names; }, () => []); } REPLServer.prototype.complete = function() { ReflectApply(this.completer, this, arguments); }; 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 eval's code like "foo.bar.baz", so it will run property // getter code. 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(this.context.require.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, 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, _builtinLibs, 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(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, _builtinLibs, nodeSchemeBuiltinLibs); } else if ((match = RegExpPrototypeExec(fsAutoCompleteRE, line)) !== null && this.allowBlockingCompletions) { ({ 0: completionGroups, 1: completeOn } = completeFSFunctions(match)); // Handle variable member lookup. // We support simple chained expressions like the following (no function // calls, etc.). That is for simplicity and also because we *eval* that // leading expression so for safety (see WARNING above) don't want to // eval function calls. // // foo.bar<|> # completions for 'foo' with filter 'bar' // spam.eggs.<|> # completions for 'spam.eggs' with filter '' // foo<|> # all scope vars with filter 'foo' // foo.<|> # completions for 'foo' with filter '' } else if (line.length === 0 || RegExpPrototypeExec(/\w|\.|\$/, line[line.length - 1]) !== null) { const { 0: match } = RegExpPrototypeExec(simpleExpressionRE, line) || ['']; if (line.length !== 0 && !match) { completionGroupsLoaded(); return; } let expr = ''; completeOn = match; if (StringPrototypeEndsWith(line, '.')) { expr = StringPrototypeSlice(match, 0, -1); } else if (line.length !== 0) { const bits = StringPrototypeSplit(match, '.'); 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; } let chaining = '.'; if (StringPrototypeEndsWith(expr, '?')) { expr = StringPrototypeSlice(expr, 0, -1); chaining = '?.'; } const memberGroups = []; const evalExpr = `try { ${expr} } catch {}`; this.eval(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; } 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]); } } REPLServer.prototype.completeOnEditorMode = (callback) => (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]); }; REPLServer.prototype.defineCommand = function(keyword, cmd) { if (typeof cmd === 'function') { cmd = { action: cmd }; } else { validateFunction(cmd.action, 'cmd.action'); } this.commands[keyword] = cmd; }; // 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 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 _turnOnEditorMode(repl) { repl.editorMode = true; ReflectApply(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'); }, }); } } function Recoverable(err) { this.err = err; } ObjectSetPrototypeOf(Recoverable.prototype, SyntaxErrorPrototype); ObjectSetPrototypeOf(Recoverable, SyntaxError); module.exports = { start, writer, REPLServer, REPL_MODE_SLOPPY, REPL_MODE_STRICT, Recoverable, }; ObjectDefineProperty(module.exports, 'builtinModules', { __proto__: null, get: () => _builtinLibs, set: (val) => _builtinLibs = val, enumerable: true, configurable: true, }); ObjectDefineProperty(module.exports, '_builtinLibs', { __proto__: null, get: pendingDeprecation ? deprecate( () => _builtinLibs, 'repl._builtinLibs is deprecated. Check module.builtinModules instead', 'DEP0142', ) : () => _builtinLibs, set: pendingDeprecation ? deprecate( (val) => _builtinLibs = val, 'repl._builtinLibs is deprecated. Check module.builtinModules instead', 'DEP0142', ) : (val) => _builtinLibs = val, enumerable: false, configurable: true, });