惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Attack and Defense Labs
Attack and Defense Labs
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园_首页
博客园 - 司徒正美
月光博客
月光博客
Apple Machine Learning Research
Apple Machine Learning Research
T
Tailwind CSS Blog
Jina AI
Jina AI
GbyAI
GbyAI
Y
Y Combinator Blog
罗磊的独立博客
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
P
Proofpoint News Feed
Last Week in AI
Last Week in AI
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
量子位
雷峰网
雷峰网
博客园 - 【当耐特】
H
Hackread – Cybersecurity News, Data Breaches, AI and More
The GitHub Blog
The GitHub Blog
S
Secure Thoughts
博客园 - 三生石上(FineUI控件)
Cyberwarzone
Cyberwarzone
NISL@THU
NISL@THU
J
Java Code Geeks
C
Cisco Blogs
人人都是产品经理
人人都是产品经理
Webroot Blog
Webroot Blog
腾讯CDC
博客园 - 叶小钗
C
Cyber Attacks, Cyber Crime and Cyber Security
T
Troy Hunt's Blog
AI
AI
L
LangChain Blog
Know Your Adversary
Know Your Adversary
T
Tenable Blog
M
MIT News - Artificial intelligence
P
Privacy & Cybersecurity Law Blog
L
LINUX DO - 最新话题
Hugging Face - Blog
Hugging Face - Blog
F
Full Disclosure
P
Proofpoint News Feed
AWS News Blog
AWS News Blog
有赞技术团队
有赞技术团队
A
Arctic Wolf
Security Archives - TechRepublic
Security Archives - TechRepublic
S
Schneier on Security
Recent Commits to openclaw:main
Recent Commits to openclaw:main
W
WeLiveSecurity
The Cloudflare Blog

Anthony Fu

Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony's Roads to Open Source - The Set Theory (React ver.) Mental Health in Open Source The Evolution of Shiki v1.0 The Magic in Shiki Magic Move Anthony's Roads to Open Source - The Progressive Path Anthony Fu Anthony Fu Anthony Fu Anthony's Roads to Open Source - The Set Theory Now, and the Future of Nuxt Devtools Anthony's Roads to Open Source - The Set Theory Anthony Fu Anthony Fu Stable Diffusion QR Code 101 Refining AI Generated QR Code Stylistic QR Code with Stable Diffusion Anthony Fu How I Manage GitHub Notifications Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Dev SSR on Nuxt with Vite Why I don't use Prettier Anthony Fu Anthony Fu Ship ESM & CJS in one Package Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Reflection of Speaking in Public Anthony Fu Windi CSS and Tailwind JIT Typed Provide and Inject in Vue Color Scheme for VS Code Anthony Fu Anthony Fu Anthony Fu Anthony Fu Anthony Fu Destructuring... with object or array? Anthony Fu Anthony Fu Make Libraries Working with Vue 2 and 3 Anthony Fu Anthony Fu Anthony Fu Anthony Fu
Anthony Fu
Anthony Fu · 2025-02-05 · via Anthony Fu
  • The Toolings are Ready
    • Modern Tools
    • Top-Down & Bottom-Up
    • Requiring ESM in Node.js
  • The Troubles with Dual Formats
    • Interop Issues
    • Dependency Resolution
    • Package Size
  • When Should We Move to ESM-only?
    • New Packages
    • Browser-targeted Packages
    • Standalone CLI
    • Node.js Support
    • Know Your Consumers
  • How Far We Are?
  • Moving Forward

Three years ago, I wrote a post about shipping ESM & CJS in a single package, advocating for dual CJS/ESM formats to ease user migration and trying to make the best of both worlds. Back then, I didn’t fully agree with aggressively shipping ESM-only, as I considered the ecosystem wasn’t ready, especially since the push was mostly from low-level libraries. Over time, as tools and the ecosystem have evolved, my perspective has gradually shifted towards more and more on adopting ESM-only.

As of 2025, a decade has passed since ESM was first introduced in 2015. Modern tools and libraries have increasingly adopted ESM as the primary module format. According to WOOORM’s script, the packages that ships ESM on npm in 2021 was 7.8%, and by the end of 2024, it had reached 25.8%. Although a significant portion of packages still use CJS, the trend clearly shows a good shift towards ESM.

ESM adoption over time, generated by the npm-esm-vs-cjs script. Last updated at 2024-11-27

Here in this post, I’d like to share my thoughts on the current state of the ecosystem and why I believe it’s time to move on to ESM-only.

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.

Top-Down & Bottom-Up

Back in 2021, when SINDRESORHUS first started migrating all his packages to ESM-only, for example, find-up and execa, it was a bold move. I consider this move as a bottom-up approach, as the packages that rather low-level and many their dependents are not ready for ESM yet. I was worried that this would force those dependents to stay on the old version of the packages, which might result in the ecosystem being fragmented. (As of today, I actually appreciate that move bringing us quite a lot of high-quality ESM packages, regardless that the process wasn’t super smooth).

It’s way easier for an ESM or Dual formats package to depend on CJS packages, but not the other way around. In terms of smooth adoption, I believe the top-down approach is more effective in pushing the ecosystem forward. With the support of high-level frameworks and tools from top-down, it’s no longer a significant obstacle to use ESM-only packages. The remaining challenges in terms of ESM adoption primarily lie with package authors needing to migrate and ship their code in ESM format.

Requiring ESM in Node.js

The capability to require() ESM modules in Node.js, initiated by JOYEECHEUNG, marks an incredible milestone. This feature allows packages to be published as ESM-only while still being consumable by CJS codebases with minimal modifications. It helps avoid the async infection (also known as Red Functions) introduced by dynamic import() ESM, which can be pretty hard, if not impossible in some cases, to migrate and adapt.

This feature was recently unflagged and backported to Node.js v22 (and soon v20), which means it should be available to many developers already. Consider the top-down or bottom-up metaphor, this feature actually makes it possible to start ESM migration also from middle-out, as it allows import chains like ESM → CJS → ESM → CJS to work seamlessly.

To solve the interop issue between CJS and ESM in this case, Node.js also introduced a new export { Foo as 'module.exports' } syntax in ESM to export CJS-compatible exports (by this PR). This allows package authors to publish ESM-only packages while still supporting CJS consumers, without even introducing breaking changes (expcet for changing the required Node.js version).

For more details on the progress and discussions around this feature, keep track on this issue.

The Troubles with Dual Formats

While dual CJS/ESM packages have been a quite helpful transition mechanism, they come with their own set of challenges. Maintaining two separate formats can be cumbersome and error-prone, especially when dealing with complex codebases. Here are some of the issues that arise when maintaining dual formats:

Interop Issues

Fundamentally, CJS and ESM are different module systems with distinct design philosophies. Although Node.js has made it possible to import CJS modules in ESM, dynamically import ESM in CJS, and even require() ESM modules, there are still many tricky cases that can lead to interop issues.

One key difference is that CJS typically uses a single module.exports object, while ESM supports both default and named exports. When authoring code in ESM and transpiling to CJS, handling exports can be particularly challenging, especially when the exported value is a non-object, such as a function or a class. Additionally, to make the types correct, we also need to introduce further complications with .d.mts and .d.cts declaration files. And so on…

As I am trying to explain this problem deeper, I found that I actually wish you didn’t even need to be bothered with this problem at all. It’s frankly too complicated and frustrating. If you are just a user of packages, let alone the package authors to worry about that. This is one of the reasons I advocate for the entire ecosystem to transition to ESM, to leave these problems behind and spare everyone from this unnecessary hassle.

Dependency Resolution

When a package has both CJS and ESM formats, the resolution of dependencies can become convoluted. For example, if a package depends on another package that only ships ESM, the consumer must ensure that the ESM version is used. This can lead to version conflicts and dependency resolution issues, especially when dealing with transitive dependencies.

Also for packages that are designed to used with singleton pattern, this might introduce multiple copies of the same package and cause unexpected behaviors.

Package Size

Shipping dual formats essentially doubles the package size, as both CJS and ESM bundles need to be included. While a few extra kilobytes might not seem significant for a single package, the overhead can quickly add up in projects with hundreds of dependencies, leading to the infamous node_modules bloat. Therefore, package authors should keep an eye on their package size. Moving to ESM-only is a way to optimize it, especially if the package doesn’t have strong requirements on CJS.

When Should We Move to ESM-only?

This post does not intend to diminish the value of dual-format publishing. Instead, I want to encourage evaluating the current state of the ecosystem and the potential benefits of transitioning to ESM-only.

There are several factors to consider when deciding whether to move to ESM-only:

New Packages

I strongly recommend that all new packages be released as ESM-only, as there are no legacy dependencies to consider. New adopters are likely already using a modern, ESM-ready stack, there being ESM-only should not affect the adoption. Additionally, maintaining a single module system simplifies development, reduces maintenance overhead, and ensures that your package benefits from future ecosystem advancements.

Browser-targeted Packages

If a package is primarily targeted for the browser, it makes total sense to ship ESM-only. In most cases, browser packages go through a bundler, where ESM provides significant advantages in static analysis and tree-shaking. This leads to smaller and more optimized bundles, which would also improve loading performance and reduce bandwidth consumption for end users.

Standalone CLI

For a standalone CLI tool, it’s no difference to end users whether it’s ESM or CJS. However, using ESM would enable your dependencies to also be ESM, facilitating the ecosystem’s transition to ESM from a top-down approach.

Node.js Support

If a package is targeting the evergreen Node.js versions, it’s a good time to consider ESM-only, especially with the recent require(ESM) support.

Know Your Consumers

If a package already has certain users, it’s essential to understand the dependents’ status and requirements. For example, for an ESLint plugin/utils that requires ESLint v9, while ESLint v9’s new config system supports ESM natively even in CJS projects, there is no blocker for it to be ESM-only.

Definitely, there are different factors to consider for different projects. But in general, I believe the ecosystem is ready for more packages to move to ESM-only, and it’s a good time to evaluate the benefits and potential challenges of transitioning.

How Far We Are?

The transition to ESM is a gradual process that requires collaboration and effort from the entire ecosystem. Which I believe we are on a good track moving forward.

To improve the transparency and visibility of the ESM adoption, I recently built a visualized tool called Node Modules Inspector for analyzing your packages’s dependencies. It provides insights into the ESM adoption status of your dependencies and helps identify potential issues when migrating to ESM.

Here are some screenshots of the tool to give you a quick impression:

Node Modules Inspector - Overview
Node Modules Inspector - Dependency Graph
Node Modules Inspector - Reports like ESM Adoptions and Duplicated Packages

This tool is still in its early stages, but I hope it will be a valuable resource for package authors and maintainers to track the ESM adoption progress of their dependencies and make informed decisions about transitioning to ESM-only.

To learn more about how to use it and inspect your projects, check the repository node-modules-inspector.

Moving Forward

I am planning to gradually transition the packages I maintain to ESM-only and take a closer look at the dependencies we rely on. We also have plenty of exciting ideas for the Node Modules Inspector, aiming to provide more useful insights and help find the best path forward.

I look forward to a more portable, resilient, and optimized JavaScript/TypeScript ecosystem.

I hope this post has shed some light on the benefits of moving to ESM-only and the current state of the ecosystem. If you have any thoughts or questions, feel free to reach out using the links below. Thank you for reading!