diff --git a/lib/repl.js b/lib/repl.js index 60ea4457ab1bf6..d1895f5d88fd37 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -247,10 +247,19 @@ fixReplRequire(module); const writer = (obj) => inspect(obj, writer.options); writer.options = { ...inspect.defaultOptions, showProxy: true }; -// Converts static import statement to dynamic import statement +// Converts static import statement to dynamic import statement. +// Returns an empty string if `codeLine` cannot be parsed as a module — this +// happens when the user typed something that triggers V8's "Cannot use import +// statement outside a module" but isn't a complete, parseable import statement +// (e.g. `import` alone in the REPL). const toDynamicImport = (codeLine) => { let dynamicImportStatement = ''; - const ast = acornParse(codeLine, { __proto__: null, sourceType: 'module', ecmaVersion: 'latest' }); + let ast; + try { + ast = acornParse(codeLine, { __proto__: null, sourceType: 'module', ecmaVersion: 'latest' }); + } catch { + return ''; + } acornWalk.ancestor(ast, { ImportDeclaration(node) { const awaitDynamicImport = `await import(${JSONStringify(node.source.value)});`; @@ -1036,8 +1045,12 @@ class REPLServer extends Interface { 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(this.lines, -1)); + const dynamicImport = + toDynamicImport(ArrayPrototypeAt(this.lines, -1)); + e.message = dynamicImport ? + 'Cannot use import statement inside the Node.js REPL, ' + + `alternatively use dynamic import: ${dynamicImport}` : + 'Cannot use import statement inside the Node.js REPL.'; e.stack = SideEffectFreeRegExpPrototypeSymbolReplace( /SyntaxError:.*\n/, e.stack, diff --git a/test/parallel/test-repl-import-statement-no-crash.js b/test/parallel/test-repl-import-statement-no-crash.js new file mode 100644 index 00000000000000..3c199bbbe6442a --- /dev/null +++ b/test/parallel/test-repl-import-statement-no-crash.js @@ -0,0 +1,31 @@ +'use strict'; +// Regression test for https://github.com/nodejs/node/issues/63551: +// Typing a bare `import` keyword in the REPL used to terminate the process. +// +// V8 throws SyntaxError("Cannot use import statement outside a module"), +// which the REPL enriched with a dynamic-import suggestion via +// `toDynamicImport`. That helper called acorn without a try/catch; on +// incomplete input acorn threw, and the exception escaped the REPL's input +// pipeline to crash the process. +// +// The fix wraps the acorn call in try/catch and falls back to a plain +// error message when the line cannot be parsed as a complete import +// statement. This test asserts that: +// 1. Emitting an incomplete `import` line through the REPL does not +// throw out of the line-handler, and +// 2. The REPL still surfaces a SyntaxError to the user. + +require('../common'); +const assert = require('assert'); +const { startNewREPLServer } = require('../common/repl'); + +const { replServer, output } = startNewREPLServer(); + +replServer.emit('line', 'import'); +replServer.emit('line', '.exit'); + +assert.match(output.accumulator, /SyntaxError/); +assert.match( + output.accumulator, + /Cannot use import statement inside the Node\.js REPL\b/, +);