From 896debb4a282d91bb3598e339e5703b27a2f721f Mon Sep 17 00:00:00 2001 From: Abdelhadi Sabani Date: Sun, 31 May 2026 05:06:37 +0100 Subject: [PATCH 1/2] repl: avoid crash when typing incomplete import statement Typing `import` alone in the REPL triggers V8's "Cannot use import statement outside a module" SyntaxError, which the REPL was enriching with a dynamic-import suggestion via `toDynamicImport`. That helper called acorn to re-parse the line; on incomplete input acorn throws, the exception escaped the keypress handler, and the process exited with code 1. Catch the parse error inside `toDynamicImport` and fall back to a plain error message when the input cannot be parsed as a complete import statement. Fixes: https://github.com/nodejs/node/issues/63551 Signed-off-by: Abdelhadi Sabani --- lib/repl.js | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/lib/repl.js b/lib/repl.js index 60ea4457ab1bf6..bca046a17a85c7 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, From 115701927f8f154a224fd41ca6293f5cb7eaa80b Mon Sep 17 00:00:00 2001 From: Abdelhadi Sabani Date: Sun, 31 May 2026 05:06:37 +0100 Subject: [PATCH 2/2] test: switch repl import-no-crash to in-process REPL Replace the child_process.spawn-based regression test with the in-process startNewREPLServer pattern (similar to test-repl-null.js). This is faster, avoids subprocess flake, and lets the test assert on the REPL output stream directly instead of bytes piped through stdio. Verified to still catch the regression: the test exits with code 1 against unpatched v26.1.0 (acorn SyntaxError escapes) and exits 0 against the fix. Signed-off-by: Abdelhadi Sabani --- .../test-repl-import-statement-no-crash.js | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 test/parallel/test-repl-import-statement-no-crash.js 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/, +);