nodejs / nodejs/node

Improve DX of importing ESM-compiled-to-CJS from native ESM

Abierto
#50,981 25 comentarios 7 reacciones 0 asignados Ver en GitHub

Nadie ha tomado este issue todavía.

esm feature request module never-stale
Lenguaje dominante
JavaScript
Estrellas
122k
Forks
37.4k
Merge medio
4 d 3 h
PR fusionados (30 d)
272

Descripción

What is the problem this feature will solve?

The current ESM-CJS interop has two problems when it comes to files authored as ESM and then compiled to CJS

Problem 1

Due to how ESM works, Node.js needs to statically detect the list of bindings exported form CJS modules. This static analysis is performed by cjs-module-lexer, which recognizes patterns produced by popular tools at the time that this helper library was authored.

This has two main limitations:

In addition to these limitations, cjs-module-lexer has to perform a full lexing pass on the imported CJS file, which has a non-zero cost.

Problem 2

The default export exposed by Node.js for CJS files is always module.exports. This was a good choice at the beginning, to give a way of importing CJS from ESM, but now that Node.js detects named exports it's increasing friction when using the two modules systems together.

Consider this library, authored as ESM and published as CJS:

// as ESM
export default function circ(radius) {
  return radius * PI;
}
export const PI = 3.14;
// compiled to CJS and published
exports.__esModule = true;
exports.default = function circ(radius) {
  return radius * PI;
};
const PI = exports.PI = 3.14;

When importing this library from ESM, the named exports PI "just works":

import { PI } from "lib";

console.log(PI);

However, to use the default export you need to first import the library and then, separately, grab the default export from it:

import _lib from "lib";
const circ = _lib.default;

console.log(circ);

Developers sometimes try to rewrite it to use the "destructuring import" syntax with default as if it was a named import, but it obviously still points to module.exports:

import { default as circ } from "lib";

Unfortunately Node.js cannot change it's behavior and use exports.default as the default export (when exports.__esModule is defined to signal that the CJS file was originally an ES module, as every single other tool does) for backwards compatibility.

What is the feature you are proposing to solve the problem?

Node.js should have some sort of comment/directive at the beginning of the file to let build tools communicate what is the list of exports that a file should expose.

For example, the library example above could be compiled by tools to the following:

"exports:default,PI";

exports.__esModule = true;
exports.default = function circ(radius) {
  return radius * PI;
};
const PI = exports.PI = 3.14;

Then Node.js would know that when imported as ESM this file should have a default export (pointing to exports.default) and a PI export (pointing to exports.PI). Tools would be free to change their output code, and the "exports:..." directive would give an unambiguous and easy-to-parse signal to Node.js about the original intention of the code author.

From a tool author perspective, I have a few opinion about how this should work more in details:

  • "exports:foo,bar"; should cause the module to only have foo and bar exports, and not a default export pointing to module.exports. This is so that adding a default export to the module will not be a breaking change.
  • the list of exports need a separator (I'm using , just because it looks nice), but , could also appear in an export name. For this reason, all \s and ,s need to be escaped:
    // input
    const foo = 3;
    export { foo as "abc \\ , def", foo };
    
    // CJS output
    "exports:abc \\\\ \\, def,foo"
    const foo = exports["abc \\ ,"] = exports.foo = 3;
    
  • it would be great if there was still a way to say "expose module.exports as the default export", so that tools could start emitting the new directive without breaking changes (and then they would stop enabling that flag when the compiled library is ready for a major release). For example:
    "exports:abc,def"
    // means
    export const abc = exports.abc;
    export const def = exports.def;
    
    "exports!:abc,def"
    // means
    export const abc = exports.abc;
    export const def = exports.def;
    export default module.exports;
    

Additionally, I noticed that cjs-module-exports also returns a list of modules re-exported with export * from. This is necessary for Node.js to get the list of re-exported bindings. We would need to have a separate directive for that. For example:

export const foo = 1, bar = 2;
export * from "./dep.js";
export * from "mod";

// becomes

"exports:foo,bar";
"exports*:./dep.js,mod";
What alternatives have you considered?

Instead of a directive this could be a comment, like for linking source maps. However, it is important that this comment/directive must be at the beginning of the file, so that Node.js doesn't need to scan/parse the whole file to find it.

Somebody was already thinking about this in the past (I vaguely remember a discussion about it in a Babel issue with a Node.js collaborator), but I cannot find any references to it.


Some people that might be interested in the discussion:
@guybedford, @lukastaegert (Rollup), @kdy1 (SWC), @TheLarkInn (Webpack), @patak-dev (Vite), @evanw (esbuild), @andrewbranch (TypeScript) me (Babel)

Guía de contribución

Abrir la guía de contribución

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Línea de trabajo

Comienza revisando la interoperabilidad ESM-to-CJS de Node.js y el papel de cjs-module-lexer descrito en el issue. Compara las directivas exports y exports* propuestas con el comportamiento documentado de default-export y re-export. Done debe incluir un formato de directiva acordado que evite el escaneo completo de archivos y preserve los requisitos de compatibilidad indicados.

Escrito por el modelo de indexación a partir del texto del issue.

Evaluación

Stack tecnológico
javascript, node.js
Área
backend
Tipo de issue
Nueva funcionalidad
Dificultad
5/5
Tiempo estimado
Más de una semana
Estado de actividad
Estancado
Claridad
Bastante claro
Aptitud para principiantes
25/100

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.