← Back to Releasing JavaScript
Making the switch to native ESM packages
Status: work in progress.
Using ES modules natively in Node
Node.js versions 12 and later support ES modules natively. To switch your whole package to ES modules, include type: module in your package.json:
{
"type": "module"
}
This instructs Node.js to consider all the .js files in your project folder to be ES modules.
Publishing ES modules to npm
There are several approaches here. You could point your main file to the ESM entry point, or use the newer exports.
Using the exports field
Publishing responsibly. Switching to using the
exportsfield, or changing the exports (with the exception of adding new exports), is a breaking change and entails a major version bump in semver.
When using the exports field, you restrict what can be imported from the project.
It's good practice to include
package.jsonas one of the exports, as some build tools depend on being able to read it.
Converting CJS modules to ESM
You can consult the documentation for the differences between ESM and CJS modules. Here are some common rewriting tasks:
Add the file extension to relative import specifiers
Bundlers allow you to import modules without an extension, like in require(). To use native ESM, you'll need to add explicit file extensions. You can write a jscodeshift transform to add it as described in Use code to explore and change JavaScript files.
// CJS:
const toPrecision = require('./util/to-precision');
// ESM (Most bundlers):
import toPrecision from './util/to-precision';
// ESM (Native):
import toPrecision from './util/to-precision.js';
Use import.meta.url instead of __filename, __dirname
Methods in the node:fs module accept URL objects instead of paths:
// CJS:
const fs = require('node:fs/promises');
const path = require('node:path');
const content = await fs.readFile(path.join(__dirname, './some.txt'), 'utf8');
// ESM:
import fs from 'node:fs/promises';
const content = await fs.readFile(new URL('./some.txt', import.meta.url), 'utf8');
Node.js 20.11 also added direct equivalents to __dirname and __filename as import.meta.dirname and import.meta.filename respectively.
Use import attributes to read JSON files
Starting with Node.js 18.20, you can use import attributes to import JSON files directly:
import content from './some.json' with { type: 'json' };
For earlier versions of Node.js, you must read JSON files with fs:
import fs from 'node:fs/promises';
const content = JSON.parse(
await fs.readFile(new URL('./some.json', import.meta.url))
);
Providing fallbacks
ESM + CJS dual package
There are some disadvantages to publishing a dual, ESM + CJS, package.
The dual package hazard
The export from your ESM module is not the same as the export from the CJS module. If your library is both import-ed and require()-d in the same codebase (either directly, or undirectly via a dependency), it can cause a number of effects, collectively named dual package hazard:
- code is imported, run, and bundled twice — once for the ESM version, and once for the CJS version.
- if the library has state (e.g. global configuration), this state will be separate in the ESM and CJS versions.
UMD bundle
TBD. Some online tools, such as Observable, need an UMD bundle.
Further reading
- Node.js documentation on: ESM, Packages
- Pure ESM package by Sindre Sorhus
- Node Modules at War: Why CommonJS and ES Modules Can’t Get Along by Dan Fabulich