Skip to content

Commit

Permalink
repl: display dynamic import version in error message
Browse files Browse the repository at this point in the history
    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");
    ```
  • Loading branch information
hemanth committed May 24, 2023
1 parent 85ac915 commit cd2c691
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 43 deletions.
107 changes: 65 additions & 42 deletions lib/repl.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,11 @@ const {
const {
isIdentifierStart,
isIdentifierChar,
Parser: acornParser,
} = require('internal/deps/acorn/acorn/dist/acorn');

const acornWalk = require('internal/deps/acorn/acorn-walk/dist/walk');

const {
decorateErrorStack,
isError,
Expand Down Expand Up @@ -223,6 +227,25 @@ module.paths = CJSModule._nodeModulePaths(module.filename);
const writer = (obj) => inspect(obj, writer.options);
writer.options = { ...inspect.defaultOptions, showProxy: true };

// Converts static import statement to dynamic import statement
const toDynamicImport = (codeLine) => {
let dynamicImportStatement = '';
const ast = acornParser(codeLine, { sourceType: 'module' });

acornWalk.ancestor(ast, {
ImportDeclaration(node, ancestors) {
const importedModules = node.source.value;
let importedSpecifiers = node.specifiers.map((specifier) => specifier.local.name);
if (importedSpecifiers.length > 1) {
importedSpecifiers = `{${importedSpecifiers.join(',')}}`;
}
dynamicImportStatement += `const ${importedSpecifiers.length || importedModules} = await import('${importedModules}');`;
},
});
return dynamicImportStatement;
};


function REPLServer(prompt,
stream,
eval_,
Expand Down Expand Up @@ -283,13 +306,13 @@ function REPLServer(prompt,
get: pendingDeprecation ?
deprecate(() => this.input,
'repl.inputStream and repl.outputStream are deprecated. ' +
'Use repl.input and repl.output instead',
'Use repl.input and repl.output instead',
'DEP0141') :
() => this.input,
set: pendingDeprecation ?
deprecate((val) => this.input = val,
'repl.inputStream and repl.outputStream are deprecated. ' +
'Use repl.input and repl.output instead',
'Use repl.input and repl.output instead',
'DEP0141') :
(val) => this.input = val,
enumerable: false,
Expand All @@ -300,13 +323,13 @@ function REPLServer(prompt,
get: pendingDeprecation ?
deprecate(() => this.output,
'repl.inputStream and repl.outputStream are deprecated. ' +
'Use repl.input and repl.output instead',
'Use repl.input and repl.output instead',
'DEP0141') :
() => this.output,
set: pendingDeprecation ?
deprecate((val) => this.output = val,
'repl.inputStream and repl.outputStream are deprecated. ' +
'Use repl.input and repl.output instead',
'Use repl.input and repl.output instead',
'DEP0141') :
(val) => this.output = val,
enumerable: false,
Expand Down Expand Up @@ -344,9 +367,9 @@ function REPLServer(prompt,
// instance and that could trigger the `MaxListenersExceededWarning`.
process.prependListener('newListener', (event, listener) => {
if (event === 'uncaughtException' &&
process.domain &&
listener.name !== 'domainUncaughtExceptionClear' &&
domainSet.has(process.domain)) {
process.domain &&
listener.name !== 'domainUncaughtExceptionClear' &&
domainSet.has(process.domain)) {
// Throw an error so that the event will not be added and the current
// domain takes over. That way the user is notified about the error
// and the current code evaluation is stopped, just as any other code
Expand All @@ -363,8 +386,8 @@ function REPLServer(prompt,
const savedRegExMatches = ['', '', '', '', '', '', '', '', '', ''];
const sep = '\u0000\u0000\u0000';
const regExMatcher = new RegExp(`^${sep}(.*)${sep}(.*)${sep}(.*)${sep}(.*)` +
`${sep}(.*)${sep}(.*)${sep}(.*)${sep}(.*)` +
`${sep}(.*)$`);
`${sep}(.*)${sep}(.*)${sep}(.*)${sep}(.*)` +
`${sep}(.*)$`);

eval_ = eval_ || defaultEval;

Expand Down Expand Up @@ -417,7 +440,7 @@ function REPLServer(prompt,
// an expression. Note that if the above condition changes,
// lib/internal/repl/utils.js needs to be changed to match.
if (RegExpPrototypeExec(/^\s*{/, code) !== null &&
RegExpPrototypeExec(/;\s*$/, code) === null) {
RegExpPrototypeExec(/;\s*$/, code) === null) {
code = `(${StringPrototypeTrim(code)})\n`;
wrappedCmd = true;
}
Expand Down Expand Up @@ -492,7 +515,7 @@ function REPLServer(prompt,
while (true) {
try {
if (self.replMode === module.exports.REPL_MODE_STRICT &&
RegExpPrototypeExec(/^\s*$/, code) === null) {
RegExpPrototypeExec(/^\s*$/, code) === null) {
// "void 0" keeps the repl from returning "use strict" as the result
// value for statements and declarations that don't return a value.
code = `'use strict'; void 0;\n${code}`;
Expand Down Expand Up @@ -684,7 +707,7 @@ function REPLServer(prompt,
'module';
if (StringPrototypeIncludes(e.message, importErrorStr)) {
e.message = 'Cannot use import statement inside the Node.js ' +
'REPL, alternatively use dynamic import';
'REPL, alternatively use dynamic import: ' + toDynamicImport(self.lines.at(-1));
e.stack = SideEffectFreeRegExpPrototypeSymbolReplace(
/SyntaxError:.*\n/,
e.stack,
Expand Down Expand Up @@ -712,7 +735,7 @@ function REPLServer(prompt,
}

if (options[kStandaloneREPL] &&
process.listenerCount('uncaughtException') !== 0) {
process.listenerCount('uncaughtException') !== 0) {
process.nextTick(() => {
process.emit('uncaughtException', e);
self.clearBufferedCommand();
Expand All @@ -729,7 +752,7 @@ function REPLServer(prompt,
errStack = '';
ArrayPrototypeForEach(lines, (line) => {
if (!matched &&
RegExpPrototypeExec(/^\[?([A-Z][a-z0-9_]*)*Error/, line) !== null) {
RegExpPrototypeExec(/^\[?([A-Z][a-z0-9_]*)*Error/, line) !== null) {
errStack += writer.options.breakLength >= line.length ?
`Uncaught ${line}` :
`Uncaught:\n${line}`;
Expand Down Expand Up @@ -875,8 +898,8 @@ function REPLServer(prompt,
// display next prompt and return.
if (trimmedCmd) {
if (StringPrototypeCharAt(trimmedCmd, 0) === '.' &&
StringPrototypeCharAt(trimmedCmd, 1) !== '.' &&
NumberIsNaN(NumberParseFloat(trimmedCmd))) {
StringPrototypeCharAt(trimmedCmd, 1) !== '.' &&
NumberIsNaN(NumberParseFloat(trimmedCmd))) {
const matches = RegExpPrototypeExec(/^\.([^\s]+)\s*(.*)$/, trimmedCmd);
const keyword = matches && matches[1];
const rest = matches && matches[2];
Expand All @@ -901,10 +924,10 @@ function REPLServer(prompt,
ReflectApply(_memory, self, [cmd]);

if (e && !self[kBufferedCommandSymbol] &&
StringPrototypeStartsWith(StringPrototypeTrim(cmd), 'npm ')) {
StringPrototypeStartsWith(StringPrototypeTrim(cmd), 'npm ')) {
self.output.write('npm should be run outside of the ' +
'Node.js REPL, in your normal shell.\n' +
'(Press Ctrl+D to exit.)\n');
'Node.js REPL, in your normal shell.\n' +
'(Press Ctrl+D to exit.)\n');
self.displayPrompt();
return;
}
Expand All @@ -929,11 +952,11 @@ function REPLServer(prompt,

// If we got any output - print it (if no error)
if (!e &&
// When an invalid REPL command is used, error message is printed
// immediately. We don't have to print anything else. So, only when
// the second argument to this function is there, print it.
arguments.length === 2 &&
(!self.ignoreUndefined || ret !== undefined)) {
// When an invalid REPL command is used, error message is printed
// immediately. We don't have to print anything else. So, only when
// the second argument to this function is there, print it.
arguments.length === 2 &&
(!self.ignoreUndefined || ret !== undefined)) {
if (!self.underscoreAssigned) {
self.last = ret;
}
Expand Down Expand Up @@ -984,7 +1007,7 @@ function REPLServer(prompt,
if (!self.editorMode || !self.terminal) {
// Before exiting, make sure to clear the line.
if (key.ctrl && key.name === 'd' &&
self.cursor === 0 && self.line.length === 0) {
self.cursor === 0 && self.line.length === 0) {
self.clearLine();
}
clearPreview(key);
Expand Down Expand Up @@ -1181,7 +1204,7 @@ const importRE = /\bimport\s*\(\s*['"`](([\w@./:-]+\/)?(?:[\w@./:-]*))(?![^'"`])
const requireRE = /\brequire\s*\(\s*['"`](([\w@./:-]+\/)?(?:[\w@./:-]*))(?![^'"`])$/;
const fsAutoCompleteRE = /fs(?:\.promises)?\.\s*[a-z][a-zA-Z]+\(\s*["'](.*)/;
const simpleExpressionRE =
/(?:[\w$'"`[{(](?:\w|\$|['"`\]})])*\??\.)*[a-zA-Z_$](?:\w|\$)*\??\.?$/;
/(?:[\w$'"`[{(](?:\w|\$|['"`\]})])*\??\.)*[a-zA-Z_$](?:\w|\$)*\??\.?$/;
const versionedFileNamesRe = /-\d+\.\d+/;

function isIdentifier(str) {
Expand Down Expand Up @@ -1337,15 +1360,15 @@ function complete(line, callback) {
const dirents = gracefulReaddir(dir, { withFileTypes: true }) || [];
ArrayPrototypeForEach(dirents, (dirent) => {
if (RegExpPrototypeExec(versionedFileNamesRe, dirent.name) !== null ||
dirent.name === '.npm') {
dirent.name === '.npm') {
// Exclude versioned names that 'npm' installs.
return;
}
const extension = path.extname(dirent.name);
const base = StringPrototypeSlice(dirent.name, 0, -extension.length);
if (!dirent.isDirectory()) {
if (StringPrototypeIncludes(extensions, extension) &&
(!subdir || base !== 'index')) {
(!subdir || base !== 'index')) {
ArrayPrototypePush(group, `${subdir}${base}`);
}
return;
Expand Down Expand Up @@ -1398,7 +1421,7 @@ function complete(line, callback) {
ArrayPrototypeForEach(dirents, (dirent) => {
const { name } = dirent;
if (RegExpPrototypeExec(versionedFileNamesRe, name) !== null ||
name === '.npm') {
name === '.npm') {
// Exclude versioned names that 'npm' installs.
return;
}
Expand Down Expand Up @@ -1431,20 +1454,20 @@ function complete(line, callback) {

ArrayPrototypePush(completionGroups, _builtinLibs, nodeSchemeBuiltinLibs);
} else if ((match = RegExpPrototypeExec(fsAutoCompleteRE, line)) !== null &&
this.allowBlockingCompletions) {
this.allowBlockingCompletions) {
({ 0: completionGroups, 1: completeOn } = completeFSFunctions(match));
// Handle variable member lookup.
// We support simple chained expressions like the following (no function
// calls, etc.). That is for simplicity and also because we *eval* that
// leading expression so for safety (see WARNING above) don't want to
// eval function calls.
//
// foo.bar<|> # completions for 'foo' with filter 'bar'
// spam.eggs.<|> # completions for 'spam.eggs' with filter ''
// foo<|> # all scope vars with filter 'foo'
// foo.<|> # completions for 'foo' with filter ''
// Handle variable member lookup.
// We support simple chained expressions like the following (no function
// calls, etc.). That is for simplicity and also because we *eval* that
// leading expression so for safety (see WARNING above) don't want to
// eval function calls.
//
// foo.bar<|> # completions for 'foo' with filter 'bar'
// spam.eggs.<|> # completions for 'spam.eggs' with filter ''
// foo<|> # all scope vars with filter 'foo'
// foo.<|> # completions for 'foo' with filter ''
} else if (line.length === 0 ||
RegExpPrototypeExec(/\w|\.|\$/, line[line.length - 1]) !== null) {
RegExpPrototypeExec(/\w|\.|\$/, line[line.length - 1]) !== null) {
const { 0: match } = RegExpPrototypeExec(simpleExpressionRE, line) || [''];
if (line.length !== 0 && !match) {
completionGroupsLoaded();
Expand Down Expand Up @@ -1495,7 +1518,7 @@ function complete(line, callback) {
try {
let p;
if ((typeof obj === 'object' && obj !== null) ||
typeof obj === 'function') {
typeof obj === 'function') {
memberGroups.push(filteredOwnPropertyNames(obj));
p = ObjectGetPrototypeOf(obj);
} else {
Expand Down
2 changes: 1 addition & 1 deletion test/parallel/test-repl.js
Original file line number Diff line number Diff line change
Expand Up @@ -818,7 +818,7 @@ const tcpTests = [
kArrow,
'',
'Uncaught:',
/^SyntaxError: .* dynamic import/,
/^SyntaxError: .* dynamic import: {2}const comeOn = await import('fhqwhgads');/,
]
},
];
Expand Down

0 comments on commit cd2c691

Please sign in to comment.