diff --git a/doc/api/repl.md b/doc/api/repl.md index 075fa91581c428..17cff2f3d839a9 100644 --- a/doc/api/repl.md +++ b/doc/api/repl.md @@ -603,7 +603,9 @@ changes: * `eval` {Function} The function to be used when evaluating each given line of input. **Default:** an async wrapper for the JavaScript `eval()` function. An `eval` function can error with `repl.Recoverable` to indicate - the input was incomplete and prompt for additional lines. + the input was incomplete and prompt for additional lines. If a custom + `eval` function is provided, `callback` must be invoked to allow processing + next command. * `useColors` {boolean} If `true`, specifies that the default `writer` function should include ANSI color styling to REPL output. If a custom `writer` function is provided then this has no effect. **Default:** checking diff --git a/lib/repl.js b/lib/repl.js index c73ad5d9d61ade..8e97e18d914cb6 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -634,6 +634,7 @@ function REPLServer(prompt, self._domain.on('error', function debugDomainError(e) { debug('domain error'); let errStack = ''; + self.processing = false; if (typeof e === 'object' && e !== null) { overrideStackTrace.set(e, (error, stackFrames) => { @@ -807,6 +808,8 @@ function REPLServer(prompt, let sawCtrlD = false; const prioritizedSigintQueue = new SafeSet(); self.on('SIGINT', function onSigInt() { + self.processing = false; + if (prioritizedSigintQueue.size > 0) { for (const task of prioritizedSigintQueue) { task(); @@ -838,7 +841,18 @@ function REPLServer(prompt, self.displayPrompt(); }); - self.on('line', function onLine(cmd) { + self.processing = false; + self.queuedLines = []; + self.skipQueue = false; + + function _onLine(cmd) { + if (self.processing && !self.skipQueue) { + debug('queued line %j', cmd); + ArrayPrototypePush(self.queuedLines, cmd); + return; + } + self.skipQueue = false; + self.processing = true; debug('line %j', cmd); cmd = cmd || ''; sawSIGINT = false; @@ -856,6 +870,7 @@ function REPLServer(prompt, self.cursor = prefix.length; } ReflectApply(_memory, self, [cmd]); + self.processing = false; return; } @@ -872,6 +887,7 @@ function REPLServer(prompt, const keyword = matches && matches[1]; const rest = matches && matches[2]; if (ReflectApply(_parseREPLKeyword, self, [keyword, rest]) === true) { + self.processing = false; return; } if (!self[kBufferedCommandSymbol]) { @@ -888,56 +904,70 @@ function REPLServer(prompt, self.eval(evalCmd, self.context, getREPLResourceName(), finish); function finish(e, ret) { - debug('finish', e, ret); - ReflectApply(_memory, self, [cmd]); + try { + (() => { + debug('finish', e, ret); + ReflectApply(_memory, self, [cmd]); + + if (e && !self[kBufferedCommandSymbol] && + StringPrototypeStartsWith(StringPrototypeTrim(cmd), 'npm ')) { + 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 (e && !self[kBufferedCommandSymbol] && - StringPrototypeStartsWith(StringPrototypeTrim(cmd), 'npm ')) { - 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); + } - // 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'); + } - // 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; + // Display prompt again (unless we already did by emitting the 'error' + // event on the domain instance). + if (!e) { + self.displayPrompt(); + } + })(); + } finally { + if (self.queuedLines.length) { + self.skipQueue = true; + _onLine(ArrayPrototypeShift(self.queuedLines)); + } else { + self.processing = false; } - 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('line', _onLine); self.on('SIGCONT', function onSigCont() { if (self.editorMode) { @@ -1731,6 +1761,7 @@ function defineDefaultCommands(repl) { if (stats && stats.isFile()) { _turnOnEditorMode(this); const data = fs.readFileSync(file, 'utf8'); + repl.skipQueue = true; this.write(data); _turnOffEditorMode(this); this.write('\n'); diff --git a/test/parallel/test-repl-autolibs.js b/test/parallel/test-repl-autolibs.js index 5cf3b1497221d0..e6ae4f251a4d8c 100644 --- a/test/parallel/test-repl-autolibs.js +++ b/test/parallel/test-repl-autolibs.js @@ -42,7 +42,7 @@ function test1() { `${util.inspect(require('fs'), null, 2, false)}\n`); // Globally added lib matches required lib assert.strictEqual(global.fs, require('fs')); - test2(); + process.nextTick(test2); } }; assert(!gotWrite); diff --git a/test/parallel/test-repl-empty.js b/test/parallel/test-repl-empty.js index 44281f117f0bba..75f42ff36a5c05 100644 --- a/test/parallel/test-repl-empty.js +++ b/test/parallel/test-repl-empty.js @@ -7,10 +7,11 @@ const repl = require('repl'); let evalCalledWithExpectedArgs = false; const options = { - eval: common.mustCall((cmd, context) => { + eval: common.mustCall((cmd, _context, _file, cb) => { // Assertions here will not cause the test to exit with an error code // so set a boolean that is checked later instead. evalCalledWithExpectedArgs = (cmd === '\n'); + cb(); }) }; diff --git a/test/parallel/test-repl-eval.js b/test/parallel/test-repl-eval.js index d775423fb74a52..b120286c65edf8 100644 --- a/test/parallel/test-repl-eval.js +++ b/test/parallel/test-repl-eval.js @@ -7,11 +7,12 @@ const repl = require('repl'); let evalCalledWithExpectedArgs = false; const options = { - eval: common.mustCall((cmd, context) => { + eval: common.mustCall((cmd, context, _file, cb) => { // Assertions here will not cause the test to exit with an error code // so set a boolean that is checked later instead. evalCalledWithExpectedArgs = (cmd === 'function f() {}\n' && context.foo === 'bar'); + cb(); }) }; diff --git a/test/parallel/test-repl-line-queue.js b/test/parallel/test-repl-line-queue.js new file mode 100644 index 00000000000000..e5ad4d81800500 --- /dev/null +++ b/test/parallel/test-repl-line-queue.js @@ -0,0 +1,26 @@ +'use strict'; +require('../common'); +const ArrayStream = require('../common/arraystream'); + +const assert = require('assert'); +const repl = require('repl'); + +// Flags: --expose-internals --experimental-repl-await + +const putIn = new ArrayStream(); +repl.start({ + input: putIn, + output: putIn, + useGlobal: false +}); + +let expectedIndex = -1; +const expected = ['undefined', '> ', '1', '> ']; + +putIn.write = function(data) { + assert.strict(data, expected[expectedIndex += 1]); +}; + +putIn.run([ + 'const x = await new Promise((r) => setTimeout(() => r(1), 500));\nx;', +]);