Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Pure ESM package

Pure ESM package

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. 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

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 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.

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?

  • 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.

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 Jest

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

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

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

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?

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 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=${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?

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

import {promises as fs} from 'fs';

const packageJson = JSON.parse(await fs.readFile('package.json', 'utf8')).
@fisker

This comment has been minimized.

Copy link

@fisker fisker commented Apr 16, 2021

Hate and love you, man.

@Qix-

This comment has been minimized.

Copy link

@Qix- Qix- commented Apr 16, 2021

Been waiting years for this move.

@dangowans

This comment has been minimized.

Copy link

@dangowans dangowans commented Apr 19, 2021

Using this as a "how to" guide to update some of my own packages. Thank you!

@Qix-

This comment has been minimized.

Copy link

@Qix- Qix- commented Apr 19, 2021

@sindresorhus

Might want to:

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

This comment has been minimized.

Copy link
Owner Author

@sindresorhus sindresorhus commented Apr 19, 2021

@qix Done

@cduff

This comment has been minimized.

Copy link

@cduff cduff commented Apr 21, 2021

Hi @sindresorhus,

Thought I'd comment on this with my experience so far.

We're using crypto-random-string, which moved to pure ESM in v4, in a web server project built with typescript and making use of ts-node and ts-node-dev. As per your suggestion, I tried updating my project to ESM but this does not seem possible at the moment:

I don't like the idea of having to move some modules to async dynamic imports but gave it a try. However it doesn't work when TypeScript is configured with "module": "CommonJS" as it translates import() function calls to require().

Staying on old package versions is possible but I worry a time will soon come where it means I can no longer upgrade to realise package improvements/bug fixes.

I'm therefore feeling stuck and not sure of the best path forward on this. So far for us it relates to only one minor dependency but I worry the problem will soon grow much larger.

Any suggestions?

I think I understand your motivation but also wonder about the impact at a time when tooling will struggle to keep up, especially given Node's ESM loader hooks are experimental and changing.

@ujwal-setlur

This comment has been minimized.

Copy link

@ujwal-setlur ujwal-setlur commented Apr 23, 2021

@cduff I agree. Have very similar issues. I love the idea of moving to ESM, but the tools are just not there yet, and I can’t wholesale switch my entire typescript project to ESM quite yet. Unfortunately, for now, I had to switch to d3-random for the random utils. Hopefully the tooling will catch up.

@Qix-

This comment has been minimized.

Copy link

@Qix- Qix- commented Apr 25, 2021

Usually, moves like this are the kick in the pants to get tooling upgraded to begin with. If neither end does anything, innovation stagnates.

This is a good thing. The pain is growth.

@PopGoesTheWza

This comment has been minimized.

Copy link

@PopGoesTheWza PopGoesTheWza commented Apr 25, 2021

Note that in addition to import statements, you need to also update local export ... from with full relative file paths.

And rather than replacing "main" with "exports" in your package.json, it can be safer to have both. First, "exports" has precedence over "main" anyway; but some tooling may sometime not be fully up-to-date and get confused when there is no "main". I ran into issues with some lerna monorepos when only "export" was present.

@moltar

This comment has been minimized.

Copy link

@moltar moltar commented Apr 27, 2021

Also cannot upgrade multiple projects. Stuck in many directions. There is no path forward 🤷‍♂️

@foray1010

This comment has been minimized.

Copy link

@foray1010 foray1010 commented Apr 28, 2021

Hi @sindresorhus, I suggest:

- Update the "engines" field in package.json to Node.js 12: "node": ">=12".
+ Update the "engines" field in package.json to Node.js 12: "node": ">=12.7".

because:

  1. the exports field is not supported until 12.7.0 (see the diff between 12.6.0 doc and 12.7.0 doc), user with 12.6.0 will be able to install but cannot import this package
  2. You might want >=12.13.0 since it is the first LTS version
  3. You might even want >=12.17.0 since no --experimental-modules flag is required in >=12.17.0 (see the diff between 12.6.0 doc and 12.7.0 doc)
@2c2c

This comment has been minimized.

Copy link

@2c2c 2c2c commented Apr 30, 2021

does there exist a codemod to change any relative imports to add .js file extension? getting burned by dependency update

@oncet

This comment has been minimized.

Copy link

@oncet oncet commented Apr 30, 2021

When integrating with Next.js I had to use next-transpile-module to avoid the Error [ERR_REQUIRE_ESM]: Must use import to load ES Module #28 error (sindresorhus/ky-universal#28).

// next.config.js
const withTM = require("next-transpile-modules")(["ky"]);

module.exports = withTM();
@mesoic

This comment has been minimized.

Copy link

@mesoic mesoic commented May 1, 2021

Add "type": "module" to your package.json.
This can break things

@RDIL

This comment has been minimized.

Copy link

@RDIL RDIL commented May 1, 2021

@mesoic the whole idea of moving from CommonJS to ESM can (and most likely will) break things!

@xavierfoucrier

This comment has been minimized.

Copy link

@xavierfoucrier xavierfoucrier commented May 1, 2021

ESM is the future. Go for it 💗

@akx

This comment has been minimized.

Copy link

@akx akx commented May 4, 2021

Jest can't currently resolve "main"less ESM packages (as these instructions prescribe), so the instructions under "I'm having problems with ESM and Jest" don't necessarily help in the least.

See facebook/jest#11373 & facebook/jest#9771 ...

@sindresorhus

This comment has been minimized.

Copy link
Owner Author

@sindresorhus sindresorhus commented May 5, 2021

@foray1010 Ok. Done.

@mkubilayk

This comment has been minimized.

Copy link

@mkubilayk mkubilayk commented May 6, 2021

Worth noting that node: imports were added in v12.20.0 so "node": ">=12.17" engine restriction doesn't cover that.

@sindresorhus

This comment has been minimized.

Copy link
Owner Author

@sindresorhus sindresorhus commented May 7, 2021

@mkubilayk Good point 👍

@coderaiser

This comment has been minimized.

Copy link

@coderaiser coderaiser commented May 8, 2021

does there exist a codemod to change any relative imports to add .js file extension? getting burned by dependency update

You can use putout code transformer with plugin @putout/plugin-convert-commonjs-to-esm for this purpose, it will convert:

-const {fn} = require('./local');
-module.exports = () => {};
+import {fn} from './local.js';
+export default () => {};
@madnight

This comment has been minimized.

Copy link

@madnight madnight commented May 9, 2021

import pkg from '../package.json';

This line results in the following error:

(node:7863) ExperimentalWarning: The ESM module loader is experimental.
internal/modules/run_main.js:54
    internalBinding('errors').triggerUncaughtException(
                              ^

TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".json" for /home/user/files/project/package.json imported from /home/user/files/project/version.js
    at Loader.resolve [as _resolve] (internal/modules/esm/default_resolve.js:126:13)
    at Loader.resolve (internal/modules/esm/loader.js:72:33)
    at Loader.getModuleJob (internal/modules/esm/loader.js:156:40)
    at ModuleWrap.<anonymous> (internal/modules/esm/module_job.js:42:40)
    at link (internal/modules/esm/module_job.js:41:36) {
  code: 'ERR_UNKNOWN_FILE_EXTENSION'
}

@sindresorhus How to import .json files with ESM?

@Qix-

This comment has been minimized.

Copy link

@Qix- Qix- commented May 9, 2021

@madnight you don't because JSON is not javascript. The hacks for custom file extensions in node have been removed in ESM.

Read it in like a normal file and then parse the JSON. That's what the old CommonJS stuff did anyway.

@coderaiser

This comment has been minimized.

Copy link

@coderaiser coderaiser commented May 9, 2021

@madnight you can use simport for this purpose:

import {createCommons} from 'simport';
const {require} = createCommons(import.meta.url);

const pkg = require('../package.json');
@madnight

This comment has been minimized.

Copy link

@madnight madnight commented May 9, 2021

Thx @coderaiser looks good to me, I'll try that.

@Qix-

This comment has been minimized.

Copy link

@Qix- Qix- commented May 9, 2021

@madnight It's really not that difficult, and that's a hack that requires yet another dependency. Just do the right thing? Not that hard.

import {promises as fsp} from 'fs';
const pkg = JSON.parse(await fsp.readFile('package.json', 'utf-8')).

By using a require-type means of importing, you lose out on all of the language benefits of import statements. We're moving away from CommonJS, why are we re-inventing it?

Don't add cruft to the ecosystem please.

@madnight

This comment has been minimized.

Copy link

@madnight madnight commented May 9, 2021

@Qix-

We're moving away

Moving away would be okay for me, but I don't like to be forced immediately. I have a dependency that can only be included via ESM and I still want to use require at least for some time. I call this transition period. Hard breaking changes are not a good thing (python 2 -> 3), but slow transition is absolutely okay.

@Qix-

This comment has been minimized.

Copy link

@Qix- Qix- commented May 9, 2021

We've been in a transition period for a long time. That's the problem. At some point, some switches need to be flipped.

Remember when we did so with getting people to update from Node 0.10/0.12. We can't perpetually support old systems.

@madnight

This comment has been minimized.

Copy link

@madnight madnight commented May 9, 2021

If that's true, then it would have been possible for quite some time to use both require and import. Without a hard switch from one to the other.

@Qix-

This comment has been minimized.

Copy link

@Qix- Qix- commented May 9, 2021

Node 12 introduced the imports via a command line flag and was released 2019-04-23. Is two years long enough?

@madnight

This comment has been minimized.

Copy link

@madnight madnight commented May 9, 2021

So I can simply use Node 12 and require and import works in the same file? Similar like writing python 2 and 3 compatible code in the past?

@Qix-

This comment has been minimized.

Copy link

@Qix- Qix- commented May 9, 2021

No, because .mjs is different from .js. You're not going to find Python 2/3-like compatibility.

@coderaiser

This comment has been minimized.

Copy link

@coderaiser coderaiser commented May 9, 2021

So I can simply use Node 12 and require and import works in the same file? Similar like writing python 2 and 3 compatible code in the past?

Yes, you can, with help of simport :).
And later you can switch to import assertions when it would be implemented in node(it is available behind the flag), it is stage 3 right now.

@madnight

This comment has been minimized.

Copy link

@madnight madnight commented May 9, 2021

Then I consider it as breaking change, that breaks your code, without a transition period. I understand that many wait for ESM to happen for a long time. I just want to criticize the way it is introduced now (hard switch).

@madnight

This comment has been minimized.

Copy link

@madnight madnight commented May 9, 2021

@coderaiser

This comment has been minimized.

Copy link

@coderaiser coderaiser commented May 9, 2021

@coderaiser yes https://github.com/tc39/proposal-import-assertions would be great

Yep, and codemode for transition using putout would be not harder then:

module.exports.report = () => 'Import assertions should be used instead of require';

module.exports.replace = () => ({
    'const __a = require("./package.json")': 'import __a from "./package.json" assert { type: "json" }',
});

Which will transit all codebase at once and clean up unused imports with @putout/plugin-remove-unused-variables as well.

@Qix-

This comment has been minimized.

Copy link

@Qix- Qix- commented May 9, 2021

Then I consider it as breaking change, that breaks your code, without a transition period. I understand that many wait for ESM to happen for a long time. I just want to criticize the way it is introduced now (hard switch).

Please point to where this was a "hard switch". Everything I've seen has been done via major releases, which indicate a break in the public API.

Unless this isn't the case somewhere (it's the case with all of @sindresorhus's modules AFAIK) then this is entirely your problem, not anyone else's. Apologies for the bluntness.

@madnight

This comment has been minimized.

Copy link

@madnight madnight commented May 10, 2021

It's completely okay for me if it is just "my problem". I just haven't expected it that way. I will use the workaround from @coderaiser for a while and then slowly migrate to ESM only and call it a day.

@moltar

This comment has been minimized.

Copy link

@moltar moltar commented May 10, 2021

Read it in like a normal file and then parse the JSON. That's what the old CommonJS stuff did anyway.

Doesn't work that well with TS, as TS was inferring the types from the JSON file automatically. When loading and parsing manually everything is just any.

@clouedoc

This comment has been minimized.

Copy link

@clouedoc clouedoc commented May 10, 2021

Read it in like a normal file and then parse the JSON. That's what the old CommonJS stuff did anyway.

Doesn't work that well with TS, as TS was inferring the types from the JSON file automatically. When loading and parsing manually everything is just any.

Zod is the right tool for this kind of problem.

@moltar

This comment has been minimized.

Copy link

@moltar moltar commented May 10, 2021

Read it in like a normal file and then parse the JSON. That's what the old CommonJS stuff did anyway.

Doesn't work that well with TS, as TS was inferring the types from the JSON file automatically. When loading and parsing manually everything is just any.

Zod is the right tool for this kind of problem.

Really isn't though. Zod is no different than writing types yourself. While TypeScript infers types automatically from imported JSON.

@Qix-

This comment has been minimized.

Copy link

@Qix- Qix- commented May 10, 2021

That's a Typescript problem, not a JavaScript problem.

@clouedoc

This comment has been minimized.

Copy link

@clouedoc clouedoc commented May 10, 2021

Really isn't though...

Let me rephrase it.

Zod is the right tool for this kind of problem because:

  1. You don't get an any blob
  2. You're 100% sure that what the typing system allows you to do will work at runtime - just like type inference from JSON.

If you want to skip the schema writing part, you can combine Quicktype and the extension that someone wrote to convert from interface to Zod schema.

Zod is no different than writing types yourself
It's different because you get to validate the data instead of casting it.

There is no reason to not use Zod when dealing with JSON file, even for file that don't change. You get solid typings and strong validation. The schema-writing overhead is negligible against the time it'll take you to debug a any.foo is undefined, be it when developing or running on a server.

@moltar

This comment has been minimized.

Copy link

@moltar moltar commented May 11, 2021

Zod is the right tool for this kind of problem because:

It is one of the tools. I maintain a large list with benchmarks of this class of tools. Zod actually happens to be one of the slowest, if not the slowest of all of them.

But the tool is for the problem, which I did not have before.

Before, I could just do import myJson from './file.json' and have all of the types automatically inferred.

@shirshak55

This comment has been minimized.

Copy link

@shirshak55 shirshak55 commented May 11, 2021

this seems to be major ecosystem churn unfortunately. Some module doesn't work in esm some works on common js :(

@terribleplan

This comment has been minimized.

Copy link

@terribleplan terribleplan commented May 14, 2021

👎

Node itself isn't mature enough in its implementation, this shouldn't happen until it is. Lack of such maturity means I can't use yarn with PNP, which is important to me. This is the sort of stuff that encourages people to stick to old versions of packages long after they are unmaintained and insecure.

Also referring to "release notes" was confusing to me as they are not very discoverable from npmjs.org (or even github really, it took me a while to understand you even meant to read the "releases" page, I kept looking for a RELEASE_NOTES.md in-repo or something). Maybe a link to this gist at the top of the impacted modules' README would help confused people who's stuff broke from making issues.

Notwithstanding the issue of timing, this is a good goal and the right direction for the ecosystem to move (though I selfishly wish you had gone with migration option #2 from your blog)

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