From d639e32f6513a1afa77e8b8cd9a622a600b6e029 Mon Sep 17 00:00:00 2001 From: Abdelhadi Sabani Date: Mon, 25 May 2026 22:39:53 +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 --- lib/repl.js | 21 +++++++++--- .../test-repl-import-statement-no-crash.js | 33 +++++++++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 test/parallel/test-repl-import-statement-no-crash.js 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..dd6cb3409e27a1 --- /dev/null +++ b/test/parallel/test-repl-import-statement-no-crash.js @@ -0,0 +1,33 @@ +'use strict'; +// Regression test for the REPL crashing when an incomplete `import` statement +// is entered. Typing `import` alone used to trigger V8's "Cannot use import +// statement outside a module" SyntaxError, which the REPL tried to enrich with +// a dynamic-import suggestion. Building that suggestion called acorn to parse +// the line; acorn threw on the incomplete input, and the exception escaped to +// terminate the process. +// Refs: https://github.com/nodejs/node/issues/63551 + +require('../common'); +const assert = require('assert'); +const child_process = require('child_process'); + +const child = child_process.spawn(process.execPath, ['--interactive'], { + stdio: 'pipe', +}); + +let stdout = ''; +child.stdout.setEncoding('utf8'); +child.stdout.on('data', (chunk) => { stdout += chunk; }); + +child.stdin.write('import\n'); +child.stdin.write('.exit\n'); + +child.on('exit', (code, signal) => { + assert.strictEqual(signal, null); + assert.strictEqual(code, 0, + `REPL exited with code ${code} (expected 0). ` + + `stdout was:\n${stdout}`); + // The REPL should still have printed a SyntaxError message describing the + // issue with the input, rather than crashing silently. + assert.match(stdout, /SyntaxError/); +}); From ec2dec9194b05fb6d93455b4f3103686830eb217 Mon Sep 17 00:00:00 2001 From: Abdelhadi Sabani Date: Mon, 25 May 2026 23:02:18 +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. --- .../test-repl-import-statement-no-crash.js | 50 +++++++++---------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/test/parallel/test-repl-import-statement-no-crash.js b/test/parallel/test-repl-import-statement-no-crash.js index dd6cb3409e27a1..3c199bbbe6442a 100644 --- a/test/parallel/test-repl-import-statement-no-crash.js +++ b/test/parallel/test-repl-import-statement-no-crash.js @@ -1,33 +1,31 @@ 'use strict'; -// Regression test for the REPL crashing when an incomplete `import` statement -// is entered. Typing `import` alone used to trigger V8's "Cannot use import -// statement outside a module" SyntaxError, which the REPL tried to enrich with -// a dynamic-import suggestion. Building that suggestion called acorn to parse -// the line; acorn threw on the incomplete input, and the exception escaped to -// terminate the process. -// Refs: https://github.com/nodejs/node/issues/63551 +// 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 child_process = require('child_process'); +const { startNewREPLServer } = require('../common/repl'); -const child = child_process.spawn(process.execPath, ['--interactive'], { - stdio: 'pipe', -}); +const { replServer, output } = startNewREPLServer(); -let stdout = ''; -child.stdout.setEncoding('utf8'); -child.stdout.on('data', (chunk) => { stdout += chunk; }); +replServer.emit('line', 'import'); +replServer.emit('line', '.exit'); -child.stdin.write('import\n'); -child.stdin.write('.exit\n'); - -child.on('exit', (code, signal) => { - assert.strictEqual(signal, null); - assert.strictEqual(code, 0, - `REPL exited with code ${code} (expected 0). ` + - `stdout was:\n${stdout}`); - // The REPL should still have printed a SyntaxError message describing the - // issue with the input, rather than crashing silently. - assert.match(stdout, /SyntaxError/); -}); +assert.match(output.accumulator, /SyntaxError/); +assert.match( + output.accumulator, + /Cannot use import statement inside the Node\.js REPL\b/, +);