From f7620bdbfb277a5b9ec3fd3707eb1072f9232351 Mon Sep 17 00:00:00 2001 From: Hemanth HM Date: Sat, 27 May 2023 07:17:38 -0700 Subject: [PATCH] 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: Cannot use import statement inside the Node.js REPL, alternatively use dynamic import: const {foo,bar} = await import('moo'); ``` --- lib/repl.js | 113 +++++++++++++++++++++++-------------- test/parallel/test-repl.js | 36 +++++++++++- 2 files changed, 106 insertions(+), 43 deletions(-) diff --git a/lib/repl.js b/lib/repl.js index df038901d7834a..368d442c3b6e8d 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -58,6 +58,7 @@ const { ArrayPrototypeSome, ArrayPrototypeSort, ArrayPrototypeSplice, + ArrayPrototypeToString, ArrayPrototypeUnshift, Boolean, Error, @@ -104,7 +105,12 @@ const { const { isIdentifierStart, isIdentifierChar, + parse: acornParse, } = require('internal/deps/acorn/acorn/dist/acorn'); + + +const acornWalk = require('internal/deps/acorn/acorn-walk/dist/walk'); + const { decorateErrorStack, isError, @@ -223,6 +229,29 @@ 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 = ''; + let moduleName = ''; + const toCamelCase = (str) => str.replace(/[-_](\w)/g, (_, c) => c.toUpperCase()); + const ast = acornParse(codeLine, { sourceType: 'module', ecmaVersion: 'latest' }); + acornWalk.ancestor(ast, { + ImportDeclaration: (node) => { + const importedModules = node.source.value; + const importedSpecifiers = node.specifiers.map((specifier) => specifier.local.name); + if (importedSpecifiers.length > 1) { + moduleName = `{${importedSpecifiers.join(',')}}`; + } else { + const formattedSpecifiers = importedSpecifiers.length ? ArrayPrototypeToString(importedSpecifiers) : ''; + moduleName = toCamelCase(formattedSpecifiers || importedModules); + } + dynamicImportStatement += `const ${moduleName} = await import('${importedModules}');`; + }, + }); + return dynamicImportStatement; +}; + + function REPLServer(prompt, stream, eval_, @@ -283,13 +312,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, @@ -300,13 +329,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, @@ -344,9 +373,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 @@ -363,8 +392,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; @@ -417,7 +446,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; } @@ -492,7 +521,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}`; @@ -684,7 +713,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, @@ -712,7 +741,7 @@ function REPLServer(prompt, } if (options[kStandaloneREPL] && - process.listenerCount('uncaughtException') !== 0) { + process.listenerCount('uncaughtException') !== 0) { process.nextTick(() => { process.emit('uncaughtException', e); self.clearBufferedCommand(); @@ -729,7 +758,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}`; @@ -875,8 +904,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]; @@ -901,10 +930,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; } @@ -929,11 +958,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; } @@ -984,7 +1013,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); @@ -1181,7 +1210,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) { @@ -1337,7 +1366,7 @@ 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; } @@ -1345,7 +1374,7 @@ function complete(line, callback) { 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; @@ -1398,7 +1427,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; } @@ -1431,20 +1460,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(); @@ -1495,7 +1524,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 { diff --git a/test/parallel/test-repl.js b/test/parallel/test-repl.js index 4981816151f55b..4fc6ebb6fc4b54 100644 --- a/test/parallel/test-repl.js +++ b/test/parallel/test-repl.js @@ -818,7 +818,41 @@ const tcpTests = [ kArrow, '', 'Uncaught:', - /^SyntaxError: .* dynamic import/, + 'SyntaxError: Cannot use import statement inside the Node.js REPL, \ +alternatively use dynamic import: const comeOn = await import(\'fhqwhgads\');', + ] + }, + { + send: 'import { export1, export2 } from "module-name"', + expect: [ + kSource, + kArrow, + '', + 'Uncaught:', + 'SyntaxError: Cannot use import statement inside the Node.js REPL, \ +alternatively use dynamic import: const {export1,export2} = await import(\'module-name\');', + ] + }, + { + send: 'import * as name from "module-name";', + expect: [ + kSource, + kArrow, + '', + 'Uncaught:', + 'SyntaxError: Cannot use import statement inside the Node.js REPL, \ +alternatively use dynamic import: const name = await import(\'module-name\');', + ] + }, + { + send: 'import "module-name";', + expect: [ + kSource, + kArrow, + '', + 'Uncaught:', + 'SyntaxError: Cannot use import statement inside the Node.js REPL, \ +alternatively use dynamic import: const moduleName = await import(\'module-name\');', ] }, ];