Skip to content

Instantly share code, notes, and snippets.

@sindresorhus
Last active May 8, 2024 22:50
Show Gist options
  • Save sindresorhus/a39789f98801d908bbc7ff3ecc99d99c to your computer and use it in GitHub Desktop.
Save sindresorhus/a39789f98801d908bbc7ff3ecc99d99c to your computer and use it in GitHub Desktop.
Pure ESM package

Pure ESM package

The package that linked you 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.
  4. Since Node.js 22, you may be able to require() ESM modules. However, I strongly recommend moving to ESM instead.

You also need to make sure you're on the latest minor version of Node.js. At minimum Node.js 16.

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

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

FAQ

How can I move my CommonJS project to ESM?

  • 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 16: "node": ">=16".
  • 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.
  • Use the node: protocol for Node.js built-in 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?

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

How can I make my TypeScript project output ESM?

Read the official ESM guide.

Quick steps:

  • Make sure you are using TypeScript 4.7 or later.
  • 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 16: "node": ">=16".
  • Add "module": "node16", "moduleResolution": "node16" to your tsconfig.json. (Example)
  • Use only full relative file paths for imports: import x from '.';import x from './index.js';.
  • Remove namespace usage and use export instead.
  • Use the node: protocol for Node.js built-in 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. Example config.

How can I import ESM in Electron?

Electron supports ESM as of Electron 28. Please read this.

I'm having problems with ESM and Webpack

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

Upgrade to Next.js 12 which has full ESM support.

I'm having problems with ESM and Jest

Read this.

I'm having problems with ESM and TypeScript

If you have decided to make your project ESM ("type": "module" in your package.json), make sure you have "module": "node16" 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

I would recommend tsx instead.

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

Example config.

I'm having problems with ESM and Create React App

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?

Follow this guide.

How can I make sure I don't accidentally use CommonJS-specific conventions?

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

What do I use instead of __dirname and __filename?

Node.js 20.11+

Use import.meta.dirname and import.meta.filename.

Older Node.js versions

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?

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=${Date.now()}`);

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?

Node.js 18.20+

import packageJson from './package.json' with {type: 'json'};

Older Node.js versions

import fs from 'node:fs/promises';

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

When should I use a default export or named exports?

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

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

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

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();
@jimmywarting
Copy link

jimmywarting commented Dec 12, 2021

I agree with you @jaydenseric... some typescript user think they use esm while in fact they are transpiling to cjs without even knowing it.

I would not call cjs depricated... legacy is a more correct word for it... it will likely never go away for a foreseeable future but new code should start to using esm by default now...

From my own experiences we at node-fetch fist wanted to use import and if we wanted to have dual support then it meant that we would have to convert everything back to cjs (even sup dependencies), which we didn't want to do anymore. We didn't want to duplicate all our code. and we didn't want to reintroduce a compiler. And We now also depend on other esm only packages. if we wanted to have dual support at this stage now then it meant that we would have to embed our sub dependencies into node-fetch and transpile them to cjs as well to keep a own copy for our self, which would duplicate all the code and classes and interfere with instances.
If someone where to depend on the same version of any of the public classes or sup packages (formdata, header, blob, file, abortsignal whatwg streams) or whatever then it would just be conflicting with instanceof checks and node-fetch would be stuck on one internal version of one package while you might depend on some newer version.

dual support is just a hazard that i want to avoid. cjs don't work in any other env other than NodeJS and NodeJS supports ESM now so why would one want to use cjs still? NodeJS is not the only platform, developers want their code to work cross other env too, even if you built something in cjs that was solo built to run in NodeJS, then there will always be that one guy who are going to wish that you built it as ESM so they can import it without the need of npm or any bundler for Deno or the Browaser.

Someone must start converting to pure ESM at some point. We can't sitt around and wait for all 23k packages who depend on node-fetch to switch to esm first. Or the other way around. We can't sitt around and wait for sub dependencies or build tools to all be ESM first. And we can't control how gulp, webpack, jest, rollup, typescript, babel, etc all deal with cjs and esm. then we would be stuck with cjs/dual packages forever.

It's just a chicken and the egg paradox of who should convert to ESM first... typescript and other have long stick there head in the sand about extension less paths and depend on cjs to resolve the path. This can't continue like this when we have remote path resolver that are dynamic lazy imported

i fear that ppl will start using the experimental-specifier-resolution flag and hope that it stays implicit for remote http path resolvers sakes
i wish experimental-specifier-resolution was never introduced into NodeJS, it's considered a bad practices now to depend on index and extensionless paths.

I don't have much sympathy for typescript user either. typescript isn't javascript... i work with javascript, nothing else.

sindre have my respect and esm is the feature, weather you like it or not. it's time to switch

@jaydenseric
Copy link

@ljharb

The dual package "hazard" exists if and only if your package is stateful, or relies on identity. The vast majority of packages don't, and thus, for the vast majority of packages, there is no such thing as a dual package hazard.

This is simply untrue. Runtime identity issues are just one symptom of the dual package hazard. If you are bundling, consuming the ESM and CJS formats of the same code in different parts of your code base / dependency graph result in the same code being bundled twice, which slows your builds, and increases the download and parse times for users loading the web app. If you are not bundling, loading and parsing more code still has a performance cost.

Technically not the dual package hazard, but another downside regardless, is if every package in node_modules duplicated it's API in seperate ESM and CJS files, node_modules takes up considerably more disk space. node_modules install size is a serious problem the industry is facing; it slows installations in CI or for deployments, and increases disk space usage increasing costs or exceeding environment limits (particularly for serverless).

@OmgImAlexis

you can check the existence of all of those files at the same time and then choose which to use. It's not going to take any noticeable difference in time.

There are good reasons automatic file extension resolution was dropped from the final Node.js ESM system, and it isn't a thing in Deno or browsers either. It's a bad idea. Looking up all the possible resolutions is prohibitive if the modules are imported using HTTP imports over network; either in a server environment (Deno) or in browsers for apps using ESM for real on the client. It also significantly complicates and slows a lot of dev tooling that statically analyses imports. For an example of just one tool, look at all the extra complexity supporting it added to find-unused-exports:

@sindresorhus
Copy link
Author

sindresorhus commented Feb 4, 2022

This discussion thread is not productive and I never meant for this gist to be a place for everyone to vent about ESM.

CONSIDER THIS THREAD LOCKED

ANY COMMENTS AFTER THIS ONE WILL BE DELETED

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment