To use an ESM Node.js module in your own, you need to use
import. But node can only use import inside ESM modules, so you need to convert your own CommonJS modules to ESM.
import and converting it to
require() with Babel does not count!
Otherwise you will get an error like this:
SyntaxError: Cannot use import statement outside a module
So, how do you migrate your code to use
import instead of
First, use at least Node.js 12.17 so that ESM modules are available without the
Next, get your own code to run as an ESM module. Either rename your files to the
.mjs extension or add
type: module at the top level in
With the first option only the renamed files run as ESM modules. With the second option, you need to rename to
.cjs the files you wish to run as CommonJS modules.
The next issue is that you cannot use the global
require() inside an ESM module. Node.js will exit and print an error like:
ReferenceError: require is not defined in ES module scope, you can use import instead
So in order to be able to
import an ESM-only module, you need to replace every single
require() call, even those that import CommonJS modules.
require(), there’s just one issue that is likely to bite you: while
require() supports a directory name, import only accepts the path to a single file. When use
require() with a directory name,
require() imports the file named
index.js in that directory. So you need to replace
const myModule = require('../directory');
import myModule from '../directory/index.js';
If the directory contains a
package.json file, require will look at the
package.json in that directory to find which file to import, so you need to replace the directory name with the file indicated in
package.json. Usually it’s the file in the
If you use dynamic requires (hopefully you are in the minority, but for some framework-like tools it is the sad truth as they import different modules depending on the environment), you can replace them with dynamic
The problem is that
import() is async which will make your function change return type to a Promise. This propagates the
async up the function call chain, and can lead to pretty invasive API changes depending on how your code uses dynamic
require(). You might be forced to use top-level await, but ESLint does not support it out of the box.
To work around this issue you can use
createRequire(). This function takes the current module URL (
import.meta.url) and returns a function that you can use to synchronously include other modules. This avoids having to propagate async functions everywhere.
The final issue is often related to dynamic imports and it is the usage of
__dirname variable. In CommonJS modules, it gives you the absolute path of the directory containing the currently executing file, so it is useful to be able to include files relative to the current file no matter from which directory the node process starts.
__dirname does not work in ESM modules. You will get an error like:
__dirname is not defined
To solve this you’ve got two possibilities. Both use
import.meta.url. This is a special variable that’s available inside ESM modules which contains the URL of the module file. The best solution in most cases is to replace the file path constructed with
__dirname with an URL constructed with
import.meta.url, as many file APIs have been updated to accept an URL. This is less verbose. The alternative is to build the exact equivalent of
import.meta.url. The recipe for doing so is the first search result on many search engines. To avoid an error in ESLint with
import.meta.url you need to change the language level to 2021 in the ESLint configuration.
The three main problems when converting a CommonJS module to ESM are directory imports,
require() calls and
__dirname usage. If your code base uses a lot of
require() calls to dynamically constructed paths, you might be in for a lot of work. Good luck!