Files
node/lib/repl.js
T
2026-07-25 23:56:46 -07:00

1275 lines
40 KiB
JavaScript

// 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 {
ArrayPrototypeFilter,
ArrayPrototypeFindLastIndex,
ArrayPrototypeForEach,
ArrayPrototypeJoin,
ArrayPrototypeMap,
ArrayPrototypePop,
ArrayPrototypePush,
ArrayPrototypeShift,
ArrayPrototypeSlice,
ArrayPrototypeSort,
Boolean,
Error: MainContextError,
FunctionPrototypeBind,
FunctionPrototypeCall,
MathMaxApply,
NumberIsNaN,
NumberParseFloat,
ObjectAssign,
ObjectDefineProperty,
ObjectGetOwnPropertyDescriptor,
ObjectGetOwnPropertyNames,
ObjectKeys,
ReflectApply,
RegExpPrototypeExec,
StringPrototypeCharAt,
StringPrototypeEndsWith,
StringPrototypeRepeat,
StringPrototypeSlice,
StringPrototypeStartsWith,
StringPrototypeTrim,
Symbol,
globalThis,
} = primordials;
const {
makeRequireFunction,
addBuiltinLibsToObject,
} = require('internal/modules/helpers');
const {
decorateErrorStack,
isError,
deprecate,
SideEffectFreeRegExpPrototypeSymbolReplace,
SideEffectFreeRegExpPrototypeSymbolSplit,
} = require('internal/util');
const { inspect } = require('internal/util/inspect');
const vm = require('vm');
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 { highlight } = require('internal/repl/highlight');
const CJSModule = require('internal/modules/cjs/loader').Module;
const { AsyncLocalStorage } = require('async_hooks');
let debug = require('internal/util/debuglog').debuglog('repl', (fn) => {
debug = fn;
});
const {
ErrorPrepareStackTrace,
codes: {
ERR_INVALID_ARG_VALUE,
ERR_INVALID_REPL_EVAL_CONFIG,
ERR_INVALID_STATE,
ERR_MISSING_ARGS,
ERR_SCRIPT_EXECUTION_INTERRUPTED,
},
isErrorStackTraceLimitWritable,
overrideStackTrace,
} = require('internal/errors');
const { getReplInspector } = require('internal/repl/inspector');
const { getOptionValue } = require('internal/options');
const {
validateFunction,
validateObject,
} = require('internal/validators');
const pendingDeprecation = getOptionValue('--pending-deprecation');
const {
REPL_MODE_SLOPPY,
REPL_MODE_STRICT,
kStandaloneREPL,
setupPreview,
setupReverseSearch,
kContextId,
getREPLResourceName,
getGlobalBuiltins,
getReplBuiltinLibs,
setReplBuiltinLibs,
fixReplRequire,
isValidSyntax,
} = require('internal/repl/utils');
const {
complete,
} = require('internal/repl/completion');
const {
createReplEval,
kThrownValue,
Recoverable,
} = require('internal/repl/eval');
const {
kMultilinePrompt,
kAddNewLineOnTTY,
kLastCommandErrored,
} = require('internal/readline/interface');
const parentModule = module;
// AsyncLocalStorage to track which REPL instance owns the current async context.
const replContext = new AsyncLocalStorage();
let exceptionCaptureSetup = false;
/**
* Sets up the uncaught exception capture callback to route errors
* to the appropriate REPL instance. Uses addUncaughtExceptionCaptureCallback
* so it coexists with the primary callback (e.g. the domain module).
*/
function setupExceptionCapture() {
if (exceptionCaptureSetup) return;
process.addUncaughtExceptionCaptureCallback((err) => {
const store = replContext.getStore();
if (store?.replServer) {
const result = store.replServer._handleError(err);
return result !== 'unhandled'; // We handled it
}
// No active REPL context - let other handlers try
});
exceptionCaptureSetup = true;
}
const kBufferedCommandSymbol = Symbol('bufferedCommand');
const kLoadingSymbol = Symbol('loading');
// TODO(@avivkeller): While this behavior is consistent with the old REPL,
// I wonder if it's still desirable to close in this way, or if we should
// just close immediately and let the pending evaluations fail.
const kPendingClose = Symbol('pendingClose');
const kHasPendingWork = Symbol('hasPendingWork');
const kDrainOnClose = Symbol('drainOnClose');
fixReplRequire(module);
// This is the default "writer" value, if none is passed in the REPL options,
// and it can be overridden by custom print functions, such as `probe` or
// `eyes.js`.
const writer = (obj) => inspect(obj, writer.options);
writer.options = { ...inspect.defaultOptions, showProxy: true };
class REPLServer extends Interface {
constructor(prompt,
stream,
eval_,
useGlobal,
ignoreUndefined,
replMode) {
let options;
if (prompt !== null && typeof prompt === 'object') {
// An options object was given.
options = { ...prompt };
stream = options.stream || options.socket;
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);
}
const preview = options.terminal &&
(options.preview !== undefined ? !!options.preview : !eval_);
super({
input: options.input,
output: options.output,
completer: options.completer || completer,
terminal: options.terminal,
historySize: options.historySize,
prompt,
colorize: options.useColors ? highlight : undefined,
});
ObjectDefineProperty(this, 'inputStream', {
__proto__: null,
get: pendingDeprecation ?
deprecate(() => this.input,
'repl.inputStream and repl.outputStream are deprecated. ' +
'Use repl.input and repl.output instead',
'DEP0141') :
() => this.input,
set: pendingDeprecation ?
deprecate((val) => this.input = val,
'repl.inputStream and repl.outputStream are deprecated. ' +
'Use repl.input and repl.output instead',
'DEP0141') :
(val) => this.input = val,
enumerable: false,
configurable: true,
});
ObjectDefineProperty(this, 'outputStream', {
__proto__: null,
get: pendingDeprecation ?
deprecate(() => this.output,
'repl.inputStream and repl.outputStream are deprecated. ' +
'Use repl.input and repl.output instead',
'DEP0141') :
() => this.output,
set: pendingDeprecation ?
deprecate((val) => this.output = val,
'repl.inputStream and repl.outputStream are deprecated. ' +
'Use repl.input and repl.output instead',
'DEP0141') :
(val) => this.output = val,
enumerable: false,
configurable: true,
});
this.allowBlockingCompletions = !!options.allowBlockingCompletions;
this.useColors = !!options.useColors;
this._isStandalone = !!options[kStandaloneREPL];
if (options.domain !== undefined) {
throw new ERR_INVALID_ARG_VALUE('options.domain', options.domain,
'is no longer supported');
}
this.useGlobal = !!useGlobal;
this.ignoreUndefined = !!ignoreUndefined;
this.replMode = replMode || REPL_MODE_SLOPPY;
this.underscoreAssigned = false;
this.last = undefined;
this.underscoreErrAssigned = false;
this.lastError = undefined;
this.breakEvalOnSigint = !!options.breakEvalOnSigint;
this.editorMode = false;
this._userErrorHandler = options.handleError;
// Context id for use with the inspector protocol.
this[kContextId] = undefined;
this[kLastCommandErrored] = false;
this[kPendingClose] = false;
this[kDrainOnClose] = false;
// Readline calls close() for both explicit programmatic closure and input
// EOF. Only EOF should wait for queued evaluations to drain.
this.input.prependListener('end', () => {
this[kDrainOnClose] = true;
});
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;
}
// Set up exception capture for async error handling
setupExceptionCapture();
const self = this;
eval_ ||= createReplEval(self);
// 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;
}
// Wrap eval to run within the REPL's async context for error tracking.
// The function names are needed for stack trace filtering - they must not
// be anonymous, but we can't use 'eval' as a name since it's reserved.
const originalEval = eval_;
// eslint-disable-next-line node-core/func-name-matching
self.eval = function REPLEval(code, context, file, cb) {
replContext.run({ replServer: self }, function REPLEvalInContext() {
originalEval(code, context, file, cb);
});
};
self.clearBufferedCommand();
async function completer(line, callback) {
try {
const result = await complete(line, self);
callback(null, self.editorMode ? collapseCompletions(result) : result);
} catch (err) {
callback(err);
}
}
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) {
FunctionPrototypeCall(cmd.action, this, rest);
return true;
}
return false;
}
self.on('close', function emitExit() {
if (paused) {
ArrayPrototypePush(pausedBuffer, ['close']);
return;
}
self.emit('exit');
});
let sawSIGINT = false;
let sawCtrlD = false;
// Evaluation is always asynchronous (the inspector round-trips through
// its message channel), so lines must be processed strictly one at a time.
// Without this, a chunk containing several piped lines would emit every
// 'line' event synchronously and launch overlapping evaluations, breaking
// multi-line (recoverable) buffering. The queue serializes them: the next
// line is not started until the current evaluation has reported back.
const lineQueue = [];
let busy = false;
let pumping = false;
// The finish callback of the in-flight evaluation, if any. Used both to
// resume the queue and, when `breakEvalOnSigint` is set, to abort a stuck
// evaluation in response to Ctrl+C.
let currentEval = null;
function pump() {
if (pumping) return;
pumping = true;
while (!busy && lineQueue.length > 0) {
busy = true;
const cmd = ArrayPrototypeShift(lineQueue);
processLine(cmd);
}
pumping = false;
}
function idle() {
busy = false;
pump();
// A close requested while an evaluation was running was deferred (see
// `close()`); now that the queue has drained, perform it so that no
// queued line's output is dropped.
if (!busy && lineQueue.length === 0 && self[kPendingClose]) {
self[kPendingClose] = false;
self.close();
}
}
// Bridges the constructor-local queue state to the prototype `close()`.
self[kHasPendingWork] = () => busy || lineQueue.length > 0;
self.on('SIGINT', function onSigInt() {
// When an evaluation is in flight and the REPL was configured to break on
// SIGINT, abandon it and report the interruption. The inspector response
// for the abandoned evaluation, if it ever arrives, is ignored by the
// idempotent finish callback.
if (currentEval !== null && self.breakEvalOnSigint) {
const finish = currentEval;
currentEval = null;
const tmp = MainContextError.stackTraceLimit;
if (isErrorStackTraceLimitWritable()) MainContextError.stackTraceLimit = 0;
const err = new ERR_SCRIPT_EXECUTION_INTERRUPTED();
if (isErrorStackTraceLimitWritable()) MainContextError.stackTraceLimit = tmp;
finish(err);
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) {
// Editor-mode input only accumulates into the buffered command and never
// triggers an asynchronous evaluation, so it must not pass through the
// busy-gated line queue.
if (self.editorMode) {
processLine(cmd);
return;
}
ArrayPrototypePush(lineQueue, cmd);
pump();
});
function processLine(cmd) {
debug('line %j', cmd);
cmd ||= '';
sawSIGINT = false;
if (self.editorMode) {
self[kBufferedCommandSymbol] += cmd + '\n';
// code alignment
const matches = self._sawKeyPress && !self[kLoadingSymbol] ?
RegExpPrototypeExec(/^\s+/, cmd) : null;
if (matches) {
const prefix = matches[0];
self.write(prefix);
self.line = prefix;
self.cursor = prefix.length;
}
FunctionPrototypeCall(_memory, self, cmd);
idle();
return;
}
// Check REPL keywords and empty lines against a trimmed line input.
const trimmedCmd = StringPrototypeTrim(cmd);
// Set up the (idempotent) finish callback for this line's evaluation.
let done = false;
function finish() {
if (done) return;
done = true;
currentEval = null;
try {
ReflectApply(onComplete, self, arguments);
} catch (e) {
// `onComplete` runs inside the asynchronous evaluator's promise
// chain, so a throw here (e.g. an invalid value returned by a user
// `handleError`) would otherwise be silently swallowed.
replContext.exit(() => process.nextTick(() => { throw e; }));
} finally {
// Clear the `busy` flag first so that the next line submitted while
// replaying buffered keystrokes can start its own evaluation.
idle();
unpause();
}
}
// Check to see if a REPL keyword was used. If it returns true,
// display next prompt and return.
if (trimmedCmd) {
if (StringPrototypeCharAt(trimmedCmd, 0) === '.' &&
StringPrototypeCharAt(trimmedCmd, 1) !== '.' &&
NumberIsNaN(NumberParseFloat(trimmedCmd))) {
const matches = RegExpPrototypeExec(/^\.([^\s]+)\s*(.*)$/, trimmedCmd);
const keyword = matches?.[1];
const rest = matches?.[2];
if (FunctionPrototypeCall(_parseREPLKeyword, self, keyword, rest) === true) {
idle();
return;
}
if (!self[kBufferedCommandSymbol]) {
self.output.write('Invalid REPL keyword\n');
finish(null);
return;
}
}
}
const evalCmd = self[kBufferedCommandSymbol] + cmd + '\n';
debug('eval %j', evalCmd);
currentEval = finish;
pause();
self.eval(evalCmd, self.context, getREPLResourceName(), finish);
function onComplete(e, ret) {
debug('finish', e, ret);
FunctionPrototypeCall(_memory, self, cmd);
if (e && !self[kBufferedCommandSymbol] &&
StringPrototypeStartsWith(StringPrototypeTrim(cmd), 'npm ') &&
!(e instanceof Recoverable)) {
self.output.write('npm should be run outside of the ' +
'Node.js REPL, in your normal shell.\n' +
'(Press Ctrl+D to exit.)\n');
self.displayPrompt();
return;
}
// If error was SyntaxError and not JSON.parse error
// We can start a multiline command
if (e instanceof Recoverable && !sawCtrlD) {
if (self.terminal) {
self[kAddNewLineOnTTY]();
} else {
self[kBufferedCommandSymbol] += cmd + '\n';
self.displayPrompt();
}
return;
}
if (e) {
self._handleError(
e instanceof Recoverable ? e.err :
e === kThrownValue ? ret : e,
e === kThrownValue,
);
self[kLastCommandErrored] = true;
}
// Clear buffer if no SyntaxErrors
self.clearBufferedCommand();
sawCtrlD = false;
// If we got any output - print it (if no error)
if (!e &&
// When an invalid REPL command is used, error message is printed
// immediately. We don't have to print anything else. So, only when
// the second argument to this function is there, print it.
arguments.length === 2 &&
(!self.ignoreUndefined || ret !== undefined)) {
if (!self.underscoreAssigned) {
self.last = ret;
}
self.output.write(self.writer(ret) + '\n');
}
// If the REPL sever hasn't closed display prompt again (unless we already
// did by emitting the 'error' event on the domain instance).
if (!self.closed && !e) {
self[kLastCommandErrored] = false;
self.displayPrompt();
}
}
}
self.on('SIGCONT', function onSigCont() {
if (self.editorMode) {
self.output.write(`${self._initialPrompt}.editor\n`);
self.output.write(
'// Entering editor mode (Ctrl+D to finish, Ctrl+C to cancel)\n');
self.output.write(`${self[kBufferedCommandSymbol]}\n`);
self.prompt(true);
} else {
self.displayPrompt(true);
}
});
const { reverseSearch } = setupReverseSearch(this);
const {
clearPreview, showPreview,
} = setupPreview(
this,
kContextId,
kBufferedCommandSymbol,
preview,
);
// Wrap readline tty to enable editor mode and pausing.
const ttyWrite = FunctionPrototypeBind(self._ttyWrite, self);
self._ttyWrite = (d, key) => {
key ||= {};
if (paused && !(self.breakEvalOnSigint && key.ctrl && key.name === 'c')) {
ArrayPrototypePush(pausedBuffer,
['key', [d, key], self.isCompletionEnabled]);
return;
}
if (!self.editorMode || !self.terminal) {
// Before exiting, make sure to clear the line.
if (key.ctrl && key.name === 'd' &&
self.cursor === 0 && self.line.length === 0) {
self.clearLine();
}
clearPreview(key);
if (!reverseSearch(d, key)) {
ttyWrite(d, key);
const showCompletionPreview = key.name !== 'escape';
showPreview(showCompletionPreview);
}
return;
}
// Editor mode
if (key.ctrl && !key.shift) {
switch (key.name) {
// TODO(BridgeAR): There should not be a special mode necessary for full
// multiline support.
case 'd': // End editor mode
_turnOffEditorMode(self);
sawCtrlD = true;
ttyWrite(d, { name: 'return' });
break;
case 'n': // Override next history item
case 'p': // Override previous history item
break;
default:
ttyWrite(d, key);
}
} else {
switch (key.name) {
case 'up': // Override previous history item
case 'down': // Override next history item
break;
case 'tab':
// Prevent double tab behavior
self._previousKey = null;
ttyWrite(d, key);
break;
default:
ttyWrite(d, key);
}
}
};
self.displayPrompt();
}
setupHistory(historyConfig = { __proto__: null }, cb) {
// TODO(puskin94): necessary because historyConfig can be a string for backwards compatibility
const options = typeof historyConfig === 'string' ?
{ filePath: historyConfig } :
historyConfig;
if (typeof cb === 'function') {
options.onHistoryFileLoaded = cb;
}
this.setupHistoryManager(options);
}
clearBufferedCommand() {
this[kBufferedCommandSymbol] = '';
}
_handleError(e, forceDisplay = false) {
debug('handle error');
if (this._userErrorHandler) {
const state = this._userErrorHandler(e);
if (state !== 'ignore' && state !== 'print' && state !== 'unhandled') {
throw new ERR_INVALID_STATE(
'External REPL error handler must return either "ignore", "print"' +
`, or "unhandled", but received: ${state}`);
}
if (state === 'ignore') {
return;
}
if (state === 'unhandled') {
return 'unhandled';
}
}
let errStack = '';
if (typeof e === 'object' && e !== null) {
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 (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, ''),
'');
} else if (this.replMode === REPL_MODE_STRICT) {
e.stack = SideEffectFreeRegExpPrototypeSymbolReplace(
/(\s+at\s+REPL\d+:)(\d+)/,
e.stack,
(_, pre, line) => pre + (line - 1),
);
}
}
errStack = this.writer(e);
// Remove one line error braces to keep the old style in place.
if (errStack[0] === '[' && errStack[errStack.length - 1] === ']') {
errStack = StringPrototypeSlice(errStack, 1, -1);
}
}
}
if (!this.underscoreErrAssigned) {
this.lastError = e;
}
if (!forceDisplay && this._isStandalone &&
process.listenerCount('uncaughtException') !== 0) {
process.nextTick(() => {
process.emit('uncaughtException', e);
this.clearBufferedCommand();
this.lines.level = [];
if (!this.closed) {
this.displayPrompt();
}
});
} else {
if (errStack === '') {
errStack = this.writer(e);
}
const lines = SideEffectFreeRegExpPrototypeSymbolSplit(/(?<=\n)/, errStack);
let matched = false;
errStack = '';
ArrayPrototypeForEach(lines, (line) => {
if (!matched &&
RegExpPrototypeExec(/^\[?([A-Z][a-z0-9_]*)*Error/, line) !== null) {
errStack += writer.options.breakLength >= line.length ?
`Uncaught ${line}` :
`Uncaught:\n${line}`;
matched = true;
} else {
errStack += line;
}
});
if (!matched) {
const ln = lines.length === 1 ? ' ' : ':\n';
errStack = `Uncaught${ln}${errStack}`;
}
// Normalize line endings.
errStack += StringPrototypeEndsWith(errStack, '\n') ? '' : '\n';
this.output.write(errStack);
this.clearBufferedCommand();
this.lines.level = [];
if (!this.closed) {
this.displayPrompt();
}
}
}
close() {
// Evaluation is asynchronous, so close() may be reached while evaluations
// are still queued or in flight (e.g. readline reaching EOF on piped input
// that contains a top-level `await`). Defer the teardown until the queue
// drains so the pending output is emitted first; `idle()` retries it.
if (this[kDrainOnClose] && this[kHasPendingWork]?.()) {
this[kPendingClose] = true;
return;
}
this[kDrainOnClose] = false;
if (this.terminal && this.historyManager?.isFlushing && !this._closingOnFlush) {
this._closingOnFlush = true;
this.once('flushHistory', () => this.close());
return;
}
// Ensure the history file handle is closed before completing
if (this.terminal && this.historyManager?.closeHandle && !this._historyHandleClosed) {
this._historyHandleClosed = true;
this.historyManager.closeHandle().then(() => super.close());
return;
}
process.nextTick(() => super.close());
}
createContext() {
let context;
if (this.useGlobal) {
context = globalThis;
} else {
// Capture the new context's inspector execution-context id on the REPL's
// shared session, so later evaluations can be scoped to it.
const created = getReplInspector(this).captureExecutionContextId(
() => vm.createContext(vm.constants.DONT_CONTEXTIFY));
context = created.context;
this[kContextId] = created.contextId;
ArrayPrototypeForEach(ObjectGetOwnPropertyNames(globalThis), (name) => {
// Only set properties that do not already exist as a global builtin.
if (!getGlobalBuiltins().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('<repl>');
replModule.paths = CJSModule._resolveLookupPaths('<repl>', 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, '<REPL>');
return context;
}
resetContext() {
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);
}
displayPrompt(preserveCursor) {
let prompt = this._initialPrompt;
if (this[kBufferedCommandSymbol].length) {
prompt = kMultilinePrompt.description;
}
// Do not overwrite `_initialPrompt` here
super.setPrompt(prompt);
this.prompt(preserveCursor);
}
// When invoked as an API method, overwrite _initialPrompt
setPrompt(prompt) {
this._initialPrompt = prompt;
super.setPrompt(prompt);
}
complete() {
ReflectApply(this.completer, this, arguments);
}
defineCommand(keyword, cmd) {
if (typeof cmd === 'function') {
cmd = { action: cmd };
} else {
validateFunction(cmd.action, 'cmd.action');
}
this.commands[keyword] = cmd;
}
}
// Collapses a `[completions, completeOn]` result to a single common-prefix
// candidate, used in editor mode so tab inserts that prefix instead of listing.
function collapseCompletions({ 0: completions, 1: completeOn = '' }) {
let result = ArrayPrototypeFilter(completions, Boolean);
if (completeOn && result.length !== 0) {
result = [commonPrefix(result)];
}
return [result, completeOn];
}
// 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);
}
// TODO(BridgeAR): This should be replaced with acorn to build an AST. The
// language became more complex and using a simple approach like this is not
// sufficient anymore.
function _memory(cmd) {
const self = this;
self.lines ||= [];
self.lines.level ||= [];
// Save the line so I can do magic later
if (cmd) {
const len = self.lines.level.length ? self.lines.level.length - 1 : 0;
ArrayPrototypePush(self.lines, StringPrototypeRepeat(' ', len) + cmd);
} else {
// I don't want to not change the format too much...
ArrayPrototypePush(self.lines, '');
}
if (!cmd) {
self.lines.level = [];
return;
}
// I need to know "depth."
// Because I can not tell the difference between a } that
// closes an object literal and a } that closes a function
const countMatches = (regex, str) => {
let count = 0;
while (RegExpPrototypeExec(regex, str) !== null) count++;
return count;
};
// Going down is { and ( e.g. function() {
// going up is } and )
const dw = countMatches(/[{(]/g, cmd);
const up = countMatches(/[})]/g, cmd);
let depth = dw.length - up.length;
if (depth) {
(function workIt() {
if (depth > 0) {
// Going... down.
// Push the line#, depth count, and if the line is a function.
// Since JS only has functional scope I only need to remove
// "function() {" lines, clearly this will not work for
// "function()
// {" but nothing should break, only tab completion for local
// scope will not work for this function.
ArrayPrototypePush(self.lines.level, {
line: self.lines.length - 1,
depth: depth,
});
} else if (depth < 0) {
// Going... up.
const curr = ArrayPrototypePop(self.lines.level);
if (curr) {
const tmp = curr.depth + depth;
if (tmp < 0) {
// More to go, recurse
depth += curr.depth;
workIt();
} else if (tmp > 0) {
// Remove and push back
curr.depth += depth;
ArrayPrototypePush(self.lines.level, curr);
}
}
}
}());
}
}
function _turnOnEditorMode(repl) {
repl.editorMode = true;
FunctionPrototypeCall(Interface.prototype.setPrompt, repl, '');
}
function _turnOffEditorMode(repl) {
repl.editorMode = false;
repl.setPrompt(repl._initialPrompt);
}
function defineDefaultCommands(repl) {
repl.defineCommand('break', {
help: 'Sometimes you get stuck, this gets you out',
action: function() {
this.clearBufferedCommand();
this.displayPrompt();
},
});
let clearMessage;
if (repl.useGlobal) {
clearMessage = 'Alias for .break';
} else {
clearMessage = 'Break, and also clear the local context';
}
repl.defineCommand('clear', {
help: clearMessage,
action: function() {
this.clearBufferedCommand();
if (!this.useGlobal) {
this.output.write('Clearing context...\n');
this.resetContext();
}
this.displayPrompt();
},
});
repl.defineCommand('exit', {
help: 'Exit the REPL',
action: function() {
this.close();
},
});
repl.defineCommand('help', {
help: 'Print this help message',
action: function() {
const names = ArrayPrototypeSort(ObjectKeys(this.commands));
const longestNameLength = MathMaxApply(
ArrayPrototypeMap(names, (name) => name.length),
);
ArrayPrototypeForEach(names, (name) => {
const cmd = this.commands[name];
const spaces =
StringPrototypeRepeat(' ', longestNameLength - name.length + 3);
const line = `.${name}${cmd.help ? spaces + cmd.help : ''}\n`;
this.output.write(line);
});
this.output.write('\nPress Ctrl+C to abort current expression, ' +
'Ctrl+D to exit the REPL\n');
this.displayPrompt();
},
});
repl.defineCommand('save', {
help: 'Save all evaluated commands in this REPL session to a file',
action: function(file) {
try {
if (file === '') {
throw new ERR_MISSING_ARGS('file');
}
fs.writeFileSync(file, ArrayPrototypeJoin(this.lines, '\n'));
this.output.write(`Session saved to: ${file}\n`);
} catch (error) {
if (error instanceof ERR_MISSING_ARGS) {
this.output.write(`${error.message}\n`);
} else {
this.output.write(`Failed to save: ${file}\n`);
}
}
this.displayPrompt();
},
});
repl.defineCommand('load', {
help: 'Load JS from a file into the REPL session',
action: function(file) {
try {
if (file === '') {
throw new ERR_MISSING_ARGS('file');
}
const stats = fs.statSync(file);
if (stats && stats.isFile()) {
_turnOnEditorMode(this);
this[kLoadingSymbol] = true;
const data = fs.readFileSync(file, 'utf8');
this.write(data);
this[kLoadingSymbol] = false;
_turnOffEditorMode(this);
this.write('\n');
} else {
this.output.write(
`Failed to load: ${file} is not a valid file\n`,
);
}
} catch (error) {
if (error instanceof ERR_MISSING_ARGS) {
this.output.write(`${error.message}\n`);
} else {
this.output.write(`Failed to load: ${file}\n`);
}
}
this.displayPrompt();
},
});
if (repl.terminal) {
repl.defineCommand('editor', {
help: 'Enter editor mode',
action() {
_turnOnEditorMode(this);
this.output.write(
'// Entering editor mode (Ctrl+D to finish, Ctrl+C to cancel)\n');
},
});
}
}
module.exports = {
start,
writer,
REPLServer,
REPL_MODE_SLOPPY,
REPL_MODE_STRICT,
Recoverable,
isValidSyntax,
};
ObjectDefineProperty(module.exports, 'builtinModules', {
__proto__: null,
get: pendingDeprecation ? deprecate(
() => getReplBuiltinLibs(),
'repl.builtinModules is deprecated. Check module.builtinModules instead',
'DEP0191',
) : () => getReplBuiltinLibs(),
set: pendingDeprecation ? deprecate(
(val) => setReplBuiltinLibs(val),
'repl.builtinModules is deprecated. Check module.builtinModules instead',
'DEP0191',
) : (val) => setReplBuiltinLibs(val),
enumerable: false,
configurable: true,
});
ObjectDefineProperty(module.exports, '_builtinLibs', {
__proto__: null,
get: pendingDeprecation ? deprecate(
() => getReplBuiltinLibs(),
'repl._builtinLibs is deprecated. Check module.builtinModules instead',
'DEP0142',
) : () => getReplBuiltinLibs(),
set: pendingDeprecation ? deprecate(
(val) => setReplBuiltinLibs(val),
'repl._builtinLibs is deprecated. Check module.builtinModules instead',
'DEP0142',
) : (val) => setReplBuiltinLibs(val),
enumerable: false,
configurable: true,
});