Skip to content
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 error decoration for cjs named exports for multi-line import statements #35259

Closed
ctavan opened this issue Sep 18, 2020 · 12 comments
Labels
esm Issues and PRs related to the ECMAScript Modules implementation. feature request Issues that request new features to be added to Node.js. stale

Comments

@ctavan
Copy link
Contributor

ctavan commented Sep 18, 2020

  • Version: v14.11.0
  • Platform: Darwin *** 19.6.0 Darwin Kernel Version 19.6.0: Thu Jun 18 20:49:00 PDT 2020; root:xnu-6153.141.1~1/RELEASE_X86_64 x86_64
  • Subsystem: modules

What steps will reproduce the bug?

Failing test can be found here: ctavan@ba9e734

How often does it reproduce? Is there a required condition?

Always.

What is the expected behavior?

When trying to import named exports from a CommonJS module, there's usually a helpful error message which is assembled here:

if (StringPrototypeIncludes(e.message,
' does not provide an export named')) {
const splitStack = StringPrototypeSplit(e.stack, '\n');
const parentFileUrl = splitStack[0];
const childSpecifier = StringPrototypeMatch(e.message, /module '(.*)' does/)[1];
const childFileURL =
await this.loader.resolve(childSpecifier, parentFileUrl);
const format = await this.loader.getFormat(childFileURL);
if (format === 'commonjs') {
const importStatement = splitStack[1];
const namedImports = StringPrototypeMatch(importStatement, /{.*}/)[0];
const destructuringAssignment = StringPrototypeReplace(namedImports, /\s+as\s+/g, ': ');
e.message = `The requested module '${childSpecifier}' is expected ` +
'to be of type CommonJS, which does not support named exports. ' +
'CommonJS modules can be imported by importing the default ' +
'export.\n' +
'For example:\n' +
`import pkg from '${childSpecifier}';\n` +
`const ${destructuringAssignment} = pkg;`;
const newStack = StringPrototypeSplit(e.stack, '\n');
newStack[3] = `SyntaxError: ${e.message}`;
e.stack = ArrayPrototypeJoin(newStack, '\n');
}

What do you see instead?

When using multi-line import statements like:

import {
  comeOn,
  rightNow,
} from './fail.cjs';

the following regex does not match:

const namedImports = StringPrototypeMatch(importStatement, /{.*}/)[0];

and the following error is produced:

TypeError: Cannot read property '0' of null
      at ModuleJob._instantiate (internal/modules/esm/module_job.js:112:77)
      at async ModuleJob.run (internal/modules/esm/module_job.js:137:5)
      at async Loader.import (internal/modules/esm/loader.js:165:24)
      at async rejects.name (file:///***/node/test/es-module/test-esm-cjs-named-error.mjs:56:3)
      at async waitForActual (assert.js:721:5)
      at async rejects (assert.js:830:25),

Additional information

I would love to contribute a fix for this issue, however I need some guidance on how to proceed. The problem I see is that the full error stack only contains the first line of the multi-line import statement:

[
  'file:///***/node/test/fixtures/es-modules/package-cjs-named-error/multi-line.mjs:2',
  '  comeOn,',
  '  ^^^^^^',
  "SyntaxError: The requested module './fail.cjs' does not provide an export named 'comeOn'",
  '    at ModuleJob._instantiate (internal/modules/esm/module_job.js:98:21)',
  '    at async ModuleJob.run (internal/modules/esm/module_job.js:141:5)',
  '    at async Loader.import (internal/modules/esm/loader.js:165:24)',
  '    at async rejects.name (file:///***/node/test/es-module/test-esm-cjs-named-error.mjs:56:3)',
  '    at async waitForActual (assert.js:721:5)',
  '    at async rejects (assert.js:830:25)'
]

So while the goal of the additional error decoration which was added in #33256 seems to be to provide a copy & pastable solution, I don't see how this could be achieved with the error information at hand when the error comes from a multi-line import statement.

Options that come to my mind:

  • Just resort to the original error?
  • Skip the example if it cannot be generated from the error stack, see: ctavan@248eada
  • Produce something like the following, even though it's incomplete:
      import pkg from './fail.cjs';
      const { comeOn } = pkg;
  • Produce the correct example, which would probably involve using an actual parser to read the full failing import statement.

/cc @MylesBorins

@ljharb
Copy link
Member

ljharb commented Sep 18, 2020

cc @nodejs/modules-active-members

@MylesBorins MylesBorins added confirmed-bug Issues with confirmed bugs. esm Issues and PRs related to the ECMAScript Modules implementation. labels Sep 19, 2020
@MylesBorins
Copy link
Member

This is definitely less than ideal. I like the solution in ctavan@248eada as a place to start. We can definitely improve the regex and support multi-line in the future.

@ctavan
Copy link
Contributor Author

ctavan commented Sep 19, 2020

Great, I‘ll provide a PR tomorrow.

ctavan added a commit to ctavan/node that referenced this issue Sep 20, 2020
The node process crashes when trying to parse a multiline import
statement for named exports of a CommonJS module:

    TypeError: Cannot read property '0' of null
          at ModuleJob._instantiate (internal/modules/esm/module_job.js:112:77)
          at async ModuleJob.run (internal/modules/esm/module_job.js:137:5)
          at async Loader.import (internal/modules/esm/loader.js:165:24)
          at async rejects.name (file:///***/node/test/es-module/test-esm-cjs-named-error.mjs:56:3)
          at async waitForActual (assert.js:721:5)
          at async rejects (assert.js:830:25),

The reason is that the regexp that is currently used to decorate the
original error fails for multi line import statements.

Unfortunately the undecorated error stack only contains the single line
which causes the import to fail:

    file:///***/node/test/fixtures/es-modules/package-cjs-named-error/multi-line.mjs:2
      comeOn,
      ^^^^^^
    SyntaxError: The requested module './fail.cjs' does not provide an export named 'comeOn'
        at ModuleJob._instantiate (internal/modules/esm/module_job.js:98:21)
        at async ModuleJob.run (internal/modules/esm/module_job.js:141:5)
        at async Loader.import (internal/modules/esm/loader.js:165:24)
        at async rejects.name (file:///***/node/test/es-module/test-esm-cjs-named-error.mjs:56:3)
        at async waitForActual (assert.js:721:5)
        at async rejects (assert.js:830:25)

Hence, for multiline import statements we cannot create an equivalent
piece of code that uses default import followed by an object
destructuring assignment.

In any case the node process should definitely not crash. So until we
have a more sophisticated way of extracting the entire problematic
multiline import statement, show the code example only for single-line
imports where the current regexp approach works well.

Refs: nodejs#35259
MylesBorins pushed a commit that referenced this issue Sep 22, 2020
The node process crashes when trying to parse a multiline import
statement for named exports of a CommonJS module:

    TypeError: Cannot read property '0' of null
          at ModuleJob._instantiate (internal/modules/esm/module_job.js:112:77)
          at async ModuleJob.run (internal/modules/esm/module_job.js:137:5)
          at async Loader.import (internal/modules/esm/loader.js:165:24)
          at async rejects.name (file:///***/node/test/es-module/test-esm-cjs-named-error.mjs:56:3)
          at async waitForActual (assert.js:721:5)
          at async rejects (assert.js:830:25),

The reason is that the regexp that is currently used to decorate the
original error fails for multi line import statements.

Unfortunately the undecorated error stack only contains the single line
which causes the import to fail:

    file:///***/node/test/fixtures/es-modules/package-cjs-named-error/multi-line.mjs:2
      comeOn,
      ^^^^^^
    SyntaxError: The requested module './fail.cjs' does not provide an export named 'comeOn'
        at ModuleJob._instantiate (internal/modules/esm/module_job.js:98:21)
        at async ModuleJob.run (internal/modules/esm/module_job.js:141:5)
        at async Loader.import (internal/modules/esm/loader.js:165:24)
        at async rejects.name (file:///***/node/test/es-module/test-esm-cjs-named-error.mjs:56:3)
        at async waitForActual (assert.js:721:5)
        at async rejects (assert.js:830:25)

Hence, for multiline import statements we cannot create an equivalent
piece of code that uses default import followed by an object
destructuring assignment.

In any case the node process should definitely not crash. So until we
have a more sophisticated way of extracting the entire problematic
multiline import statement, show the code example only for single-line
imports where the current regexp approach works well.

Refs: #35259

PR-URL: #35275
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Myles Borins <myles.borins@gmail.com>
Reviewed-By: Rich Trott <rtrott@gmail.com>
MylesBorins pushed a commit that referenced this issue Sep 24, 2020
The node process crashes when trying to parse a multiline import
statement for named exports of a CommonJS module:

    TypeError: Cannot read property '0' of null
          at ModuleJob._instantiate (internal/modules/esm/module_job.js:112:77)
          at async ModuleJob.run (internal/modules/esm/module_job.js:137:5)
          at async Loader.import (internal/modules/esm/loader.js:165:24)
          at async rejects.name (file:///***/node/test/es-module/test-esm-cjs-named-error.mjs:56:3)
          at async waitForActual (assert.js:721:5)
          at async rejects (assert.js:830:25),

The reason is that the regexp that is currently used to decorate the
original error fails for multi line import statements.

Unfortunately the undecorated error stack only contains the single line
which causes the import to fail:

    file:///***/node/test/fixtures/es-modules/package-cjs-named-error/multi-line.mjs:2
      comeOn,
      ^^^^^^
    SyntaxError: The requested module './fail.cjs' does not provide an export named 'comeOn'
        at ModuleJob._instantiate (internal/modules/esm/module_job.js:98:21)
        at async ModuleJob.run (internal/modules/esm/module_job.js:141:5)
        at async Loader.import (internal/modules/esm/loader.js:165:24)
        at async rejects.name (file:///***/node/test/es-module/test-esm-cjs-named-error.mjs:56:3)
        at async waitForActual (assert.js:721:5)
        at async rejects (assert.js:830:25)

Hence, for multiline import statements we cannot create an equivalent
piece of code that uses default import followed by an object
destructuring assignment.

In any case the node process should definitely not crash. So until we
have a more sophisticated way of extracting the entire problematic
multiline import statement, show the code example only for single-line
imports where the current regexp approach works well.

Refs: #35259

PR-URL: #35275
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Myles Borins <myles.borins@gmail.com>
Reviewed-By: Rich Trott <rtrott@gmail.com>
@MylesBorins MylesBorins changed the title module: error decoration for cjs named exports not working for multi-line import statements module: improve error decoration for cjs named exports for multi-line import statements Sep 24, 2020
@MylesBorins MylesBorins added feature request Issues that request new features to be added to Node.js. and removed confirmed-bug Issues with confirmed bugs. labels Sep 24, 2020
@MylesBorins
Copy link
Member

I've changed the title of this issue to track having to improve the existing message which is not giving the hint for a multiline import

@ctavan
Copy link
Contributor Author

ctavan commented Sep 25, 2020

@MylesBorins if you have a rough idea of how to extract the multiline import in that place then let me know and I'm happy to work on a patch.

@MylesBorins
Copy link
Member

@ctavan honestly the best idea I can come up with is using acorn to parse the entire file, but that seems like it might be overkill

guybedford pushed a commit to guybedford/node that referenced this issue Sep 28, 2020
The node process crashes when trying to parse a multiline import
statement for named exports of a CommonJS module:

    TypeError: Cannot read property '0' of null
          at ModuleJob._instantiate (internal/modules/esm/module_job.js:112:77)
          at async ModuleJob.run (internal/modules/esm/module_job.js:137:5)
          at async Loader.import (internal/modules/esm/loader.js:165:24)
          at async rejects.name (file:///***/node/test/es-module/test-esm-cjs-named-error.mjs:56:3)
          at async waitForActual (assert.js:721:5)
          at async rejects (assert.js:830:25),

The reason is that the regexp that is currently used to decorate the
original error fails for multi line import statements.

Unfortunately the undecorated error stack only contains the single line
which causes the import to fail:

    file:///***/node/test/fixtures/es-modules/package-cjs-named-error/multi-line.mjs:2
      comeOn,
      ^^^^^^
    SyntaxError: The requested module './fail.cjs' does not provide an export named 'comeOn'
        at ModuleJob._instantiate (internal/modules/esm/module_job.js:98:21)
        at async ModuleJob.run (internal/modules/esm/module_job.js:141:5)
        at async Loader.import (internal/modules/esm/loader.js:165:24)
        at async rejects.name (file:///***/node/test/es-module/test-esm-cjs-named-error.mjs:56:3)
        at async waitForActual (assert.js:721:5)
        at async rejects (assert.js:830:25)

Hence, for multiline import statements we cannot create an equivalent
piece of code that uses default import followed by an object
destructuring assignment.

In any case the node process should definitely not crash. So until we
have a more sophisticated way of extracting the entire problematic
multiline import statement, show the code example only for single-line
imports where the current regexp approach works well.

Refs: nodejs#35259

PR-URL: nodejs#35275
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Myles Borins <myles.borins@gmail.com>
Reviewed-By: Rich Trott <rtrott@gmail.com>
@ctavan
Copy link
Contributor Author

ctavan commented Sep 28, 2020

@MylesBorins hmm, I was hoping for a simpler solution.

While digging through the error decoration code I realized that @bcoe you have touched this recently with the sourcemap patch (458677f5ef2). I didn't check the sourcemap implementation in detail, but maybe it also involves rewriting error messages that show the offending (mapped) lines of code? Any ideas from that perspective?

@GeoffreyBooth
Copy link
Member

@ctavan honestly the best idea I can come up with is using acorn to parse the entire file, but that seems like it might be overkill

Is this really overkill? The process is exiting, so the few milliseconds it takes for Acorn to parse the file shouldn’t matter too much I’d think. And this would be a solid solution without the brittleness of a regex.

@ctavan
Copy link
Contributor Author

ctavan commented Sep 28, 2020

@GeoffreyBooth I’ll explore this over the next few days to see how much complexity this will add.

codebytere pushed a commit that referenced this issue Oct 1, 2020
The node process crashes when trying to parse a multiline import
statement for named exports of a CommonJS module:

    TypeError: Cannot read property '0' of null
          at ModuleJob._instantiate (internal/modules/esm/module_job.js:112:77)
          at async ModuleJob.run (internal/modules/esm/module_job.js:137:5)
          at async Loader.import (internal/modules/esm/loader.js:165:24)
          at async rejects.name (file:///***/node/test/es-module/test-esm-cjs-named-error.mjs:56:3)
          at async waitForActual (assert.js:721:5)
          at async rejects (assert.js:830:25),

The reason is that the regexp that is currently used to decorate the
original error fails for multi line import statements.

Unfortunately the undecorated error stack only contains the single line
which causes the import to fail:

    file:///***/node/test/fixtures/es-modules/package-cjs-named-error/multi-line.mjs:2
      comeOn,
      ^^^^^^
    SyntaxError: The requested module './fail.cjs' does not provide an export named 'comeOn'
        at ModuleJob._instantiate (internal/modules/esm/module_job.js:98:21)
        at async ModuleJob.run (internal/modules/esm/module_job.js:141:5)
        at async Loader.import (internal/modules/esm/loader.js:165:24)
        at async rejects.name (file:///***/node/test/es-module/test-esm-cjs-named-error.mjs:56:3)
        at async waitForActual (assert.js:721:5)
        at async rejects (assert.js:830:25)

Hence, for multiline import statements we cannot create an equivalent
piece of code that uses default import followed by an object
destructuring assignment.

In any case the node process should definitely not crash. So until we
have a more sophisticated way of extracting the entire problematic
multiline import statement, show the code example only for single-line
imports where the current regexp approach works well.

Refs: #35259

PR-URL: #35275
Backport-PR-URL: #35385
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Myles Borins <myles.borins@gmail.com>
Reviewed-By: Rich Trott <rtrott@gmail.com>
@ctavan
Copy link
Contributor Author

ctavan commented Oct 1, 2020

@GeoffreyBooth @MylesBorins @ljharb @guybedford I have prepared a pull request over at #35453 which uses acorn to parse the offending file to always produce an equivalent code example.

Since I'm still new to the Node.js codebase I have a ton of questions w.r.t. my code which I have added as inline comments on the PR. Would be super happy to receive your feedback over there.

ctavan added a commit to ctavan/node that referenced this issue Oct 16, 2020
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
joesepi pushed a commit to joesepi/node that referenced this issue Jan 8, 2021
The node process crashes when trying to parse a multiline import
statement for named exports of a CommonJS module:

    TypeError: Cannot read property '0' of null
          at ModuleJob._instantiate (internal/modules/esm/module_job.js:112:77)
          at async ModuleJob.run (internal/modules/esm/module_job.js:137:5)
          at async Loader.import (internal/modules/esm/loader.js:165:24)
          at async rejects.name (file:///***/node/test/es-module/test-esm-cjs-named-error.mjs:56:3)
          at async waitForActual (assert.js:721:5)
          at async rejects (assert.js:830:25),

The reason is that the regexp that is currently used to decorate the
original error fails for multi line import statements.

Unfortunately the undecorated error stack only contains the single line
which causes the import to fail:

    file:///***/node/test/fixtures/es-modules/package-cjs-named-error/multi-line.mjs:2
      comeOn,
      ^^^^^^
    SyntaxError: The requested module './fail.cjs' does not provide an export named 'comeOn'
        at ModuleJob._instantiate (internal/modules/esm/module_job.js:98:21)
        at async ModuleJob.run (internal/modules/esm/module_job.js:141:5)
        at async Loader.import (internal/modules/esm/loader.js:165:24)
        at async rejects.name (file:///***/node/test/es-module/test-esm-cjs-named-error.mjs:56:3)
        at async waitForActual (assert.js:721:5)
        at async rejects (assert.js:830:25)

Hence, for multiline import statements we cannot create an equivalent
piece of code that uses default import followed by an object
destructuring assignment.

In any case the node process should definitely not crash. So until we
have a more sophisticated way of extracting the entire problematic
multiline import statement, show the code example only for single-line
imports where the current regexp approach works well.

Refs: nodejs#35259

PR-URL: nodejs#35275
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Myles Borins <myles.borins@gmail.com>
Reviewed-By: Rich Trott <rtrott@gmail.com>
ctavan added a commit to ctavan/node that referenced this issue Jan 29, 2021
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
@github-actions
Copy link
Contributor

There has been no activity on this feature request for 5 months and it is unlikely to be implemented. It will be closed 6 months after the last non-automated comment.

For more information on how the project manages feature requests, please consult the feature request management document.

@github-actions github-actions bot added the stale label Mar 18, 2022
@github-actions
Copy link
Contributor

There has been no activity on this feature request and it is being closed. If you feel closing this issue is not the right thing to do, please leave a comment.

For more information on how the project manages feature requests, please consult the feature request management document.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
esm Issues and PRs related to the ECMAScript Modules implementation. feature request Issues that request new features to be added to Node.js. stale
Projects
Development

Successfully merging a pull request may close this issue.

4 participants