-
-
Notifications
You must be signed in to change notification settings - Fork 35.4k
module: improve named cjs import errors #35453
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
22f700c
e4a018a
75c84fd
757d643
1fcb340
368305c
0677559
ae0a8ee
4d11c44
182245f
533560f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
When trying to import named exports from a CommonJS module an error is thrown. Unfortunately the V8 error only contains the single line that causes the error, it is therefore impossible to construct an equivalent code consisting of default import + object descructuring assignment. This was the reason why the example code was removed for multi line import statements in #35275 To generate a helpful error messages for any case we can parse the file where the error happens using acorn and construct a valid example code from the parsed ImportDeclaration. This will work for _any_ valid import statement. Since this code is only executed shortly before the node process crashes anyways performance should not be a concern here. Fixes: #35259 Refs: #35275
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -5,6 +5,7 @@ const { | |||||||
| ArrayPrototypeMap, | ||||||||
| ArrayPrototypePush, | ||||||||
| FunctionPrototype, | ||||||||
| Number, | ||||||||
| ObjectSetPrototypeOf, | ||||||||
| PromiseAll, | ||||||||
| PromiseResolve, | ||||||||
|
|
@@ -14,20 +15,78 @@ const { | |||||||
| SafeSet, | ||||||||
| StringPrototypeIncludes, | ||||||||
| StringPrototypeMatch, | ||||||||
| StringPrototypeReplace, | ||||||||
| StringPrototypeSplit, | ||||||||
| } = primordials; | ||||||||
|
|
||||||||
| const { ModuleWrap } = internalBinding('module_wrap'); | ||||||||
|
|
||||||||
| const { decorateErrorStack } = require('internal/util'); | ||||||||
| const { fileURLToPath } = require('url'); | ||||||||
| const assert = require('internal/assert'); | ||||||||
| const resolvedPromise = PromiseResolve(); | ||||||||
|
|
||||||||
| const noop = FunctionPrototype; | ||||||||
|
|
||||||||
| let hasPausedEntry = false; | ||||||||
|
|
||||||||
| function extractExample(file, lineNumber) { | ||||||||
| const { readFileSync } = require('fs'); | ||||||||
| const { parse } = require('internal/deps/acorn/acorn/dist/acorn'); | ||||||||
| const { findNodeAt } = require('internal/deps/acorn/acorn-walk/dist/walk'); | ||||||||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it OK to "lazy-load" inline here? My assumption is that this code is executed exactly once before the node process crashes. Is my assumption correct? I saw a different way of lazy loading acorn in assert Lines 216 to 218 in 4a6005c
|
||||||||
|
|
||||||||
| const code = readFileSync(file, { encoding: 'utf8' }); | ||||||||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it OK to
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That code would run only if the process is crashing, right? I think it's perfectly fine to use
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is precisely the question: I don't understand 100% yet of this error will always lead to termination of the node process. If so, I also think that
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unless I find a way to parse the input file in a streaming fashion… Since imports typically come in the beginning of a file this would allow us to reduce the parsing work to a minimum… But maybe that's all unnecessary micro-optimizations for the crash case.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This has been benchmarked already, see nodejs/cjs-module-lexer#4 (comment). The gist of it is that streaming is not worth it for most JS files.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the context! Great, so the one possible optimization would be, having read the entire file, around not parsing the entire file with with acorn, see #35453 (comment) |
||||||||
| const parsed = parse(code, { | ||||||||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there any way around parsing the full file? I tried working with |
||||||||
| sourceType: 'module', | ||||||||
| locations: true, | ||||||||
| }); | ||||||||
| let start = 0; | ||||||||
| let node; | ||||||||
| do { | ||||||||
| node = findNodeAt(parsed, start); | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you imagine a test case that would reproduce this?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure, but I assume it may happen because it's in your Have you tried to use
Suggested change
|
||||||||
| start = node.node.end + 1; | ||||||||
|
|
||||||||
| if (node.node.loc.end.line < lineNumber) { | ||||||||
| continue; | ||||||||
| } | ||||||||
|
|
||||||||
| if (node.node.type !== 'ImportDeclaration') { | ||||||||
| continue; | ||||||||
| } | ||||||||
|
|
||||||||
| const defaultSpecifier = node.node.specifiers.find( | ||||||||
| (specifier) => specifier.type === 'ImportDefaultSpecifier' | ||||||||
| ); | ||||||||
|
ctavan marked this conversation as resolved.
Outdated
|
||||||||
| const defaultImport = defaultSpecifier | ||||||||
| ? defaultSpecifier.local.name | ||||||||
| : 'pkg'; | ||||||||
|
|
||||||||
| const joinString = ', '; | ||||||||
| let totalLength = 0; | ||||||||
| const imports = node.node.specifiers | ||||||||
| .filter((specifier) => specifier.type === 'ImportSpecifier') | ||||||||
| .map((specifier) => { | ||||||||
| const statement = | ||||||||
| specifier.local.name === specifier.imported.name | ||||||||
| ? `${specifier.imported.name}` | ||||||||
| : `${specifier.imported.name}: ${specifier.local.name}`; | ||||||||
| totalLength += statement.length + joinString.length; | ||||||||
| return statement; | ||||||||
| }); | ||||||||
|
|
||||||||
| const boilerplate = `const { } = ${defaultImport};`; | ||||||||
| const destructuringAssignment = | ||||||||
| totalLength > 80 - boilerplate.length | ||||||||
| ? `\n${imports.map((i) => ` ${i}`).join(',\n')}\n` | ||||||||
| : ` ${imports.join(joinString)} `; | ||||||||
|
|
||||||||
| return ( | ||||||||
| `\n\nimport ${defaultImport} from '${node.node.source.value}';\n` + | ||||||||
| `const {${destructuringAssignment}} = ${defaultImport};\n` | ||||||||
| ); | ||||||||
| } while (node === undefined || node.node.loc.start.line <= lineNumber); | ||||||||
| return ''; | ||||||||
|
aduh95 marked this conversation as resolved.
Outdated
|
||||||||
| } | ||||||||
|
|
||||||||
| /* A ModuleJob tracks the loading of a single Module, and the ModuleJobs of | ||||||||
| * its dependencies, over time. */ | ||||||||
| class ModuleJob { | ||||||||
|
|
@@ -91,8 +150,11 @@ class ModuleJob { | |||||||
| } | ||||||||
| jobsInGraph.add(moduleJob); | ||||||||
| const dependencyJobs = await moduleJob.linked; | ||||||||
| return PromiseAll(new SafeArrayIterator( | ||||||||
| ArrayPrototypeMap(dependencyJobs, addJobsToDependencyGraph))); | ||||||||
| return PromiseAll( | ||||||||
| new SafeArrayIterator( | ||||||||
| ArrayPrototypeMap(dependencyJobs, addJobsToDependencyGraph) | ||||||||
| ) | ||||||||
| ); | ||||||||
| }; | ||||||||
| await addJobsToDependencyGraph(this); | ||||||||
|
|
||||||||
|
|
@@ -106,32 +168,35 @@ class ModuleJob { | |||||||
| } | ||||||||
| } catch (e) { | ||||||||
| decorateErrorStack(e); | ||||||||
| if (StringPrototypeIncludes(e.message, | ||||||||
| ' does not provide an export named')) { | ||||||||
| if ( | ||||||||
| StringPrototypeIncludes(e.message, ' does not provide an export named') | ||||||||
| ) { | ||||||||
| const splitStack = StringPrototypeSplit(e.stack, '\n'); | ||||||||
| const parentFileUrl = splitStack[0]; | ||||||||
| const { 1: childSpecifier, 2: name } = StringPrototypeMatch( | ||||||||
| e.message, | ||||||||
| /module '(.*)' does not provide an export named '(.+)'/); | ||||||||
| const childFileURL = | ||||||||
| await this.loader.resolve(childSpecifier, parentFileUrl); | ||||||||
| /module '(.*)' does not provide an export named '(.+)'/ | ||||||||
| ); | ||||||||
| const childFileURL = await this.loader.resolve( | ||||||||
| childSpecifier, | ||||||||
| parentFileUrl | ||||||||
| ); | ||||||||
| const format = await this.loader.getFormat(childFileURL); | ||||||||
| if (format === 'commonjs') { | ||||||||
| const importStatement = splitStack[1]; | ||||||||
| // TODO(@ctavan): The original error stack only provides the single | ||||||||
| // line which causes the error. For multi-line import statements we | ||||||||
| // cannot generate an equivalent object descructuring assignment by | ||||||||
| // just parsing the error stack. | ||||||||
| const oneLineNamedImports = StringPrototypeMatch(importStatement, /{.*}/); | ||||||||
| const destructuringAssignment = oneLineNamedImports && | ||||||||
| StringPrototypeReplace(oneLineNamedImports, /\s+as\s+/g, ': '); | ||||||||
| e.message = `Named export '${name}' not found. The requested module` + | ||||||||
| const [, fileUrl, lineNumber] = StringPrototypeMatch( | ||||||||
| parentFileUrl, | ||||||||
| /^(.*):([0-9]+)$/ | ||||||||
| ); | ||||||||
| const example = extractExample( | ||||||||
| fileURLToPath(fileUrl), | ||||||||
| Number(lineNumber) | ||||||||
| ); | ||||||||
| e.message = | ||||||||
| `Named export '${name}' not found. The requested module` + | ||||||||
| ` '${childSpecifier}' is a CommonJS module, which may not support` + | ||||||||
| ' all module.exports as named exports.\nCommonJS modules can ' + | ||||||||
| 'always be imported via the default export, for example using:' + | ||||||||
| `\n\nimport pkg from '${childSpecifier}';\n${ | ||||||||
| destructuringAssignment ? | ||||||||
| `const ${destructuringAssignment} = pkg;\n` : ''}`; | ||||||||
| example; | ||||||||
| const newStack = StringPrototypeSplit(e.stack, '\n'); | ||||||||
| newStack[3] = `SyntaxError: ${e.message}`; | ||||||||
| e.stack = ArrayPrototypeJoin(newStack, '\n'); | ||||||||
|
|
||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| import abc, { | ||
| comeOn as comeOnRenamed, | ||
| everybody, | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For reasons I don't understand this import statement only errors out on line 3 ( Any clues why this is the case? |
||
| } from './fail.cjs'; | ||
|
aduh95 marked this conversation as resolved.
Outdated
|
||
Uh oh!
There was an error while loading. Please reload this page.