diff --git a/lib/internal/readline/interface.js b/lib/internal/readline/interface.js index 4a5ec497369..df08875cc79 100644 --- a/lib/internal/readline/interface.js +++ b/lib/internal/readline/interface.js @@ -1324,21 +1324,19 @@ class Interface extends InterfaceConstructor { if (typeof s === 'string' && s) { // Erase state of previous searches. lineEnding.lastIndex = 0; - let nextMatch = RegExpPrototypeExec(lineEnding, s); - // If no line endings are found, just insert the string as is. - if (nextMatch === null) { - this[kInsertString](s); - } else { - // Keep track of the end of the last match. - let lastIndex = 0; - do { - this[kInsertString](StringPrototypeSlice(s, lastIndex, nextMatch.index)); - ({ lastIndex } = lineEnding); - this[kLine](); - // Restore lastIndex as the call to kLine could have mutated it. - lineEnding.lastIndex = lastIndex; - } while ((nextMatch = RegExpPrototypeExec(lineEnding, s)) !== null); + let nextMatch; + // Keep track of the end of the last match. + let lastIndex = 0; + while ((nextMatch = RegExpPrototypeExec(lineEnding, s)) !== null) { + this[kInsertString](StringPrototypeSlice(s, lastIndex, nextMatch.index)); + ({ lastIndex } = lineEnding); + this[kLine](); + // Restore lastIndex as the call to kLine could have mutated it. + lineEnding.lastIndex = lastIndex; } + // This ensures that the last line is written if it doesn't end in a newline. + // Note that the last line may be the first line, in which case this still works. + this[kInsertString](StringPrototypeSlice(s, lastIndex)); } } } diff --git a/test/fixtures/repl-load-multiline-no-trailing-newline.js b/test/fixtures/repl-load-multiline-no-trailing-newline.js new file mode 100644 index 00000000000..605d49e2d05 --- /dev/null +++ b/test/fixtures/repl-load-multiline-no-trailing-newline.js @@ -0,0 +1,7 @@ +// The lack of a newline at the end of this file is intentional. +const getLunch = () => + placeOrder('tacos') + .then(eat); + +const placeOrder = (order) => Promise.resolve(order); +const eat = (food) => ''; \ No newline at end of file diff --git a/test/parallel/test-readline-interface-no-trailing-newline.js b/test/parallel/test-readline-interface-no-trailing-newline.js new file mode 100644 index 00000000000..b3392db8619 --- /dev/null +++ b/test/parallel/test-readline-interface-no-trailing-newline.js @@ -0,0 +1,24 @@ +'use strict'; +const common = require('../common'); +const ArrayStream = require('../common/arraystream'); +const assert = require('assert'); + +common.skipIfDumbTerminal(); + +const readline = require('readline'); +const rli = new readline.Interface({ + terminal: true, + input: new ArrayStream(), + output: new ArrayStream(), +}); + +// Minimal reproduction for #47305 +const testInput = '{\n}'; + +let accum = ''; + +rli.output.write = (data) => accum += data.replace('\r', ''); + +rli.write(testInput); + +assert.strictEqual(accum, testInput); diff --git a/test/parallel/test-repl-load-multiline-no-trailing-newline.js b/test/parallel/test-repl-load-multiline-no-trailing-newline.js new file mode 100644 index 00000000000..f57638d2521 --- /dev/null +++ b/test/parallel/test-repl-load-multiline-no-trailing-newline.js @@ -0,0 +1,42 @@ +'use strict'; +const common = require('../common'); +const ArrayStream = require('../common/arraystream'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const repl = require('repl'); + +common.skipIfDumbTerminal(); + +const command = `.load ${fixtures.path('repl-load-multiline-no-trailing-newline.js')}`; +const terminalCode = '\u001b[1G\u001b[0J \u001b[1G'; +const terminalCodeRegex = new RegExp(terminalCode.replace(/\[/g, '\\['), 'g'); + +const expected = `${command} +// The lack of a newline at the end of this file is intentional. +const getLunch = () => + placeOrder('tacos') + .then(eat); + +const placeOrder = (order) => Promise.resolve(order); +const eat = (food) => ''; +undefined +`; + +let accum = ''; + +const inputStream = new ArrayStream(); +const outputStream = new ArrayStream(); + +outputStream.write = (data) => accum += data.replace('\r', ''); + +const r = repl.start({ + prompt: '', + input: inputStream, + output: outputStream, + terminal: true, + useColors: false +}); + +r.write(`${command}\n`); +assert.strictEqual(accum.replace(terminalCodeRegex, ''), expected); +r.close();