Skip to content

Commit cd2c691

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 cd2c691

File tree

2 files changed

+66
-43
lines changed

2 files changed

+66
-43
lines changed

lib/repl.js

Lines changed: 65 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,25 @@ 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(codeLine, { sourceType: 'module' });
234+
235+
acornWalk.ancestor(ast, {
236+
ImportDeclaration(node, ancestors) {
237+
const importedModules = node.source.value;
238+
let importedSpecifiers = node.specifiers.map((specifier) => specifier.local.name);
239+
if (importedSpecifiers.length > 1) {
240+
importedSpecifiers = `{${importedSpecifiers.join(',')}}`;
241+
}
242+
dynamicImportStatement += `const ${importedSpecifiers.length || importedModules} = await import('${importedModules}');`;
243+
},
244+
});
245+
return dynamicImportStatement;
246+
};
247+
248+
226249
function REPLServer(prompt,
227250
stream,
228251
eval_,
@@ -283,13 +306,13 @@ function REPLServer(prompt,
283306
get: pendingDeprecation ?
284307
deprecate(() => this.input,
285308
'repl.inputStream and repl.outputStream are deprecated. ' +
286-
'Use repl.input and repl.output instead',
309+
'Use repl.input and repl.output instead',
287310
'DEP0141') :
288311
() => this.input,
289312
set: pendingDeprecation ?
290313
deprecate((val) => this.input = val,
291314
'repl.inputStream and repl.outputStream are deprecated. ' +
292-
'Use repl.input and repl.output instead',
315+
'Use repl.input and repl.output instead',
293316
'DEP0141') :
294317
(val) => this.input = val,
295318
enumerable: false,
@@ -300,13 +323,13 @@ function REPLServer(prompt,
300323
get: pendingDeprecation ?
301324
deprecate(() => this.output,
302325
'repl.inputStream and repl.outputStream are deprecated. ' +
303-
'Use repl.input and repl.output instead',
326+
'Use repl.input and repl.output instead',
304327
'DEP0141') :
305328
() => this.output,
306329
set: pendingDeprecation ?
307330
deprecate((val) => this.output = val,
308331
'repl.inputStream and repl.outputStream are deprecated. ' +
309-
'Use repl.input and repl.output instead',
332+
'Use repl.input and repl.output instead',
310333
'DEP0141') :
311334
(val) => this.output = val,
312335
enumerable: false,
@@ -344,9 +367,9 @@ function REPLServer(prompt,
344367
// instance and that could trigger the `MaxListenersExceededWarning`.
345368
process.prependListener('newListener', (event, listener) => {
346369
if (event === 'uncaughtException' &&
347-
process.domain &&
348-
listener.name !== 'domainUncaughtExceptionClear' &&
349-
domainSet.has(process.domain)) {
370+
process.domain &&
371+
listener.name !== 'domainUncaughtExceptionClear' &&
372+
domainSet.has(process.domain)) {
350373
// Throw an error so that the event will not be added and the current
351374
// domain takes over. That way the user is notified about the error
352375
// and the current code evaluation is stopped, just as any other code
@@ -363,8 +386,8 @@ function REPLServer(prompt,
363386
const savedRegExMatches = ['', '', '', '', '', '', '', '', '', ''];
364387
const sep = '\u0000\u0000\u0000';
365388
const regExMatcher = new RegExp(`^${sep}(.*)${sep}(.*)${sep}(.*)${sep}(.*)` +
366-
`${sep}(.*)${sep}(.*)${sep}(.*)${sep}(.*)` +
367-
`${sep}(.*)$`);
389+
`${sep}(.*)${sep}(.*)${sep}(.*)${sep}(.*)` +
390+
`${sep}(.*)$`);
368391

369392
eval_ = eval_ || defaultEval;
370393

@@ -417,7 +440,7 @@ function REPLServer(prompt,
417440
// an expression. Note that if the above condition changes,
418441
// lib/internal/repl/utils.js needs to be changed to match.
419442
if (RegExpPrototypeExec(/^\s*{/, code) !== null &&
420-
RegExpPrototypeExec(/;\s*$/, code) === null) {
443+
RegExpPrototypeExec(/;\s*$/, code) === null) {
421444
code = `(${StringPrototypeTrim(code)})\n`;
422445
wrappedCmd = true;
423446
}
@@ -492,7 +515,7 @@ function REPLServer(prompt,
492515
while (true) {
493516
try {
494517
if (self.replMode === module.exports.REPL_MODE_STRICT &&
495-
RegExpPrototypeExec(/^\s*$/, code) === null) {
518+
RegExpPrototypeExec(/^\s*$/, code) === null) {
496519
// "void 0" keeps the repl from returning "use strict" as the result
497520
// value for statements and declarations that don't return a value.
498521
code = `'use strict'; void 0;\n${code}`;
@@ -684,7 +707,7 @@ function REPLServer(prompt,
684707
'module';
685708
if (StringPrototypeIncludes(e.message, importErrorStr)) {
686709
e.message = 'Cannot use import statement inside the Node.js ' +
687-
'REPL, alternatively use dynamic import';
710+
'REPL, alternatively use dynamic import: ' + toDynamicImport(self.lines.at(-1));
688711
e.stack = SideEffectFreeRegExpPrototypeSymbolReplace(
689712
/SyntaxError:.*\n/,
690713
e.stack,
@@ -712,7 +735,7 @@ function REPLServer(prompt,
712735
}
713736

714737
if (options[kStandaloneREPL] &&
715-
process.listenerCount('uncaughtException') !== 0) {
738+
process.listenerCount('uncaughtException') !== 0) {
716739
process.nextTick(() => {
717740
process.emit('uncaughtException', e);
718741
self.clearBufferedCommand();
@@ -729,7 +752,7 @@ function REPLServer(prompt,
729752
errStack = '';
730753
ArrayPrototypeForEach(lines, (line) => {
731754
if (!matched &&
732-
RegExpPrototypeExec(/^\[?([A-Z][a-z0-9_]*)*Error/, line) !== null) {
755+
RegExpPrototypeExec(/^\[?([A-Z][a-z0-9_]*)*Error/, line) !== null) {
733756
errStack += writer.options.breakLength >= line.length ?
734757
`Uncaught ${line}` :
735758
`Uncaught:\n${line}`;
@@ -875,8 +898,8 @@ function REPLServer(prompt,
875898
// display next prompt and return.
876899
if (trimmedCmd) {
877900
if (StringPrototypeCharAt(trimmedCmd, 0) === '.' &&
878-
StringPrototypeCharAt(trimmedCmd, 1) !== '.' &&
879-
NumberIsNaN(NumberParseFloat(trimmedCmd))) {
901+
StringPrototypeCharAt(trimmedCmd, 1) !== '.' &&
902+
NumberIsNaN(NumberParseFloat(trimmedCmd))) {
880903
const matches = RegExpPrototypeExec(/^\.([^\s]+)\s*(.*)$/, trimmedCmd);
881904
const keyword = matches && matches[1];
882905
const rest = matches && matches[2];
@@ -901,10 +924,10 @@ function REPLServer(prompt,
901924
ReflectApply(_memory, self, [cmd]);
902925

903926
if (e && !self[kBufferedCommandSymbol] &&
904-
StringPrototypeStartsWith(StringPrototypeTrim(cmd), 'npm ')) {
927+
StringPrototypeStartsWith(StringPrototypeTrim(cmd), 'npm ')) {
905928
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');
929+
'Node.js REPL, in your normal shell.\n' +
930+
'(Press Ctrl+D to exit.)\n');
908931
self.displayPrompt();
909932
return;
910933
}
@@ -929,11 +952,11 @@ function REPLServer(prompt,
929952

930953
// If we got any output - print it (if no error)
931954
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)) {
955+
// When an invalid REPL command is used, error message is printed
956+
// immediately. We don't have to print anything else. So, only when
957+
// the second argument to this function is there, print it.
958+
arguments.length === 2 &&
959+
(!self.ignoreUndefined || ret !== undefined)) {
937960
if (!self.underscoreAssigned) {
938961
self.last = ret;
939962
}
@@ -984,7 +1007,7 @@ function REPLServer(prompt,
9841007
if (!self.editorMode || !self.terminal) {
9851008
// Before exiting, make sure to clear the line.
9861009
if (key.ctrl && key.name === 'd' &&
987-
self.cursor === 0 && self.line.length === 0) {
1010+
self.cursor === 0 && self.line.length === 0) {
9881011
self.clearLine();
9891012
}
9901013
clearPreview(key);
@@ -1181,7 +1204,7 @@ const importRE = /\bimport\s*\(\s*['"`](([\w@./:-]+\/)?(?:[\w@./:-]*))(?![^'"`])
11811204
const requireRE = /\brequire\s*\(\s*['"`](([\w@./:-]+\/)?(?:[\w@./:-]*))(?![^'"`])$/;
11821205
const fsAutoCompleteRE = /fs(?:\.promises)?\.\s*[a-z][a-zA-Z]+\(\s*["'](.*)/;
11831206
const simpleExpressionRE =
1184-
/(?:[\w$'"`[{(](?:\w|\$|['"`\]})])*\??\.)*[a-zA-Z_$](?:\w|\$)*\??\.?$/;
1207+
/(?:[\w$'"`[{(](?:\w|\$|['"`\]})])*\??\.)*[a-zA-Z_$](?:\w|\$)*\??\.?$/;
11851208
const versionedFileNamesRe = /-\d+\.\d+/;
11861209

11871210
function isIdentifier(str) {
@@ -1337,15 +1360,15 @@ function complete(line, callback) {
13371360
const dirents = gracefulReaddir(dir, { withFileTypes: true }) || [];
13381361
ArrayPrototypeForEach(dirents, (dirent) => {
13391362
if (RegExpPrototypeExec(versionedFileNamesRe, dirent.name) !== null ||
1340-
dirent.name === '.npm') {
1363+
dirent.name === '.npm') {
13411364
// Exclude versioned names that 'npm' installs.
13421365
return;
13431366
}
13441367
const extension = path.extname(dirent.name);
13451368
const base = StringPrototypeSlice(dirent.name, 0, -extension.length);
13461369
if (!dirent.isDirectory()) {
13471370
if (StringPrototypeIncludes(extensions, extension) &&
1348-
(!subdir || base !== 'index')) {
1371+
(!subdir || base !== 'index')) {
13491372
ArrayPrototypePush(group, `${subdir}${base}`);
13501373
}
13511374
return;
@@ -1398,7 +1421,7 @@ function complete(line, callback) {
13981421
ArrayPrototypeForEach(dirents, (dirent) => {
13991422
const { name } = dirent;
14001423
if (RegExpPrototypeExec(versionedFileNamesRe, name) !== null ||
1401-
name === '.npm') {
1424+
name === '.npm') {
14021425
// Exclude versioned names that 'npm' installs.
14031426
return;
14041427
}
@@ -1431,20 +1454,20 @@ function complete(line, callback) {
14311454

14321455
ArrayPrototypePush(completionGroups, _builtinLibs, nodeSchemeBuiltinLibs);
14331456
} else if ((match = RegExpPrototypeExec(fsAutoCompleteRE, line)) !== null &&
1434-
this.allowBlockingCompletions) {
1457+
this.allowBlockingCompletions) {
14351458
({ 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 ''
1459+
// Handle variable member lookup.
1460+
// We support simple chained expressions like the following (no function
1461+
// calls, etc.). That is for simplicity and also because we *eval* that
1462+
// leading expression so for safety (see WARNING above) don't want to
1463+
// eval function calls.
1464+
//
1465+
// foo.bar<|> # completions for 'foo' with filter 'bar'
1466+
// spam.eggs.<|> # completions for 'spam.eggs' with filter ''
1467+
// foo<|> # all scope vars with filter 'foo'
1468+
// foo.<|> # completions for 'foo' with filter ''
14461469
} else if (line.length === 0 ||
1447-
RegExpPrototypeExec(/\w|\.|\$/, line[line.length - 1]) !== null) {
1470+
RegExpPrototypeExec(/\w|\.|\$/, line[line.length - 1]) !== null) {
14481471
const { 0: match } = RegExpPrototypeExec(simpleExpressionRE, line) || [''];
14491472
if (line.length !== 0 && !match) {
14501473
completionGroupsLoaded();
@@ -1495,7 +1518,7 @@ function complete(line, callback) {
14951518
try {
14961519
let p;
14971520
if ((typeof obj === 'object' && obj !== null) ||
1498-
typeof obj === 'function') {
1521+
typeof obj === 'function') {
14991522
memberGroups.push(filteredOwnPropertyNames(obj));
15001523
p = ObjectGetPrototypeOf(obj);
15011524
} 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)