nodejs/modules

Feature: Transparent interoperability for ESM importing CommonJS

GeoffreyBooth opened this issue · 5 comments

  • ESM files can import CommonJS files and packages using full ES2015 syntax, the same as if the module to be imported was ESM.
  • ESM importing a member of a CommonJS module is understood by bundlers for the purpose of tree-shaking.
  • A CommonJS module that does things that are impossible in ESM (assuming there still are any such things) can still be imported by an ESM file.
// a.js
module.exports = { b: 10 };

// b.js
import { b } from './a.js';
console.log(b); // 10

Use cases 8, 10, 12, 18, 39, 20, 40, 41, 42, 47, 48.

I know this is a rather huge feature, so it’s probably best to break out the components that make it up into issues of their own.

A CommonJS module that does things that are impossible in ESM (assuming there still are any such things) can still be imported by an ESM file.

This takes us back to the late-binding issue and the fact that CJS can do things that ESM cannot.

The simplest example I can think of to demonstrate this is:

// a.mjs
export const foo = 15;
// b.mjs
export const bar = 15;
// c.js
module.exports = Math.random() > 0.5 ? require('./a.mjs') : require('./b.mjs');
// d.mjs
import { foo } from './c.js'; // how would this even work without evaluating c.js?

This is entirely valid code in Node.js today but late binding means that this won't work. Supporting named exports like this from cjs would mean that ESM exports are not statically analysable (which was a design goal). This doesn't even go into live bindings (this is hard even if both cjs modules have the same exported names).

This means for example that an http2 server cannot use http2 push to figure out statically what files a module requires and push them to the client eliminating the need for bundling entirely which is otherwise possible. It's also unclear how a bundler would deal with this (include both files I presume).

Note that top-level await has exactly the same late-binding issue.

If we ship ESM that supports late bindings and .cjs imports with the default loader then regardless of spec compliance we also have to discuss what we're potentially losing by it.

@benjamn Don't forget support for a feature doesn't necessarily mean support for it by default. Many features may not be suitable/doable for default Node, but it's important we think of ways they could be accomplished (through things like loaders) if needed.

late binding means that this won’t work. Supporting named exports like this from cjs would mean that ESM exports are not statically analysable (which was a design goal)

Another thought might be that maybe in hybrid ESM-mixed-with-CommonJS scenarios, static analysis only covers what it can, e.g. the ESM parts of the module graph. I would presume this is how Rollup works today: it does tree-shaking for the parts of the tree that use import and export statements, and leaves alone modules imported via require. (Please correct me if I’m wrong or oversimplifying.) I think from the perspective of a user, users would expect Node to perform as well as Rollup does currently (e.g. with limitations when it comes to CommonJS) but not better.

References:

fwiw, rollup (and webpack, and everyone else) are perfectly capable of doing treeshaking with CJS, they just choose not to implement the support for it.

Adding modules-agenda to propose we discuss potentially splitting up this feature.