-
-
Notifications
You must be signed in to change notification settings - Fork 31.6k
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 all commits
22f700c
e4a018a
75c84fd
757d643
1fcb340
368305c
0677559
ae0a8ee
4d11c44
182245f
533560f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
@@ -1,10 +1,13 @@ | ||||||||
'use strict'; | ||||||||
|
||||||||
const { | ||||||||
ArrayPrototypeFilter, | ||||||||
ArrayPrototypeFind, | ||||||||
ArrayPrototypeJoin, | ||||||||
ArrayPrototypeMap, | ||||||||
ArrayPrototypePush, | ||||||||
FunctionPrototype, | ||||||||
Number, | ||||||||
ObjectSetPrototypeOf, | ||||||||
PromiseAll, | ||||||||
PromiseResolve, | ||||||||
|
@@ -14,20 +17,82 @@ const { | |||||||
SafeSet, | ||||||||
StringPrototypeIncludes, | ||||||||
StringPrototypeMatch, | ||||||||
StringPrototypeReplace, | ||||||||
StringPrototypeSplit, | ||||||||
} = primordials; | ||||||||
|
||||||||
const { ModuleWrap } = internalBinding('module_wrap'); | ||||||||
|
||||||||
const { decorateErrorStack } = require('internal/util'); | ||||||||
const { fileURLToPath } = require('internal/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'); | ||||||||
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' }); | ||||||||
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 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 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 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. 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. 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, { | ||||||||
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); | ||||||||
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 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? 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 = ArrayPrototypeFind( | ||||||||
node.node.specifiers, | ||||||||
(specifier) => specifier.type === 'ImportDefaultSpecifier' | ||||||||
); | ||||||||
const defaultImport = defaultSpecifier ? | ||||||||
defaultSpecifier.local.name : | ||||||||
'pkg'; | ||||||||
|
||||||||
const joinString = ', '; | ||||||||
let totalLength = 0; | ||||||||
const imports = ArrayPrototypeMap( | ||||||||
ArrayPrototypeFilter( | ||||||||
node.node.specifiers, | ||||||||
(specifier) => (specifier.type === 'ImportSpecifier'), | ||||||||
), | ||||||||
(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 ? | ||||||||
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. My line splitting logic is pretty "simplistic"… Any idea on how to do this better? 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 wonder if we do anything similar elsewhere? a nice to have would be checking the terminal width, rather than assuming 80. |
||||||||
`\n${ArrayPrototypeJoin( | ||||||||
ArrayPrototypeMap(imports, (i) => ` ${i}`), | ||||||||
',\n', | ||||||||
)}\n` : | ||||||||
` ${ArrayPrototypeJoin(imports, joinString)} `; | ||||||||
|
||||||||
return `\n\nimport ${defaultImport} from '${node.node.source.value}';\n` + | ||||||||
`const {${destructuringAssignment}} = ${defaultImport};\n`; | ||||||||
} while (node === undefined || node.node.loc.start.line <= lineNumber); | ||||||||
assert.fail('Could not find erroneous import statement'); | ||||||||
} | ||||||||
|
||||||||
/* A ModuleJob tracks the loading of a single Module, and the ModuleJobs of | ||||||||
* its dependencies, over time. */ | ||||||||
class ModuleJob { | ||||||||
|
@@ -117,21 +182,19 @@ class ModuleJob { | |||||||
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, ': '); | ||||||||
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, | ||
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.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,14 @@ | ||
module.exports = { | ||
comeOn: 'fhqwhgads', | ||
everybody: 'to the limit', | ||
one: 1, | ||
two: 2, | ||
three: 3, | ||
four: 4, | ||
five: 5, | ||
six: 6, | ||
seven: 7, | ||
eight: 8, | ||
nine: 9, | ||
ten: 10, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { | ||
comeOn, | ||
// everybody, | ||
one, | ||
two, | ||
three, | ||
four, | ||
five, | ||
six, | ||
seven, | ||
eight, | ||
nine, | ||
ten, | ||
} from "./fail.cjs"; |
Uh oh!
There was an error while loading. Please reload this page.