Skip to content

Commit 84ca0b2

Browse files
committed
repl: display dynamic import version in error message.
Improve REPL import error reporting to include dynamic import statement. ``` > import assert from 'node:assert' import assert from 'node:assert' ^^^^^^ Uncaught: SyntaxError: Cannot use import statement inside the Node.js REPL, alternatively use dynamic import: const assert = await import("node:assert"); ```
1 parent 85ac915 commit 84ca0b2

File tree

2 files changed

+65
-43
lines changed

2 files changed

+65
-43
lines changed

lib/repl.js

Lines changed: 64 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,11 @@ const {
104104
const {
105105
isIdentifierStart,
106106
isIdentifierChar,
107+
Parser: acornParser,
107108
} = require('internal/deps/acorn/acorn/dist/acorn');
109+
110+
const acornWalk = require('internal/deps/acorn/acorn-walk/dist/walk');
111+
108112
const {
109113
decorateErrorStack,
110114
isError,
@@ -223,6 +227,24 @@ module.paths = CJSModule._nodeModulePaths(module.filename);
223227
const writer = (obj) => inspect(obj, writer.options);
224228
writer.options = { ...inspect.defaultOptions, showProxy: true };
225229

230+
// Converts static import statement to dynamic import statement
231+
const toDynamicImport = (codeLine) => {
232+
let dynamicImportStatement = '';
233+
const ast = acornParser.parse(codeLine, { sourceType: 'module', ecmaVersion: 'latest' });
234+
acornWalk.ancestor(ast, {
235+
ImportDeclaration(node) {
236+
const importedModules = node.source.value;
237+
let importedSpecifiers = ArrayPrototypeMap(node.specifiers, (specifier) => specifier.local.name);
238+
if (importedSpecifiers.length > 1) {
239+
importedSpecifiers = `{${ArrayPrototypeJoin(importedSpecifiers, ',')}}`;
240+
}
241+
dynamicImportStatement = `const ${importedSpecifiers} = await import('${importedModules}');`;
242+
},
243+
});
244+
return dynamicImportStatement;
245+
};
246+
247+
226248
function REPLServer(prompt,
227249
stream,
228250
eval_,
@@ -283,13 +305,13 @@ function REPLServer(prompt,
283305
get: pendingDeprecation ?
284306
deprecate(() => this.input,
285307
'repl.inputStream and repl.outputStream are deprecated. ' +
286-
'Use repl.input and repl.output instead',
308+
'Use repl.input and repl.output instead',
287309
'DEP0141') :
288310
() => this.input,
289311
set: pendingDeprecation ?
290312
deprecate((val) => this.input = val,
291313
'repl.inputStream and repl.outputStream are deprecated. ' +
292-
'Use repl.input and repl.output instead',
314+
'Use repl.input and repl.output instead',
293315
'DEP0141') :
294316
(val) => this.input = val,
295317
enumerable: false,
@@ -300,13 +322,13 @@ function REPLServer(prompt,
300322
get: pendingDeprecation ?
301323
deprecate(() => this.output,
302324
'repl.inputStream and repl.outputStream are deprecated. ' +
303-
'Use repl.input and repl.output instead',
325+
'Use repl.input and repl.output instead',
304326
'DEP0141') :
305327
() => this.output,
306328
set: pendingDeprecation ?
307329
deprecate((val) => this.output = val,
308330
'repl.inputStream and repl.outputStream are deprecated. ' +
309-
'Use repl.input and repl.output instead',
331+
'Use repl.input and repl.output instead',
310332
'DEP0141') :
311333
(val) => this.output = val,
312334
enumerable: false,
@@ -344,9 +366,9 @@ function REPLServer(prompt,
344366
// instance and that could trigger the `MaxListenersExceededWarning`.
345367
process.prependListener('newListener', (event, listener) => {
346368
if (event === 'uncaughtException' &&
347-
process.domain &&
348-
listener.name !== 'domainUncaughtExceptionClear' &&
349-
domainSet.has(process.domain)) {
369+
process.domain &&
370+
listener.name !== 'domainUncaughtExceptionClear' &&
371+
domainSet.has(process.domain)) {
350372
// Throw an error so that the event will not be added and the current
351373
// domain takes over. That way the user is notified about the error
352374
// and the current code evaluation is stopped, just as any other code
@@ -363,8 +385,8 @@ function REPLServer(prompt,
363385
const savedRegExMatches = ['', '', '', '', '', '', '', '', '', ''];
364386
const sep = '\u0000\u0000\u0000';
365387
const regExMatcher = new RegExp(`^${sep}(.*)${sep}(.*)${sep}(.*)${sep}(.*)` +
366-
`${sep}(.*)${sep}(.*)${sep}(.*)${sep}(.*)` +
367-
`${sep}(.*)$`);
388+
`${sep}(.*)${sep}(.*)${sep}(.*)${sep}(.*)` +
389+
`${sep}(.*)$`);
368390

369391
eval_ = eval_ || defaultEval;
370392

@@ -417,7 +439,7 @@ function REPLServer(prompt,
417439
// an expression. Note that if the above condition changes,
418440
// lib/internal/repl/utils.js needs to be changed to match.
419441
if (RegExpPrototypeExec(/^\s*{/, code) !== null &&
420-
RegExpPrototypeExec(/;\s*$/, code) === null) {
442+
RegExpPrototypeExec(/;\s*$/, code) === null) {
421443
code = `(${StringPrototypeTrim(code)})\n`;
422444
wrappedCmd = true;
423445
}
@@ -492,7 +514,7 @@ function REPLServer(prompt,
492514
while (true) {
493515
try {
494516
if (self.replMode === module.exports.REPL_MODE_STRICT &&
495-
RegExpPrototypeExec(/^\s*$/, code) === null) {
517+
RegExpPrototypeExec(/^\s*$/, code) === null) {
496518
// "void 0" keeps the repl from returning "use strict" as the result
497519
// value for statements and declarations that don't return a value.
498520
code = `'use strict'; void 0;\n${code}`;
@@ -684,7 +706,7 @@ function REPLServer(prompt,
684706
'module';
685707
if (StringPrototypeIncludes(e.message, importErrorStr)) {
686708
e.message = 'Cannot use import statement inside the Node.js ' +
687-
'REPL, alternatively use dynamic import';
709+
'REPL, alternatively use dynamic import: ' + toDynamicImport(self.lines.at(-1));
688710
e.stack = SideEffectFreeRegExpPrototypeSymbolReplace(
689711
/SyntaxError:.*\n/,
690712
e.stack,
@@ -712,7 +734,7 @@ function REPLServer(prompt,
712734
}
713735

714736
if (options[kStandaloneREPL] &&
715-
process.listenerCount('uncaughtException') !== 0) {
737+
process.listenerCount('uncaughtException') !== 0) {
716738
process.nextTick(() => {
717739
process.emit('uncaughtException', e);
718740
self.clearBufferedCommand();
@@ -729,7 +751,7 @@ function REPLServer(prompt,
729751
errStack = '';
730752
ArrayPrototypeForEach(lines, (line) => {
731753
if (!matched &&
732-
RegExpPrototypeExec(/^\[?([A-Z][a-z0-9_]*)*Error/, line) !== null) {
754+
RegExpPrototypeExec(/^\[?([A-Z][a-z0-9_]*)*Error/, line) !== null) {
733755
errStack += writer.options.breakLength >= line.length ?
734756
`Uncaught ${line}` :
735757
`Uncaught:\n${line}`;
@@ -875,8 +897,8 @@ function REPLServer(prompt,
875897
// display next prompt and return.
876898
if (trimmedCmd) {
877899
if (StringPrototypeCharAt(trimmedCmd, 0) === '.' &&
878-
StringPrototypeCharAt(trimmedCmd, 1) !== '.' &&
879-
NumberIsNaN(NumberParseFloat(trimmedCmd))) {
900+
StringPrototypeCharAt(trimmedCmd, 1) !== '.' &&
901+
NumberIsNaN(NumberParseFloat(trimmedCmd))) {
880902
const matches = RegExpPrototypeExec(/^\.([^\s]+)\s*(.*)$/, trimmedCmd);
881903
const keyword = matches && matches[1];
882904
const rest = matches && matches[2];
@@ -901,10 +923,10 @@ function REPLServer(prompt,
901923
ReflectApply(_memory, self, [cmd]);
902924

903925
if (e && !self[kBufferedCommandSymbol] &&
904-
StringPrototypeStartsWith(StringPrototypeTrim(cmd), 'npm ')) {
926+
StringPrototypeStartsWith(StringPrototypeTrim(cmd), 'npm ')) {
905927
self.output.write('npm should be run outside of the ' +
906-
'Node.js REPL, in your normal shell.\n' +
907-
'(Press Ctrl+D to exit.)\n');
928+
'Node.js REPL, in your normal shell.\n' +
929+
'(Press Ctrl+D to exit.)\n');
908930
self.displayPrompt();
909931
return;
910932
}
@@ -929,11 +951,11 @@ function REPLServer(prompt,
929951

930952
// If we got any output - print it (if no error)
931953
if (!e &&
932-
// When an invalid REPL command is used, error message is printed
933-
// immediately. We don't have to print anything else. So, only when
934-
// the second argument to this function is there, print it.
935-
arguments.length === 2 &&
936-
(!self.ignoreUndefined || ret !== undefined)) {
954+
// When an invalid REPL command is used, error message is printed
955+
// immediately. We don't have to print anything else. So, only when
956+
// the second argument to this function is there, print it.
957+
arguments.length === 2 &&
958+
(!self.ignoreUndefined || ret !== undefined)) {
937959
if (!self.underscoreAssigned) {
938960
self.last = ret;
939961
}
@@ -984,7 +1006,7 @@ function REPLServer(prompt,
9841006
if (!self.editorMode || !self.terminal) {
9851007
// Before exiting, make sure to clear the line.
9861008
if (key.ctrl && key.name === 'd' &&
987-
self.cursor === 0 && self.line.length === 0) {
1009+
self.cursor === 0 && self.line.length === 0) {
9881010
self.clearLine();
9891011
}
9901012
clearPreview(key);
@@ -1181,7 +1203,7 @@ const importRE = /\bimport\s*\(\s*['"`](([\w@./:-]+\/)?(?:[\w@./:-]*))(?![^'"`])
11811203
const requireRE = /\brequire\s*\(\s*['"`](([\w@./:-]+\/)?(?:[\w@./:-]*))(?![^'"`])$/;
11821204
const fsAutoCompleteRE = /fs(?:\.promises)?\.\s*[a-z][a-zA-Z]+\(\s*["'](.*)/;
11831205
const simpleExpressionRE =
1184-
/(?:[\w$'"`[{(](?:\w|\$|['"`\]})])*\??\.)*[a-zA-Z_$](?:\w|\$)*\??\.?$/;
1206+
/(?:[\w$'"`[{(](?:\w|\$|['"`\]})])*\??\.)*[a-zA-Z_$](?:\w|\$)*\??\.?$/;
11851207
const versionedFileNamesRe = /-\d+\.\d+/;
11861208

11871209
function isIdentifier(str) {
@@ -1337,15 +1359,15 @@ function complete(line, callback) {
13371359
const dirents = gracefulReaddir(dir, { withFileTypes: true }) || [];
13381360
ArrayPrototypeForEach(dirents, (dirent) => {
13391361
if (RegExpPrototypeExec(versionedFileNamesRe, dirent.name) !== null ||
1340-
dirent.name === '.npm') {
1362+
dirent.name === '.npm') {
13411363
// Exclude versioned names that 'npm' installs.
13421364
return;
13431365
}
13441366
const extension = path.extname(dirent.name);
13451367
const base = StringPrototypeSlice(dirent.name, 0, -extension.length);
13461368
if (!dirent.isDirectory()) {
13471369
if (StringPrototypeIncludes(extensions, extension) &&
1348-
(!subdir || base !== 'index')) {
1370+
(!subdir || base !== 'index')) {
13491371
ArrayPrototypePush(group, `${subdir}${base}`);
13501372
}
13511373
return;
@@ -1398,7 +1420,7 @@ function complete(line, callback) {
13981420
ArrayPrototypeForEach(dirents, (dirent) => {
13991421
const { name } = dirent;
14001422
if (RegExpPrototypeExec(versionedFileNamesRe, name) !== null ||
1401-
name === '.npm') {
1423+
name === '.npm') {
14021424
// Exclude versioned names that 'npm' installs.
14031425
return;
14041426
}
@@ -1431,20 +1453,20 @@ function complete(line, callback) {
14311453

14321454
ArrayPrototypePush(completionGroups, _builtinLibs, nodeSchemeBuiltinLibs);
14331455
} else if ((match = RegExpPrototypeExec(fsAutoCompleteRE, line)) !== null &&
1434-
this.allowBlockingCompletions) {
1456+
this.allowBlockingCompletions) {
14351457
({ 0: completionGroups, 1: completeOn } = completeFSFunctions(match));
1436-
// Handle variable member lookup.
1437-
// We support simple chained expressions like the following (no function
1438-
// calls, etc.). That is for simplicity and also because we *eval* that
1439-
// leading expression so for safety (see WARNING above) don't want to
1440-
// eval function calls.
1441-
//
1442-
// foo.bar<|> # completions for 'foo' with filter 'bar'
1443-
// spam.eggs.<|> # completions for 'spam.eggs' with filter ''
1444-
// foo<|> # all scope vars with filter 'foo'
1445-
// foo.<|> # completions for 'foo' with filter ''
1458+
// Handle variable member lookup.
1459+
// We support simple chained expressions like the following (no function
1460+
// calls, etc.). That is for simplicity and also because we *eval* that
1461+
// leading expression so for safety (see WARNING above) don't want to
1462+
// eval function calls.
1463+
//
1464+
// foo.bar<|> # completions for 'foo' with filter 'bar'
1465+
// spam.eggs.<|> # completions for 'spam.eggs' with filter ''
1466+
// foo<|> # all scope vars with filter 'foo'
1467+
// foo.<|> # completions for 'foo' with filter ''
14461468
} else if (line.length === 0 ||
1447-
RegExpPrototypeExec(/\w|\.|\$/, line[line.length - 1]) !== null) {
1469+
RegExpPrototypeExec(/\w|\.|\$/, line[line.length - 1]) !== null) {
14481470
const { 0: match } = RegExpPrototypeExec(simpleExpressionRE, line) || [''];
14491471
if (line.length !== 0 && !match) {
14501472
completionGroupsLoaded();
@@ -1495,7 +1517,7 @@ function complete(line, callback) {
14951517
try {
14961518
let p;
14971519
if ((typeof obj === 'object' && obj !== null) ||
1498-
typeof obj === 'function') {
1520+
typeof obj === 'function') {
14991521
memberGroups.push(filteredOwnPropertyNames(obj));
15001522
p = ObjectGetPrototypeOf(obj);
15011523
} else {

test/parallel/test-repl.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -818,7 +818,7 @@ const tcpTests = [
818818
kArrow,
819819
'',
820820
'Uncaught:',
821-
/^SyntaxError: .* dynamic import/,
821+
/^SyntaxError: .* dynamic import: {2}const comeOn = await import('fhqwhgads');/,
822822
]
823823
},
824824
];

0 commit comments

Comments
 (0)