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 exports field, 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.json as 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 fs built-in module accept URL objects instead of paths:

// CJS:
const fs = require('fs/promises');
const path = require('path');

const content = await fs.readFile(path.join(__dirname, './some.txt'), 'utf8');

// ESM:
import fs from 'fs/promises';
const content = await fs.readFile(new URL('./some.txt', import.meta.url), 'utf8');

Use fs to read JSON files

You can't import JSON files directly (for now), so you must read them with fs:

import fs from '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:

UMD bundle

TBD. Some online tools, such as Observable, need an UMD bundle.

Further reading