Skip to content

Commit 3bb9b4e

Browse files
committed
repl: display dynamic import variant in static import error messages
Enhance the REPL message for static import error message. ``` > import {foo, bar} from 'moo'; import {foo, bar} from 'moo'; ^^^^^^ Uncaught: SyntaxError: .* dynamic import: const {foo,bar} = await import('moo'); ```
1 parent d2a1f71 commit 3bb9b4e

File tree

2 files changed

+106
-43
lines changed

2 files changed

+106
-43
lines changed

lib/repl.js

Lines changed: 71 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ const {
5858
ArrayPrototypeSome,
5959
ArrayPrototypeSort,
6060
ArrayPrototypeSplice,
61+
ArrayPrototypeToString,
6162
ArrayPrototypeUnshift,
6263
Boolean,
6364
Error,
@@ -104,7 +105,12 @@ const {
104105
const {
105106
isIdentifierStart,
106107
isIdentifierChar,
108+
parse: acornParse,
107109
} = require('internal/deps/acorn/acorn/dist/acorn');
110+
111+
112+
const acornWalk = require('internal/deps/acorn/acorn-walk/dist/walk');
113+
108114
const {
109115
decorateErrorStack,
110116
isError,
@@ -223,6 +229,29 @@ module.paths = CJSModule._nodeModulePaths(module.filename);
223229
const writer = (obj) => inspect(obj, writer.options);
224230
writer.options = { ...inspect.defaultOptions, showProxy: true };
225231

232+
// Converts static import statement to dynamic import statement
233+
const toDynamicImport = (codeLine) => {
234+
let dynamicImportStatement = '';
235+
let moduleName = '';
236+
const toCamelCase = (str) => str.replace(/[-_](\w)/g, (_, c) => c.toUpperCase());
237+
const ast = acornParse(codeLine, { sourceType: 'module', ecmaVersion: 'latest' });
238+
acornWalk.ancestor(ast, {
239+
ImportDeclaration: (node) => {
240+
const importedModules = node.source.value;
241+
const importedSpecifiers = node.specifiers.map((specifier) => specifier.local.name);
242+
if (importedSpecifiers.length > 1) {
243+
moduleName = `{${importedSpecifiers.join(',')}}`;
244+
} else {
245+
const formattedSpecifiers = importedSpecifiers.length ? ArrayPrototypeToString(importedSpecifiers) : '';
246+
moduleName = toCamelCase(formattedSpecifiers || importedModules);
247+
}
248+
dynamicImportStatement += `const ${moduleName} = await import('${importedModules}');`;
249+
},
250+
});
251+
return dynamicImportStatement;
252+
};
253+
254+
226255
function REPLServer(prompt,
227256
stream,
228257
eval_,
@@ -283,13 +312,13 @@ function REPLServer(prompt,
283312
get: pendingDeprecation ?
284313
deprecate(() => this.input,
285314
'repl.inputStream and repl.outputStream are deprecated. ' +
286-
'Use repl.input and repl.output instead',
315+
'Use repl.input and repl.output instead',
287316
'DEP0141') :
288317
() => this.input,
289318
set: pendingDeprecation ?
290319
deprecate((val) => this.input = val,
291320
'repl.inputStream and repl.outputStream are deprecated. ' +
292-
'Use repl.input and repl.output instead',
321+
'Use repl.input and repl.output instead',
293322
'DEP0141') :
294323
(val) => this.input = val,
295324
enumerable: false,
@@ -300,13 +329,13 @@ function REPLServer(prompt,
300329
get: pendingDeprecation ?
301330
deprecate(() => this.output,
302331
'repl.inputStream and repl.outputStream are deprecated. ' +
303-
'Use repl.input and repl.output instead',
332+
'Use repl.input and repl.output instead',
304333
'DEP0141') :
305334
() => this.output,
306335
set: pendingDeprecation ?
307336
deprecate((val) => this.output = val,
308337
'repl.inputStream and repl.outputStream are deprecated. ' +
309-
'Use repl.input and repl.output instead',
338+
'Use repl.input and repl.output instead',
310339
'DEP0141') :
311340
(val) => this.output = val,
312341
enumerable: false,
@@ -344,9 +373,9 @@ function REPLServer(prompt,
344373
// instance and that could trigger the `MaxListenersExceededWarning`.
345374
process.prependListener('newListener', (event, listener) => {
346375
if (event === 'uncaughtException' &&
347-
process.domain &&
348-
listener.name !== 'domainUncaughtExceptionClear' &&
349-
domainSet.has(process.domain)) {
376+
process.domain &&
377+
listener.name !== 'domainUncaughtExceptionClear' &&
378+
domainSet.has(process.domain)) {
350379
// Throw an error so that the event will not be added and the current
351380
// domain takes over. That way the user is notified about the error
352381
// and the current code evaluation is stopped, just as any other code
@@ -363,8 +392,8 @@ function REPLServer(prompt,
363392
const savedRegExMatches = ['', '', '', '', '', '', '', '', '', ''];
364393
const sep = '\u0000\u0000\u0000';
365394
const regExMatcher = new RegExp(`^${sep}(.*)${sep}(.*)${sep}(.*)${sep}(.*)` +
366-
`${sep}(.*)${sep}(.*)${sep}(.*)${sep}(.*)` +
367-
`${sep}(.*)$`);
395+
`${sep}(.*)${sep}(.*)${sep}(.*)${sep}(.*)` +
396+
`${sep}(.*)$`);
368397

369398
eval_ = eval_ || defaultEval;
370399

@@ -417,7 +446,7 @@ function REPLServer(prompt,
417446
// an expression. Note that if the above condition changes,
418447
// lib/internal/repl/utils.js needs to be changed to match.
419448
if (RegExpPrototypeExec(/^\s*{/, code) !== null &&
420-
RegExpPrototypeExec(/;\s*$/, code) === null) {
449+
RegExpPrototypeExec(/;\s*$/, code) === null) {
421450
code = `(${StringPrototypeTrim(code)})\n`;
422451
wrappedCmd = true;
423452
}
@@ -492,7 +521,7 @@ function REPLServer(prompt,
492521
while (true) {
493522
try {
494523
if (self.replMode === module.exports.REPL_MODE_STRICT &&
495-
RegExpPrototypeExec(/^\s*$/, code) === null) {
524+
RegExpPrototypeExec(/^\s*$/, code) === null) {
496525
// "void 0" keeps the repl from returning "use strict" as the result
497526
// value for statements and declarations that don't return a value.
498527
code = `'use strict'; void 0;\n${code}`;
@@ -684,7 +713,7 @@ function REPLServer(prompt,
684713
'module';
685714
if (StringPrototypeIncludes(e.message, importErrorStr)) {
686715
e.message = 'Cannot use import statement inside the Node.js ' +
687-
'REPL, alternatively use dynamic import';
716+
'REPL, alternatively use dynamic import: ' + toDynamicImport(self.lines.at(-1));
688717
e.stack = SideEffectFreeRegExpPrototypeSymbolReplace(
689718
/SyntaxError:.*\n/,
690719
e.stack,
@@ -712,7 +741,7 @@ function REPLServer(prompt,
712741
}
713742

714743
if (options[kStandaloneREPL] &&
715-
process.listenerCount('uncaughtException') !== 0) {
744+
process.listenerCount('uncaughtException') !== 0) {
716745
process.nextTick(() => {
717746
process.emit('uncaughtException', e);
718747
self.clearBufferedCommand();
@@ -729,7 +758,7 @@ function REPLServer(prompt,
729758
errStack = '';
730759
ArrayPrototypeForEach(lines, (line) => {
731760
if (!matched &&
732-
RegExpPrototypeExec(/^\[?([A-Z][a-z0-9_]*)*Error/, line) !== null) {
761+
RegExpPrototypeExec(/^\[?([A-Z][a-z0-9_]*)*Error/, line) !== null) {
733762
errStack += writer.options.breakLength >= line.length ?
734763
`Uncaught ${line}` :
735764
`Uncaught:\n${line}`;
@@ -875,8 +904,8 @@ function REPLServer(prompt,
875904
// display next prompt and return.
876905
if (trimmedCmd) {
877906
if (StringPrototypeCharAt(trimmedCmd, 0) === '.' &&
878-
StringPrototypeCharAt(trimmedCmd, 1) !== '.' &&
879-
NumberIsNaN(NumberParseFloat(trimmedCmd))) {
907+
StringPrototypeCharAt(trimmedCmd, 1) !== '.' &&
908+
NumberIsNaN(NumberParseFloat(trimmedCmd))) {
880909
const matches = RegExpPrototypeExec(/^\.([^\s]+)\s*(.*)$/, trimmedCmd);
881910
const keyword = matches && matches[1];
882911
const rest = matches && matches[2];
@@ -901,10 +930,10 @@ function REPLServer(prompt,
901930
ReflectApply(_memory, self, [cmd]);
902931

903932
if (e && !self[kBufferedCommandSymbol] &&
904-
StringPrototypeStartsWith(StringPrototypeTrim(cmd), 'npm ')) {
933+
StringPrototypeStartsWith(StringPrototypeTrim(cmd), 'npm ')) {
905934
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');
935+
'Node.js REPL, in your normal shell.\n' +
936+
'(Press Ctrl+D to exit.)\n');
908937
self.displayPrompt();
909938
return;
910939
}
@@ -929,11 +958,11 @@ function REPLServer(prompt,
929958

930959
// If we got any output - print it (if no error)
931960
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)) {
961+
// When an invalid REPL command is used, error message is printed
962+
// immediately. We don't have to print anything else. So, only when
963+
// the second argument to this function is there, print it.
964+
arguments.length === 2 &&
965+
(!self.ignoreUndefined || ret !== undefined)) {
937966
if (!self.underscoreAssigned) {
938967
self.last = ret;
939968
}
@@ -984,7 +1013,7 @@ function REPLServer(prompt,
9841013
if (!self.editorMode || !self.terminal) {
9851014
// Before exiting, make sure to clear the line.
9861015
if (key.ctrl && key.name === 'd' &&
987-
self.cursor === 0 && self.line.length === 0) {
1016+
self.cursor === 0 && self.line.length === 0) {
9881017
self.clearLine();
9891018
}
9901019
clearPreview(key);
@@ -1181,7 +1210,7 @@ const importRE = /\bimport\s*\(\s*['"`](([\w@./:-]+\/)?(?:[\w@./:-]*))(?![^'"`])
11811210
const requireRE = /\brequire\s*\(\s*['"`](([\w@./:-]+\/)?(?:[\w@./:-]*))(?![^'"`])$/;
11821211
const fsAutoCompleteRE = /fs(?:\.promises)?\.\s*[a-z][a-zA-Z]+\(\s*["'](.*)/;
11831212
const simpleExpressionRE =
1184-
/(?:[\w$'"`[{(](?:\w|\$|['"`\]})])*\??\.)*[a-zA-Z_$](?:\w|\$)*\??\.?$/;
1213+
/(?:[\w$'"`[{(](?:\w|\$|['"`\]})])*\??\.)*[a-zA-Z_$](?:\w|\$)*\??\.?$/;
11851214
const versionedFileNamesRe = /-\d+\.\d+/;
11861215

11871216
function isIdentifier(str) {
@@ -1337,15 +1366,15 @@ function complete(line, callback) {
13371366
const dirents = gracefulReaddir(dir, { withFileTypes: true }) || [];
13381367
ArrayPrototypeForEach(dirents, (dirent) => {
13391368
if (RegExpPrototypeExec(versionedFileNamesRe, dirent.name) !== null ||
1340-
dirent.name === '.npm') {
1369+
dirent.name === '.npm') {
13411370
// Exclude versioned names that 'npm' installs.
13421371
return;
13431372
}
13441373
const extension = path.extname(dirent.name);
13451374
const base = StringPrototypeSlice(dirent.name, 0, -extension.length);
13461375
if (!dirent.isDirectory()) {
13471376
if (StringPrototypeIncludes(extensions, extension) &&
1348-
(!subdir || base !== 'index')) {
1377+
(!subdir || base !== 'index')) {
13491378
ArrayPrototypePush(group, `${subdir}${base}`);
13501379
}
13511380
return;
@@ -1398,7 +1427,7 @@ function complete(line, callback) {
13981427
ArrayPrototypeForEach(dirents, (dirent) => {
13991428
const { name } = dirent;
14001429
if (RegExpPrototypeExec(versionedFileNamesRe, name) !== null ||
1401-
name === '.npm') {
1430+
name === '.npm') {
14021431
// Exclude versioned names that 'npm' installs.
14031432
return;
14041433
}
@@ -1431,20 +1460,20 @@ function complete(line, callback) {
14311460

14321461
ArrayPrototypePush(completionGroups, _builtinLibs, nodeSchemeBuiltinLibs);
14331462
} else if ((match = RegExpPrototypeExec(fsAutoCompleteRE, line)) !== null &&
1434-
this.allowBlockingCompletions) {
1463+
this.allowBlockingCompletions) {
14351464
({ 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 ''
1465+
// Handle variable member lookup.
1466+
// We support simple chained expressions like the following (no function
1467+
// calls, etc.). That is for simplicity and also because we *eval* that
1468+
// leading expression so for safety (see WARNING above) don't want to
1469+
// eval function calls.
1470+
//
1471+
// foo.bar<|> # completions for 'foo' with filter 'bar'
1472+
// spam.eggs.<|> # completions for 'spam.eggs' with filter ''
1473+
// foo<|> # all scope vars with filter 'foo'
1474+
// foo.<|> # completions for 'foo' with filter ''
14461475
} else if (line.length === 0 ||
1447-
RegExpPrototypeExec(/\w|\.|\$/, line[line.length - 1]) !== null) {
1476+
RegExpPrototypeExec(/\w|\.|\$/, line[line.length - 1]) !== null) {
14481477
const { 0: match } = RegExpPrototypeExec(simpleExpressionRE, line) || [''];
14491478
if (line.length !== 0 && !match) {
14501479
completionGroupsLoaded();
@@ -1495,7 +1524,7 @@ function complete(line, callback) {
14951524
try {
14961525
let p;
14971526
if ((typeof obj === 'object' && obj !== null) ||
1498-
typeof obj === 'function') {
1527+
typeof obj === 'function') {
14991528
memberGroups.push(filteredOwnPropertyNames(obj));
15001529
p = ObjectGetPrototypeOf(obj);
15011530
} else {

test/parallel/test-repl.js

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -818,7 +818,41 @@ const tcpTests = [
818818
kArrow,
819819
'',
820820
'Uncaught:',
821-
/^SyntaxError: .* dynamic import/,
821+
'SyntaxError: Cannot use import statement inside the Node.js REPL, \
822+
alternatively use dynamic import: const comeOn = await import(\'fhqwhgads\');',
823+
]
824+
},
825+
{
826+
send: 'import { export1, export2 } from "module-name"',
827+
expect: [
828+
kSource,
829+
kArrow,
830+
'',
831+
'Uncaught:',
832+
'SyntaxError: Cannot use import statement inside the Node.js REPL, \
833+
alternatively use dynamic import: const {export1,export2} = await import(\'module-name\');',
834+
]
835+
},
836+
{
837+
send: 'import * as name from "module-name";',
838+
expect: [
839+
kSource,
840+
kArrow,
841+
'',
842+
'Uncaught:',
843+
'SyntaxError: Cannot use import statement inside the Node.js REPL, \
844+
alternatively use dynamic import: const name = await import(\'module-name\');',
845+
]
846+
},
847+
{
848+
send: 'import "module-name";',
849+
expect: [
850+
kSource,
851+
kArrow,
852+
'',
853+
'Uncaught:',
854+
'SyntaxError: Cannot use import statement inside the Node.js REPL, \
855+
alternatively use dynamic import: const moduleName = await import(\'module-name\');',
822856
]
823857
},
824858
];

0 commit comments

Comments
 (0)