CommonJS and ESM modules interoperability in NodeJS
Introduction
NodeJS supports two types of module systems, CommonJS, and ESM (also known as ECMAScript modules or ES6 modules). You might have come across these terms around the internet and also due to errors being thrown at your face like the following.
Uncaught SyntaxError: Cannot use import statement outside a module
ReferenceError: require is not defined
Error [ERR_REQUIRE_ESM]: require() of ES Module not supported
NodeJS only supported CommonJS modules in the early days, then from around v8.x
NodeJS started experimental support for ESM and things began to become a bit complex.
Given these two types of modules, interoperability came into question.
Let's look at how to resolve these kinds of errors and work out the kinks of using ESM and CommonJS together.
Importing CommonJS in ESM
ECMAScript modules are the official standard format to package JavaScript code for reuse.
You don't need to worry about which type of module you are importing when using ESM. Because ESM supports importing CommonJS modules using an import
statement.
Let's create two files, one for CommonJS and another for ESM.
// cjs.cjs
const name = "Darth Vader";
const ability = () => {
return "Can drive spaceship";
};
module.exports = { name, ability };
// esm.mjs
const name = "Messi";
const ability = () => {
return "Can kick balls";
};
export default { name, ability };
export const extra = "He won the world cup";
Notice that we are using .cjs
and .mjs
extensions for the respective modules.
If the type
property in package.json
is omitted or set to commonjs
we can use .js
instead of .cjs
and use .mjs
for the files we want to behave as ESM.
If the type
property in package.json
is set to module
we can use .js
instead of .mjs
and use .cjs
for the files we want to behave as CommonJS.
// esm-consumer.mjs
// importing esm in esm
import messi from "./esm.mjs";
console.log(messi.name, messi.ability());
// importing cjs in esm
import darthVader from "./cjs.cjs";
console.log(darthVader.name, darthVader.ability());
As you can see in the example above, we can import the CommonJS modules using the import
statement just like if we had imported an ESM module.
We can also destructure the imported object from CommonJS just like we destructure an import from ESM.
// importing cjs in esm
import { name, ability } from "./cjs.cjs";
Importing ESM in CommonJS
CommonJS doesn't support importing ESM modules with require
statements. This is because ES modules have asynchronous execution.
Instead, we can use dynamic import()
statements to import ESM in CommonJS. Also, note that the import()
statement is not only limited to being used in CommonJS modules, it can be used in ESM modules too.
This import()
call is not the same as the import
statement in ESM modules, only the spelling is identical.
import()
returns a promise and the default
and named exports can be accessed by destructuring the resolved value of the promise.
// importing cjs in cjs
const darthVader = require("./cjs.cjs");
console.log(darthVader.name, darthVader.ability());
// importing esm in cjs
import("./esm.mjs").then(({ default: messi, extra }) => {
console.log(messi.name, messi.ability(), extra);
});
You might get errors while importing libraries like lodash-es
in your CommonJS files because those libraries export an ESM module. Use dynamic import()
calls for these libraries instead of require()
to fix those errors.
Conclusion
I hope this article helped you in making sense of the two module systems in NodeJS and their interoperability. Thanks for reading!