Skip to content(if available)orjump to list(if available)

Move on to ESM-Only

Move on to ESM-Only

89 comments

·March 24, 2025

jauntywundrkind

They key thing is that require(esm) shipped in node 22, and is being back ported to node 20.

Since Node 18 maintenance ends in less than a month, this means all Node versions will have good esm support, including for consuming esm-only libraries (which until recently did not work!)

This is noted somewhat in the article, but basically is the whole story to me. Its now possible to stop doing CJS libraries entirely. And with that, I don't see why we would do CJS at all.

moltar

Annoyingly you have to use an experimental flag. That just adds too much friction.

Latest Node version added Node options as config feature. I wish that was ported to every version of Node.

Otherwise you have to set NODE_OPTIONS which can often be overwritten by some scripts in the execution chain.

uasi

require(esm) is no longer behind a flag in v22.12.0+ and v20.19.0+.

forty

Funnily my conclusion was, on the contrary, that it's now possible to ignore ESM entirely when using/targeting nodejs ;)

forty

(sadly at work, at this point we already have done the useless work of migrateming to ESM for no good reason other than having libs that were ESM only)

9dev

ESM has heaps of benefits: being able to do static analysis, or limiting the exposed modules in a package, for example.

3np

There are still maintained and supported older versions of node.js by the likes of Debian, Ubuntu, Enterprise Linuxes. They backport security fixes and such during a longer extended window but are unlikely to port ESM-require support. It may not be relevant to you and obv nobody's obliging you to also support those users, just saying it's not that binary.

rafaelmn

If you are pinning to unsupported node you should not expect new npm packages to work, ESM or not.

However given my NPM experiences in the past, I would not be surprised that someone updated to ESM in a revision bump.

mattmanser

Is that really "support those users"?

If they need to use a new library version, they can install their own version of node instead of relying on the OS supplied one.

All the OS version is doing is supplying the convenience of not having to install it.

Or have I misunderstood how those versions of node are used?

3np

Sometimes the entire reason why they are still on that older LTS dist is because they still need (e.g.) node 12 for some reason.

graypegg

Huh, oddly no mention of native browser support in the form of <script type=“module”> and importmaps! To me, that felt like the last platform to offer ESM support.

I also feel like browser support “feels” more official than support from bundlers.

Just weird to leave out a mention of that.

codedokode

Sadly ESM don't work with local files (file:// protocol).

graypegg

That would be expected for a browser environment though! I don’t want any module my browser imports to silently import something off the filesystem without telling me.

em-bee

while i agree with that, i don't quite understand why this is an issue if the main index.html file is from the local filesystem too. the filesystem should only be off limits if the main file is loaded from a website.

WorldMaker

Most systems come with Python today, so starting an HTTP server for local testing is often a one-liner like `python -m http.server`. Anyone already working with Node has access to one-liners like `npx http-server`. (Deno and Bun also have one-liners.)

codedokode

It is not that easy. You need to open console and navigate to the project directory first. While with ordinary HTML files all you need is to start typing its name in address bar (or simply never close the tab).

null

[deleted]

akoboldfrying

It's certainly nice, and makes possible Vite's very quick reloads of developer code, but even Vite still makes (ESM) bundles for performance reasons, both for production (with everything, using Rollup) and for dev (with just the external deps, using ESBuild).

My understanding is that HTTP 2 pipelining was supposed to make requesting lots of ESM modules from the same site fast enough that bundling could be consigned to history, but for various reasons it didn't work as intended and it was ultimately removed from most browsers.

chrismorgan

HTTP protocol changes were never, ever, going to make bundling unnecessary. No one actually involved in the things said it would, or thought it would. It was less-deeply-involved people misunderstanding things that led to that popular impression.

HTTP/2’s better multiplexing helps a little, but you’ve still completely got the waterfall problem: so you have at an absolute minimum of overhead the dependency graph depth times the round-trip time—frequently multiple seconds.

HTTP/2 Server Push could have improved it in some regards, as it can in theory break out of the waterfall problem, but in practice it would have required much more complex servers, and was missing important pieces so that it was completely useless anyway (a way for the client to tell the server what resources it has cached), and they eventually just removed it all round rather than inventing and implementing the missing pieces, with which it still would have been a good deal less efficient to execute than bundling.

Minifying and bundling is just better, no matter what.

dieulot

HTTP/2 “pipelining” (multiplexing) is still there and works as intended; but bundling is still much more efficient.

This article delves into just that: https://csswizardry.com/2023/10/the-three-c-concatenate-comp... The first pair of waterfall graphics illustrates the problem clearly.

(What you vaguely remember as not working as intended is probably Server Push).

WorldMaker

> My understanding is that HTTP 2 pipelining was supposed to make requesting lots of ESM modules from the same site fast enough that bundling could be consigned to history, but for various reasons it didn't work as intended and it was ultimately removed from most browsers.

HTTP/2+ Pipelining works pretty well. (As others mentioned, it was HTTP/2 Server Push that didn't quite survive in the wild, which could have helped additional scenarios.)

I've been personally moving towards Vite's dev approach for Production (just external deps and big libraries with esbuild) and little to no bundling in dev (only external deps that don't run out-of-the-box as ESM with an importmap). A handful of small "local" files and couple big shared libraries works very well in Production in my experience.

graypegg

Oh yeah I assumed so. I just thought it was weird to not mention it in an article about ESM becoming the standard JS modules format. Even if it’s not the most performant use of ES modules, I still think it’s certainly the most official “signal” of support to have it in all major browsers.

null

[deleted]

Klaster_1

On a side note, I recently experimented with native TS support in Node.js and it felt like magic: no need for extra flags; debugging and watch just work; types are simple to re-use between browser and server. Erasable syntax seems like the way forward, can't wait for it to land in browsers.

Shacklz

Same here - it's really quite something. The future is looking good!

That being said, there are currently still some hurdles. Necessity for explicit file-extensions in the imports is definitely the big offender (it's invalid typescript syntax without the allowImportingTsExtensions-flag).

The trend is definitely clear though, most devs want ESM, most devs want types; it's just a matter of time until the ecosystem adapts. I suppose for types to finally land in the browser, TC39 will have to undergo the "progress is one funeral at a time"-principle, which will probably take another while.

normie3000

> can't wait for it to land in browsers

Presumably for download size and backward compatibility everyone will still serve JS to browsers.

eddd-ddde

But it'd be nice not to /have to/. Specially when working on new projects with no build step.

WorldMaker

Typescript Types don't add that much to download size and generally compress very well.

madeofpalk

It is really nice. I'm glad the Node and Typescript teams finally starting making pragmatic decisions to actually be useful.

Now typescript just needs some better defaults from tsc --init to match :)

patwolf

I was working on a legacy CSJ project, and I tried to upgrade an OIDC library, but the newer version would only work with ESM. I decided to use that as an excuse to migrate our project to ESM. However, I hit a bug using dd-trace with ESM. Over a year later, that bug hasn't been resolved. I try to use ESM as much as possible for new projects, but it's not always simple to migrate existing projects to ESM.

airstrike

> The Toolings are Ready

> Modern Tools

> With the rise of Vite as a popular modern frontend build tool, many meta-frameworks like Nuxt, SvelteKit, Astro, SolidStart, Remix, Storybook, Redwood, and many others are all built on top of Vite nowadays, that treating ESM as a first-class citizen.

> As a complement, we have also testing library Vitest, which was designed for ESM from the day one with powerful module mocking capability and efficient fine-grain caching support.

> CLI tools like tsx and jiti offer a seamless experience for running TypeScript and ESM code without requiring additional configuration. This simplifies the development process and reduces the overhead associated with setting up a project to use ESM.

> Other tools, for example, ESLint, in the recent v9.0, introduced a new flat config system that enables native ESM support with eslint.config.mjs, even in CJS projects.

I think the author and I must have very different definitions for the word "ready"

vosper

How would you define “ready” here?

airstrike

Definitely "stable", but also "simple" and "straightforward"

thiht

So you need a bundler, a linter and a test runner. How’s that any different from the experience in other languages where you need a compiler, a linter and a test runner?

notanaverageman

Meanwhile I'm trying to adapt ESM only packages to CJS since Goja (JS runtime library for Go) doesn't have ESM support. [1]

And yes I know that Grafana has a fork called Sobek that has ESM support, but it is tailored for k6 and they don't have plans for making it easier to use. [2]

[1] https://github.com/dop251/goja/issues/348

[2] https://github.com/grafana/sobek/issues/49

jakub_g

Did anyone make a large TypeScript codebase work with ESM + TS + node (together with IDE support and yarn pnp support?)

The thing that is annoying with ESM is that it requires to have extensions in imports, i.e. `import .. from '/foo.js'`.

This gets messy with TypeScript, where your files are named `foo.ts` but you need to import `foo.js`.

The previous "best practice" in TS world was to have extensionless JS imports, so this move would require a massive codemod to update all imports in an entire codebase.

For now, we've been using `ts-node` with swc under the hood, but without ESM. I tried `tsx`, but the compilation time of esbuild is way too slow, some of our node CLI tools written in TS take 15s to boot up, which is not acceptable (with `ts-node` it's 3-4s) (tbh, it's probably partly a fault of our CLI framework, which crawls all workspaces to discover all CLI tools, and as we have a lot of them, tsx has a lot of useless work to do).

gauben

Hey! Not sure how modern your codebase is, but you can consider the following tsconfig settings:

- rewriteRelativeImportExtensions: this will allow you to write `import foo from './foo.ts'` and have tsc transform it to `import foo from './foo.js'`

- erasableSyntaxOnly: this will error on non "erasable" syntax, that is, TypeScript code that has a runtime output (e.g. enums)

With these two settings enabled, you'd be able to run TypeScript code directly with Node: `node src/index.ts`, and cut boot up time substantially

hakkotsu

While `rewriteRelativeImportExtensions` works for frontend applications, it has a significant limitation: it doesn't fix declaration files (.d.ts), which is problematic when developing libraries or public packages.

I created a small tool to address ESM + TypeScript issues that the tsc doesn't handle: https://github.com/2BAD/tsfix

hombre_fatal

To add, those are the recommended tsconfig.json settings in Node's docs on native TS stripping. Here are the rest: https://nodejs.org/api/typescript.html#type-stripping

WorldMaker

Also there are eslint rules older than erasableSyntaxOnly that can also be useful in doing a "rip-the-bandaid-off-refactor" using all the lint warnings/warnings-as-errors to add extensions everywhere in the case where your brownfield also needs to be a version or two behind on Typescript.

paulddraper

It’s pretty easy to find/replace everywhere that needs it.

In my experience the reason people don’t is that it offends their aesthetics.

Which I understand. But personally I don’t program for aesthetics.

darepublic

All these new bundling libraries (vite/rollup) always claim to just work, chefs kiss, 100 emote. And then you actually try to use them for basic use cases and you find yourself googling or knee deep in the documentation. If at all possible I try to avoid bundling libs altogether. That's the future I want. No more vite or rollup or webpack.

eternityforest

My experience with Vue/Vite is in fact chef's kiss 100 emote.

I would not want to set up a new project from scratch myself, but with the templates it works great, as long as you don't have to do anything outside of what they want you to do.

winrid

Vite is probably the only one that just works? If you follow the first 3mins of Getting Started it works great.

thiht

Uh? That’s completely alien to my experience with Vite. Setting up a Vite/React/Typescript project with Tailwind/Tanstack Query/Tanstack Router is an absolute breeze, you just follow the docs. Adding ESLint/Prettier is also just following the docs.

What problems did you encounter specifically? Did you report them?

sarreph

> Although a significant portion of packages still use CJS, the trend clearly shows a good shift towards ESM.

Does it?

The chart being used in the opening argument is far from compelling. When I hold my phone sideways, it looks as though the ESM portion is plateauing or feebly (at best) increasing. Maybe by 2040 we’d see widespread ESM adoption?

Jokes aside, I prefer ESM (and would prefer if everyone else preferred it), but leading with adoption rates is weak sauce.

bilalq

The way this cutover is being handled is reminiscent of Python 2->3. To this day, I still run into compatibility issues between things that only work with ESM and things that only work with CJS. It's frustrating, to say the least.

paulddraper

In my experience, the biggest obstacle to ESM in production systems is every observability/tracing tool relies on require-in-the-middle or in some other form CJS require().

o11c

Even "require at the start" (mandatory for transparent polyfills) seems incompatible with the way ESM wants to do things.

bhouston

I've been ESM only for the last 2 years and it has been amazing. Now to be fair, I am doing it on new projects so that probably simplifies things, but it is very freeing and performant.