-
Notifications
You must be signed in to change notification settings - Fork 29.8k
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
module: improve named cjs import errors #35453
Conversation
Review requested:
|
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 comment
The 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
// Lazy load acorn. | |
if (parseExpressionAt === undefined) { | |
const acorn = require('internal/deps/acorn/acorn/dist/acorn'); |
const { parse } = require('internal/deps/acorn/acorn/dist/acorn'); | ||
const { findNodeAt } = require('internal/deps/acorn/acorn-walk/dist/walk'); | ||
|
||
const code = readFileSync(file, { encoding: 'utf8' }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it OK to readFileSync
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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 readFileSync
for that kind of things.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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 readFileSync
is the way to go.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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 comment
The 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 comment
The 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 { findNodeAt } = require('internal/deps/acorn/acorn-walk/dist/walk'); | ||
|
||
const code = readFileSync(file, { encoding: 'utf8' }); | ||
const parsed = parse(code, { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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 parseExpressionAt
to only part the first few lines of a file but didn't have success (received unexpected token
errors resulting from the import
statements).
@@ -0,0 +1,4 @@ | |||
import abc, { | |||
comeOn as comeOnRenamed, | |||
everybody, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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 (everybody
). I do not understand why it doesn't error out on the renamed import statement. If I swap the order it will error on line 2 (still everybody
).
Any clues why this is the case?
test/fixtures/es-modules/package-cjs-named-error/default-and-renamed-import.mjs
Show resolved
Hide resolved
f0498e2
to
6c22d54
Compare
}); | ||
|
||
const boilerplate = `const { } = ${defaultImport};`; | ||
const destructuringAssignment = totalLength > 80 - boilerplate.length ? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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 comment
The 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.
Codecov Report
@@ Coverage Diff @@
## master #35453 +/- ##
=======================================
Coverage 96.84% 96.84%
=======================================
Files 212 212
Lines 69609 69609
=======================================
Hits 67410 67410
Misses 2199 2199 Continue to review full report at Codecov.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 on the idea
68f2978
to
e7a0b5c
Compare
@MylesBorins @mcollina @Trott is there any interest in landing this change? I'm happy to work on it further. |
@guybedford Does #35249 change something for this PR? |
Can you please make example of the error message before and after this PR? |
Sure. Upon encountering a multi-line named import statement like: import {
comeOn,
everybody,
} from './fail.cjs'; Before:
After:
Diff: --- before-short 2020-10-16 12:54:45.000000000 +0200
+++ after-short 2020-10-16 12:55:05.000000000 +0200
@@ -5,8 +5,9 @@
CommonJS modules can always be imported via the default export, for example using:
import pkg from './fail.cjs';
+const { comeOn, everybody } = pkg;
- at ModuleJob._instantiate (internal/modules/esm/module_job.js:98:21)
- at async ModuleJob.run (internal/modules/esm/module_job.js:143:5)
+ at ModuleJob._instantiate (internal/modules/esm/module_job.js:164:21)
+ at async ModuleJob.run (internal/modules/esm/module_job.js:205:5)
at async Loader.import (internal/modules/esm/loader.js:165:24)
at async Object.loadESM (internal/process/esm_loader.js:68:5) For a single-line statement like: import { comeOn } from './fail.cjs'; nothing changes, it's the following before and after this patch:
Also with this patch, very long import statements that don't fit into an 80 character line are split like this:
|
let start = 0; | ||
let node; | ||
do { | ||
node = findNodeAt(parsed, start); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if findNodeAt
returns undefined
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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 comment
The 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 while
condition.
Have you tried to use findNodeAround
instead of findNodeAt
? It seems to fit better our use case.
node = findNodeAt(parsed, start); | |
node = findNodeAround(parsed, lineNumber, 'ImportDeclaration'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are still some unresolved conversations on this thread that I'd like to be addressed before landing.
809c06a
to
36db7df
Compare
The most serious issue that I must understand before landing is: #35453 (comment) The same happens with the long-multiline test that I just added: Node throws on the second import ( How is that possible? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This gives a better message for users, so it seems good to me.
Approval conditional on the following:
-
If v8 changes the error output for some class of cases we don't notice, can this code ever error itself? It is very important that these decorations can never break so the JS must be written incredibly defensively. Eg what if the JS doesn't parse? What if it is a test framework and there was no JS at that location? What if the v8 output changes slightly for some case? What if it uses language features Acorn doesn't support. It otherwise gives the user a far far worse experience.
-
As this is the first time Acorn is being used in core proper (and not just REPL), it would be good to have some ok from others involved in this integration previously this is a path we want to go down for the project as it sets precedent.
Thank you @guybedford for the input. I will put some more thought and effort into the parsing code to increase its defensiveness. One of the things that immediately came to my mind: I'm currently using the line-number from the V8 error to determine the offending import statement. @bcoe I recall you have added sourcemap support to Node.js and I believe errors are also rewritten when sourcemaps are in use. Could this interfere with this PR? Would be great if you could shed some light on this. |
@ctavan where this might break with source maps would be the extra line of context Node.js adds to stack traces:
Perhaps we could add an additional test, with a transpiled file that has a multi-line import, and make sure it works? I find these a good opportunity to add a regression test for an additional transpiler; perhaps we could use |
@ctavan 👋 let me know when you'd like to dust this off, would be happy to pair. |
@bcoe thanks for chiming in and for offering your help. Now that #35249 landed and named exports of CJS modules are pretty well supported, do we expect this error to be still relevant? I searched open issues for the error message and – apart from my own issue #35259 – only found one issue that pre-date the named CJS import support which landed in v14.13.0/v12.20.0: #32137 (comment) @guybedford could you summarize, under which conditions the error that this PR tries to improve would still be triggered? |
@guybedford sorry for the ping. Can you answer my question off the top of your head? My assumption is that we can just close this PR and the corresponding issue, but getting confirmation from your end would be nice. |
@ctavan with cjs-module-lexer we now have detectable CJS exports being handled ok, so that this message only applies to cases where that detection fails. The rebase would still be useful to users in those specific scenarios I think. Entirely up to you if you want to pick this up again. |
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 nodejs#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: nodejs#35259 Refs: nodejs#35275
Co-authored-by: Antoine du Hamel <duhamelantoine1995@gmail.com>
Co-authored-by: Antoine du Hamel <duhamelantoine1995@gmail.com>
Co-authored-by: Antoine du Hamel <duhamelantoine1995@gmail.com>
Co-authored-by: Antoine du Hamel <duhamelantoine1995@gmail.com>
This reverts commit 22f700c.
c7931bd
to
533560f
Compare
@guybedford since my priorities have shifted a little bit recently I doubt that I'll find the time to complete this PR any time soon. I'll therefore close it and leave it to other folks to pick it up again in case there's interest. |
@ctavan thanks for the update. If you do find time again in future and come across any areas we can improve in future your contributions would always be appreciated. |
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
Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passes