The Node documentation on ECMAScript Modules makes recommendations for "dual packages" that provide both CommonJS scripts and ES module scripts to users.
Specifically, they suggest transpiling ES module scripts to CommonJS, then using an "ES module wrapper", an ES module script that imports from CJS and re-exports its CJS named exports as ESM named exports.
// ./node_modules/pkg/wrapper.mjs
import cjsModule from './index.cjs';
export const name = cjsModule.name;
My question is: is it possible to do this the opposite way, providing my scripts in ES module format, but adding a simple wrapper file in CommonJS allowing CJS users to require
it?
For my part, I can't see a way to do this in synchronous code. It appears to me that CommonJS scripts can only import ES modules via asynchronous dynamic import()
.
(async () => {
const {foo} = await import('./foo.mjs');
})();
But that means my CJS script can't export anything that depends on foo
.
There is no way to synchronously import ESM modules. CJS exports are synchronous, but ESM imports are inherently asynchronous.
In CommonJS,
require()
is synchronous; it doesn't return a promise or call a callback.require()
reads from the disk (or perhaps even from the network), and then immediately runs the script, which may itself do I/O or other side effects, and then returns whatever values were set onmodule.exports
.In ESM, the module loader runs in asynchronous phases. In the first phase, it parses the script to detect calls to import and export without running the imported script. In the parsing phase, the ESM loader can immediately detect a typo in named imports and throw an exception without ever actually running the dependency code.
The ESM module loader then asynchronously downloads and parses any scripts that you imported, and then scripts that your scripts imported, building out a “module graph” of dependencies, until eventually it finds a script that doesn’t import anything. Finally, that script is allowed to execute, and then scripts that depend on that are allowed to run, and so on.
It isn't even necessarily possible to transpile ESM to CJS in all cases. For example, ESM can use top-level await, and CJS modules can't. As a result, there's no way to translate this ESM code into CJS:
export const foo = await fetch('./data.json');
CJS and ESM are completely different animals.