Pure ESM packages

👋 FYI, this note is over 6 months old. Some of the content may be out of date.
On this page

The package linked to from here is now pure ESM. It cannot be require()'d from CommonJS.

This means you have the following choices:

  1. Use ESM yourself. (preferred)
    Use import foo from 'foo' instead of const foo = require('foo') to import the package. You also need to put "type": "module" in your package.json and more. Follow the below guide.
  2. If the package is used in an async context, you could use await import(…) from CommonJS instead of require(…).
  3. Stay on the existing version of the package until you can move to ESM.

You also need to make sure you’re on the latest minor version of Node.js. At minimum Node.js 12.20, 14.14, or 16.0.

I would strongly recommend moving to ESM. ESM can still import CommonJS packages, but CommonJS packages cannot import ESM packages synchronously.

ESM is natively supported by Node.js 12 and later.

You can read more about my ESM plans.

My repos are not the place to ask ESM/TypeScript/Webpack/Jest/ts-node/CRA support questions.

FAQ Jump to heading

How can I move my CommonJS project to ESM? Jump to heading

  • Add "type": "module" to your package.json.
  • Replace "main": "index.js" with "exports": "./index.js" in your package.json.
  • Update the "engines" field in package.json to Node.js 12: "node": "^12.20.0 || ^14.13.1 || >=16.0.0".
  • Remove 'use strict'; from all JavaScript files.
  • Replace all require()/module.export with import/export.
  • Use only full relative file paths for imports: import x from '.';import x from './index.js';.
  • If you have a TypeScript type definition (for example, index.d.ts), update it to use ESM imports/exports.
  • Optional but recommended, use the node: protocol for imports.

Sidenote: If you’re looking for guidance on how to add types to your JavaScript package, check out my guide.

Can I import ESM packages in my TypeScript project? Jump to heading

Yes, but you need to convert your project to output ESM. See below.

How can I make my TypeScript project output ESM? Jump to heading

  • Add "type": "module" to your package.json.
  • Replace "main": "index.js" with "exports": "./index.js" in your package.json.
  • Update the "engines" field in package.json to Node.js 12: "node": "^12.20.0 || ^14.13.1 || >=16.0.0".
  • Add "module": "ES2020" to your tsconfig.json.
  • Use only full relative file paths for imports: import x from '.';import x from './index.js';.
  • Remove namespace usage and use export instead.
  • Optional but recommended, use the node: protocol for imports.
  • You must use a .js extension in relative imports even though you’re importing .ts files.

If you use ts-node, follow this guide.

How can I import ESM in Electron? Jump to heading

Electron doesn’t yet support ESM natively.

You have the following options:

  1. Stay on the previous version of the package in question.
  2. Bundle your dependencies with Webpack into a CommonJS bundle.
  3. Use the esm package.

I’m having problems with ESM and Webpack Jump to heading

The problem is either Webpack or your Webpack configuration. First, ensure you are on the latest version of Webpack. Please don’t open an issue on my repo. Try asking on Stack Overflow or open an issue the Webpack repo.

I’m having problems with ESM and Next.js Jump to heading

You must enable the experimental support for ESM.

I’m having problems with ESM and Jest Jump to heading

Read this first. The problem is either Jest (#9771) or your Jest configuration. First, ensure you are on the latest version of Jest. Please don’t open an issue on my repo. Try asking on Stack Overflow or open an issue the Jest repo.

I’m having problems with ESM and TypeScript Jump to heading

If you have decided to make your project ESM ("type": "module" in your package.json), make sure you have "module": "ES2020" in your tsconfig.json and that all your import statements to local files use the .js extension, not .ts or no extension.

I’m having problems with ESM and ts-node Jump to heading

Follow this guide and ensure you are on the latest version of ts-node.

I’m having problems with ESM and Create React App Jump to heading

Create React App doesn’t yet fully support ESM. I would recommend opening an issue on their repo with the problem you have encountered. One known issue is #10933.

How can I use TypeScript with AVA for an ESM project? Jump to heading

Follow this guide.

How can I make sure I don’t accidentally use CommonJS-specific conventions? Jump to heading

We got you covered with this ESLint rule. You should also use this rule.

What do I use instead of __dirname and __filename? Jump to heading

import { fileURLToPath } from 'node:url'
import path from 'node:path'

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(fileURLToPath(import.meta.url))

However, in most cases, this is better:

import { fileURLToPath } from 'node:url'

const foo = fileURLToPath(new URL('foo.js', import.meta.url))

And many Node.js APIs accept URL directly, so you can just do this:

const foo = new URL('foo.js', import.meta.url)

How can I import a module and bypass the cache for testing? Jump to heading

There’s no good way to do this yet. Not until we get ESM loader hooks. For now, this snippet can be useful:

const importFresh = async (modulePath) =>
import(`${modulePath}?x=${new Date()}`)

const chalk = (await importFresh('chalk')).default

Note: This will cause memory leaks, so only use it for testing, not in production. Also, it will only reload the imported module, not its dependencies.

How can I import JSON? Jump to heading

JavaScript Modules will eventually get native support for JSON, but for now, you can do this:

import { promises as fs } from 'node:fs'

const packageJson = JSON.parse(await fs.readFile('package.json'))

If you target Node.js 14 or later, you can import it using import fs from 'node:fs/promises'; instead.

When should I use a default export or named exports? Jump to heading

My general rule is that if something exports a single main thing, it should be a default export.

Keep in mind that you can combine a default export with named exports when it makes sense:

import readJson, { JSONError } from 'read-json'

Here, we had exported the main thing readJson, but we also exported an error as a named export.

Asynchronous and synchronous API Jump to heading

If your package has both an asynchronous and synchronous main API, I would recommend using named exports:

import { readJson, readJsonSync } from 'read-json'

This makes it clear to the reader that the package exports multiple main APIs. We also follow the Node.js convention of suffixing the synchronous API with Sync.

Readable named exports Jump to heading

I have noticed a bad pattern of packages using overly generic names for named exports:

import { parse } from 'parse-json'

This forces the consumer to either accept the ambiguous name (which might cause naming conflicts) or rename it:

import { parse as parseJson } from 'parse-json'

Instead, make it easy for the user:

import { parseJson } from 'parse-json'

Examples Jump to heading

With ESM, I now prefer descriptive named exports more often than a namespace default export:

CommonJS (before):

const isStream = require('is-stream');

isStream.writable();

ESM (now):

import {isWritableStream} from 'is-stream';

isWritableStream();

← Back home