The package that linked you here is now pure ESM. It cannot be require()
'd from CommonJS.
This means you have the following choices:
- Use ESM yourself. (preferred)
Useimport foo from 'foo'
instead ofconst foo = require('foo')
to import the package. You also need to put"type": "module"
in your package.json and more. Follow the below guide. - If the package is used in an async context, you could use
await import(…)
from CommonJS instead ofrequire(…)
. - Stay on the existing version of the package until you can move to ESM.
- 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.
- 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 18:"node": ">=18"
. - Remove
'use strict';
from all JavaScript files. - Replace all
require()
/module.export
withimport
/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.
Yes, but you need to convert your project to output ESM. See below.
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 18:"node": ">=18"
. - Add
"module": "node16", "moduleResolution": "node16"
to your tsconfig.json. (Example)moduleResolution
must be set tonode16
ornodenext
, NOTnode
!
- Use only full relative file paths for imports:
import x from '.';
→import x from './index.js';
. - Remove
namespace
usage and useexport
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.
Electron supports ESM as of Electron 28. Please read this.
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.
Upgrade to Next.js 12 which has full ESM support.
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 would recommend tsx
instead.
Follow this guide and ensure you are on the latest version of ts-node
.
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.
Follow this guide.
We got you covered with this ESLint rule. You should also use this rule.
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);
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.
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'));
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.
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
.
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';
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(…);
Most people don't realize that their imagination of a package that perfectly supports both ESM (for Node.js, Deno/browsers, and bundlers) and CJS consumers is impossible.
To avoid the dual package hazard, all the package's modules must be CJS with a single "default export" (
module.exports =
), with the exception of index files that are conditionally resolved for ESM consumers (via conditional exports) to allow proper named exports. This setup means everyone is running and bundling the same code, regardless if it's imported or required. It's incorrect for a package to publish the same API built twice; once to ESM files, and another time to CJS files. @StarpTech it appears all of the "popular projects that use it successfully" you listed are incorrect. If part of your codebase requires from them, and another imports the same thing, duplicated code will run/bundle.Here's a correct example:
The downsides are:
.mjs
support! Tooling that needs to parse the source code (e.g. via Babel) to work has to have double the complexity in some cases because the AST for a CJS module (require
calls, etc.) is very different to an ESM module (import
statements, etc.).IMO the JavaScript community has had the priorities wrong for a long time; we need to prioritize technical elegance, simplicity, DX, and documentation for package authors over package consumers. Otherwise we are building our castles on top of a foundation of mud only pigs enjoy working in.
I remember wishing 5 years ago that Node.js would just deprecate CJS and support ESM from the next major version. If only that had happened when the npm ecosystem was much smaller; literally hundreds, perhaps thousands of hours of my life have been wasted on CJS issues.
There is never going to be a time that it's painless to switch to pure ESM. The faster we rip the bandaid off and focus on publishing simple, standards-aligned packages for us all to build on, the better. All the people complaining have no idea that Deno is going to disrupt their entire world soon anyway, a runtime that only works with real ESM; browser-like HTTP/S imports.