The Great Migration: CJS to ESM in the npm Ecosystem
TL;DR
ESM is the standard. CJS is the compatibility layer. In 2026, ~65% of new npm packages ship ESM-first or ESM-only. Major packages that went ESM-only (chalk v5, node-fetch v3, got v12, p-queue v7, nanoid v4, sindresorhus's entire portfolio) forced millions of developers to understand the difference. Node.js 22 supports both, but the friction of mixing CJS and ESM is still the #1 source of confusing "ERR_REQUIRE_ESM" errors. Here's the definitive state of the migration in 2026.
Key Takeaways
- 65% of new npm packages — ship ESM first or ESM-only in 2026
- Sindresorhus effect — one developer's ESM-only decision affected ~1000 packages
- Dual packages — ~40% of popular packages ship both CJS + ESM via package.json exports
- Node.js 22 — full ESM support, top-level await,
--experimental-require-modulefor forcing ESM - Bundlers handle it — Vite, esbuild, Rollup resolve CJS/ESM automatically in most cases
CJS vs ESM: The Fundamental Difference
// CommonJS (CJS) — the original Node.js module system (2009)
// ✅ Synchronous, works in all Node.js versions
// ❌ No static analysis, no tree-shaking
const express = require('express');
const { something } = require('./utils');
module.exports = { myFunc };
// ES Modules (ESM) — the JavaScript standard (ES2015, Node.js 12+)
// ✅ Static, tree-shakeable, works in browsers
// ✅ Top-level await
// ❌ Must use .mjs extension OR "type": "module" in package.json
import express from 'express';
import { something } from './utils.js'; // .js extension required!
export { myFunc };
Why This Migration Took So Long
The CJS → ESM migration is still ongoing in 2026 because of a fundamental asymmetry:
CJS can require() CJS packages: ✅
ESM can import CJS packages: ✅ (Node.js wraps CJS)
CJS can require() ESM packages: ❌ (ERR_REQUIRE_ESM!)
ESM can import ESM packages: ✅
The problem: if package A (CJS) depends on package B (ESM-only),
package A can't upgrade B without becoming ESM itself.
This created a "migration blocker" cascade:
chalk v5 went ESM-only → many tools couldn't upgrade chalk → stayed on v4
The Packages That Forced the Migration
Sindresorhus's Portfolio (~1000 packages)
# These popular packages went ESM-only:
chalk@5 → ESM only (chalk@4 = last CJS version)
got@12 → ESM only (got@11 = last CJS version)
p-queue@7 → ESM only
execa@7 → ESM only (execa@8 now CJS+ESM again!)
is-stream@3 → ESM only
del@7 → ESM only
tempy@3 → ESM only
globby@13 → ESM only
# Package authors' message: "Use CJS version 4/5/6 until you migrate to ESM"
node-fetch v3
// node-fetch v2 (CJS — most projects still use this or native fetch)
const fetch = require('node-fetch'); // Works in CJS
// node-fetch v3 (ESM only)
import fetch from 'node-fetch'; // Requires ESM
// 2026 recommendation: Use native fetch (Node.js 18+)
// No import needed — fetch is global in Node.js 18+
const response = await fetch('https://api.example.com');
The Package.json Exports Field (Dual Packages)
The industry settled on a standard: dual-package pattern via exports:
// package.json — dual CJS + ESM package (the 2026 standard)
{
"name": "my-package",
"version": "1.0.0",
"type": "module",
"exports": {
".": {
"import": "./dist/index.js", // ESM entry
"require": "./dist/index.cjs", // CJS entry
"types": "./dist/index.d.ts" // TypeScript declarations
},
"./utils": {
"import": "./dist/utils.js",
"require": "./dist/utils.cjs"
}
},
"main": "./dist/index.cjs", // Fallback for old Node.js
"module": "./dist/index.js", // Bundler hint (Rollup, webpack)
"types": "./dist/index.d.ts"
}
# tsup — builds both CJS and ESM from one command
npx tsup src/index.ts --format cjs,esm --dts --clean
# Output:
# dist/index.js — ESM
# dist/index.cjs — CommonJS
# dist/index.d.ts — TypeScript declarations
# dist/index.d.cts — CTS declarations (for CJS consumers)
The ERR_REQUIRE_ESM Error (And Fixes)
# The most common error when mixing CJS and ESM:
Error [ERR_REQUIRE_ESM]: require() of ES Module /node_modules/chalk/source/index.js
from /my-app/src/logger.js not supported.
Instead change the require of chalk in /my-app/src/logger.js to a dynamic import()
which is available in all CommonJS modules.
// Fix 1: Stay on CJS version
// package.json: "chalk": "^4.1.2" // NOT v5
// Fix 2: Convert your file to ESM
// Rename logger.js → logger.mjs OR add "type": "module" to package.json
import chalk from 'chalk';
// Fix 3: Dynamic import (in CJS files)
// Works but ugly — only when needed
const chalk = await import('chalk');
// OR:
const { default: chalk } = await import('chalk');
chalk.green('Hello');
// Fix 4: Use an alternative that ships CJS
// chalk v4 → kleur, picocolors (CJS + ESM, tiny)
import { green } from 'kleur/colors';
Node.js Project Configuration for ESM
// package.json — opting into ESM
{
"type": "module", // .js files are now ESM
"scripts": {
"dev": "tsx watch src/index.ts", // tsx handles ESM automatically
"build": "tsc"
}
}
// tsconfig.json — ESM-compatible TypeScript config
{
"compilerOptions": {
"module": "NodeNext", // NodeNext = ESM-aware module resolution
"moduleResolution": "NodeNext",
"target": "ES2022"
}
}
// Critical: In ESM TypeScript, you must include .js extensions
// (yes, .js even when the source is .ts — TypeScript resolves to the compiled output)
import { calculateScore } from './health-score.js'; // ✅ .js required
import { db } from '../lib/db.js'; // ✅ .js required
// import { db } from '../lib/db'; // ❌ Fails in NodeNext mode
The 2026 State of Popular Packages
| Package | Current | CJS Support | Notes |
|---|---|---|---|
| chalk | v5 | ❌ (v4 = last CJS) | Use kleur/picocolors for CJS |
| got | v14 | ✅ Dual | v14 restored CJS after community pressure |
| node-fetch | v3 | ❌ | Use native fetch (Node.js 18+) |
| execa | v9 | ✅ Dual | Restored CJS in v8 after feedback |
| nanoid | v5 | ✅ Dual | v5 added CJS back after v4 ESM-only |
| p-queue | v8 | ✅ Dual | Restored CJS |
| glob | v10 | ✅ Dual | Core Node.js-adjacent packages tend dual |
| p-limit | v6 | ✅ Dual | Restored |
The pattern: packages often went ESM-only, faced community pushback, then added CJS back as dual packages. The pure ESM-only holdouts are mostly opinionated smaller utilities.
Practical Advice for 2026
# Check if a package supports your module system
cat node_modules/chalk/package.json | grep -A 10 '"exports"'
# For new projects:
# ESM: "type": "module", .js files are ESM
# CJS: no "type" field (default), .js = CJS
# For CLI tools/scripts: use tsx (handles both transparently)
tsx my-script.ts
# For library publishing: ship both via tsup
npx tsup src/index.ts --format cjs,esm --dts
# For bundled apps (Next.js, Vite): don't worry about it
# Bundlers resolve CJS/ESM automatically
Compare module ecosystem package health on PkgPulse.
See the live comparison
View rollup vs. webpack on PkgPulse →