Skip to content

Adding an option to interpret __esModule as Babel does #40891

Closed
@qnighy

Description

@qnighy

Is your feature request related to a problem? Please describe.

Node.js provides interop between ES Modules and CommonJS Modules in the following ways:

  • CJS → ESM: await import("foo.mjs") works. Since CJS module body is executed synchronously, it is not possible to import an ESM module during CJS module setup.
  • ESM → CJS: an ES module can import a CJS module as usual and the CJS module is automatically wrapped in an ES module.

So basically, the migration should start from the depending side (= application code). In this strategy, each import edge in the module graph will transition in the following manner:

  • Step 1: a CJS module imports a CJS module.
  • Step 2: an ES module imports a CJS module.
  • Step 3: an ES module imports an ES module.

And it is already a widespread practice to write each package in ESM syntax and transform the modules to CJS using Babel or TypeScript (tsc). Node.js and Babel differ in how default imports are treated. If both the importing module and the imported module were in ESM syntax, the behavior will change like:

  • Step 1: a CJS module (transformed by Babel) imports a CJS module (transformed by Babel). The default import will reference the value exported by export default.
  • Step 2: an ES module imports a CJS module (transformed by Babel). The default import will reference the namespace object.
  • Step 3: an ES module imports an ES module. The default import will reference the value exported by export default.

Instead of flipping imports manually, it would be really useful if Node.js can provide a behavior compatible with Babel's behavior.

Describe the solution you'd like

To change the behavior of the CJS compatibility layer. The current behavior is as follows:

  • The value of module.exports in CJS is mapped to the default export in ESM.

I propose to change the behavior:

  • If module.exports.__esModule is truthy, the value of module.exports.default in CJS is mapped to the default export in ESM.
  • Otherwise, the value of module.exports in CJS is mapped to the default export in ESM.

In my understanding, the existing behavior was introduced in #14369. At that time, named exports were not available and the decision was reasonable because we basically want to allow users to access all functionality from CJS modules.

When #35249 was merged to allow named exports/imports, the behavior was not changed as described in the original PR: to maintain compatibility and consistency.

The major edge case this differs from current semantics with bundlers on is that the default export is retained as the full module.exports instance for backwards compat and consistency. This is important for Node.js semantic consistency I think.

I think it's more consistent to support __esModule flag as it provides an accurate embedding of ESM semantics to CJS. However backwards compat is still important; I propose to provide a command-line option to support __esModule instead.

I made a PR which contains the solution described above: #40892

Describe alternatives you've considered

  • Keep the status quo.
    • We can still configure Babel to use importInterop: "node" and fix the code appropriately. However, the user will struggle again if the dependent package migrates to full ESM.
    • TypeScript is not yet fully aware of that pattern either, as far as I understand.
    • There's also a difficulty in macro-based libraries such as styled-components/macro as they expect the specific default import pattern.
  • Change the behavior globally.
    • The change can't be landed in Node.js 16 in this approach, as it is apparently backward-incompatible.
  • Provide a finer-grained option in package.json rather than a command-line option.
    • Option 1: interpret the option in package.json for the imported module
      • It's not effective, because not all packages are frequently maintained.
    • Option 2: interpret the option in package.json for the importing module
      • It will add an extra lookup overhead for CommonJS compatibility layer.

Metadata

Metadata

Assignees

No one assigned

    Labels

    esmIssues and PRs related to the ECMAScript Modules implementation.feature requestIssues that request new features to be added to Node.js.moduleIssues and PRs related to the module subsystem.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions