
Surviving the CJS vs ESM War in Next.js
Digging into the CommonJS vs ES Modules conflict that causes "Named export not found". A complete guide from package.json exports to transpilePackages.

Digging into the CommonJS vs ES Modules conflict that causes "Named export not found". A complete guide from package.json exports to transpilePackages.
Clicked a button, but the parent DIV triggered too? Events bubble up like water. Understand Propagation and Delegation.

Anatomy of the GCC pipeline. Preprocessor, Compiler, Assembler, and Linker. What happens when you type `gcc main.c`.

I updated the database, but the page still shows old data. We analyze the powerful (and evil) caching mechanism of Next.js 13+ in 4 layers, compare it with React Query, and share practical debugging strategies.

Understanding Lexical Scoping. How React Hooks (useState) rely on Closures. Memory management and common pitfalls.

It was a peaceful afternoon. I just bumped a useful open-source library version from 1.2.0 to 1.3.0.
Suddenly, the Next.js build halted, spewing red error text everywhere.
SyntaxError: Named export 'foo' not found. The requested module 'awesome-lib' is a CommonJS module, which may not support all module.exports as named exports.
CommonJS modules can always be imported via the default export, for example using:
import pkg from 'awesome-lib';
const { foo } = pkg;
"Wait, isn't import { foo } from 'awesome-lib' the standard syntax?"
The official docs say Named Exports are supported. It works fine on my local dev server (npm run dev), but fails only in production build (npm run build).
Behind this error lies the biggest and most tiresome war in the JavaScript ecosystem: The War between CommonJS (CJS) and ES Modules (ESM).
JavaScript originally had no module system. Then Node.js came along and created CommonJS (CJS) for server-side JS. This is the require() and module.exports we know.
// CJS (Language of Node.js)
const React = require('react');
module.exports = function app() { ... }
Meanwhile, browsers and the modern web said, "We need an official standard too," and introduced ES Modules (ESM) in ES6 (2015). This is import and export.
// ESM (Language of the Web)
import React from 'react';
export default function app() { ... }
The problem is they are not compatible.
Next.js is a web framework, so it prefers ESM. But it also runs on Node.js (Server), and thousands of npm packages are still written in CJS. Next.js is a "Hybrid" monster attempting to mix these two oil-and-water standards.
The most frequent issue arises when library authors "kindly" try to provide both versions. This is called a Dual Package.
Look at package.json of awesome-lib:
{
"name": "awesome-lib",
"main": "./dist/index.js", // CJS version
"module": "./dist/index.mjs" // ESM version
}
Theoretically, Next.js should be smart enough to pick the module version because it's modern.
However, depending on Webpack config, Next.js version, and type: module settings, this selection logic often gets twisted.
Especially with Server Components, it gets messier.
If the ESM version (index.mjs) has export const foo = ..., but the CJS version (index.js) is implemented slightly differently like module.exports = { foo: ... }?
When the build tool mistakenly picks the CJS file but tries to parse it with ESM syntax (import { foo }), you get the Named export not found error.
We can't fix library code, so we fix our Next.js config.
transpilePackages (The Silver Bullet)Introduced in Next.js 13.1, this option forces Next.js to bring specific packages into its own build pipeline (Babel/SWC) and re-compile them. It's like telling Next.js: "Treat this package (CJS) as if it's my source code and build it again!" Next.js will then handle the ESM conversion compatibility.
/** @type {import('next').NextConfig} */
const nextConfig = {
transpilePackages: ['awesome-lib', 'old-legacy-ui'],
};
module.exports = nextConfig;
This single line resolves most CJS compatibility issues.
This is what the error log suggested. If Named Import fails, import the whole thing (Default Import) and then destructure.
// ❌ Error
import { foo } from 'awesome-lib';
// ✅ Works
import pkg from 'awesome-lib';
const { foo } = pkg;
It looks ugly, but it's the fastest fix if you need to build NOW. However, Tree Shaking might fail with this method, meaning the entire library code could end up in your bundle even if you only use foo.
ssr: false)If the library is only used in the browser, you can exclude it from Server-Side Rendering (SSR) entirely.
import dynamic from 'next/dynamic';
const AwesomeComponent = dynamic(
() => import('awesome-lib').then(mod => mod.AwesomeComponent),
{ ssr: false }
);
Since Node.js won't try to touch this file during server build, CJS conflicts are avoided.
exports field)If you are maintaining an internal library? Please stop relying on main and module fields. They are relics.
Use the Conditional Exports (exports) field, standardized since Node.js 12.16.
{
"name": "my-library",
"exports": {
".": {
"import": "./dist/index.mjs", // For ESM (import)
"require": "./dist/index.js", // For CJS (require)
"default": "./dist/index.js" // Fallback
}
}
}
The exports field is a strict rulebook explicitly defining "which file to take in which environment."
Modern tools like Next.js prioritize exports over main. Correctly setting this prevents 99% of "Dual Package Hazards".
.mjs and .cjsExtensions tell the identity of the file.
.mjs: "I am definitely ESM. I use import/export. Don't argue, Node.js.".cjs: "I am definitely CJS. I use require.".js: "I depend on the type field in package.json." (Default is CJS).Recently, using explicit .mjs and .cjs is trending. It removes guesswork for build tools.
If your project has "type": "module" in package.json, all .js files are treated as ESM. If you have a legacy config file (like next.config.js) using require(), it will break. In that case, rename it to next.config.cjs.
The JavaScript ecosystem is clearly moving towards ESM (ES Modules).
New runtimes like Deno or Bun treat CJS as legacy from day one.
The React ecosystem sees more ESM-only packages (e.g., node-fetch v3, d3).
But due to the massive legacy accumulated over 10 years in npm, CommonJS will likely survive as a zombie for another decade, haunting us.
Your best strategy:
from '...') for new code.transpilePackages.exports field.To win this war, you must know your enemy (CJS) and know yourself (ESM).