Converting CommonJS to ES Modules
Node.js has always supported Common.js as the module system.
When building a package we use modules.export
to define the elements in the package that can be consumed by other modules:
This is the traditional way of defining modules in Node.js:
Ecmascript developed a module system as a standard way to import and export
When building a package we use export
to define the elements in the package that can be consumed by other modules:
However, Node adopted the module specification from Ecmascript 2015 (ES6) in a gradual way.
ES Modules where first introduced as an experimental feature that required a flag in Node 8.5.0 (September 2017)
It spent a long time in the experimental phase and it wasn't until Node 21.2.0 (October 2021) that it was declared stable.
Defining the module in ES6, that would work in Node would look like this:
And importing it would look like this:
These two module systems are incompatible but whether you can import CommonJS modules into ESM code will depend on what version of Node.js you're using.
This post will describe some aspects of the conversion process and how to work with both module systems in the same codebase.
Converting CommonJS to ES Modules #
If you own the code that you're working with, the simplest way to change the syntax for exports and imports.
Replace the import statement with the equivalent ESM import statement:
Instead of the CommonJS export statementUse the ESM export statement in the function declaration:
If you’re using package.json, you’ll need to make a few adjustments to support ESM. We add a type
field to the package.json file and set it to module
and the exports
field to indicate the entry point for the ESM package. The leading ./
in ESM is necessary as every reference has to use the full pathname, including directory and file extension.
The exports
declaration is a modern alternative to main
in that it gives authors the ability to clearly define the public interface for their package by allowing multiple entry points, supporting conditional entry resolution between environments, and preventing other entry points outside of those defined in "exports".
Another way to tell Node to run the file in ESM is to use the .mjs
file extension. This is great if you want to update a single file to ESM. But if your goal is to convert your entire code base, it’s easier to update the type in your package.json.
Other changes #
Since JavaScript inside an ESM will automatically run in strict mode, you can remove all instances of "use strict"; from your code base.
CommonJS also supported a handful of built-in globals that do not exist in ESM, such as __dirname
and __filename
.
This is what we would normally do to get the directory and file name:
Starting with Node 20.11 we can use the import.meta
object to get the directory and file name:
Using file name extensions #
Another way to tell Node to run the file in to use specific extensions for each type of file:
- If your file is an ESM module, use the
.mjs
file extension - If your file is a CommonJS module, use the
.cjs
file extension.
If your goal is to convert your entire code base, it’s easier to update the type
field in your package.json. However, if you're updating a few files, you can use the file extension to tell Node what type of modules they use.
Importing ESM in CommonJS: Dynamic Imports #
Dynamic import() introduces a new function-like form of import that caters to additional use cases like the ones below:
- On-demand module import
- Conditional module import
- Compute the module specifier at runtime
- Import a module from within a regular script (as opposed to a module)
The function-like syntax, import(moduleSpecifier)
, returns a promise for the requested module's namespace object, which is created after fetching, instantiating, and evaluating the module and all its dependencies.
Here’s how to dynamically import and use the ./utils.mjs
module:
Because import()
returns a promise, we can use async/await
to write our code more concisely:
Warning
Although import()
looks like a function call, it is specified as syntax that just happens to use parentheses (similar to super()
). This means that import
doesn’t inherit from Function.prototype
so you can't use any of the function prototype's methods like call
, apply
, or bind
.
Mix and Match Imports #
When working with ESM code you can use the import syntax for both ESM and CommonJS modules.
Using these two module definitions:
You can import modules from both systems in the same file using the import
statement.
This solution will work with existing code in ESM codebases, making your code future proof when your dependencies are updated to ESM (with only renaming the file extension in the import statements and changing how you call the exported methods).