ESM and CJS Dual Package Output
Generate both ES module and CommonJS builds with correct package.json exports field configuration.
ESM and CJS Dual Package Output is a free React Academy lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the React Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
What is ESM?
ESM (ECMAScript Modules) is the official JavaScript module standard using import and export syntax. ESM is statically analyzable — bundlers can determine at build time exactly which exports are used, enabling tree-shaking. Modern browsers and Node.js both support ESM natively.
What is CJS?
CJS (CommonJS) uses require() and module.exports syntax. It was Node.js's original module system and is still needed for compatibility with older Node.js environments, Jest (which historically used CJS), and code that uses require(). CJS is dynamically evaluated, making tree-shaking harder.
Dual Package: Ship Both Formats
A modern npm package should ship both ESM and CJS to maximize compatibility. ESM consumers (Vite, Next.js, modern bundlers) get tree-shakable imports. CJS consumers (older Node.js scripts, Jest without configuration) get require() compatibility. The package.json exports field tells Node and bundlers which format to use.
The exports Field
The exports field in package.json is the modern way to define conditional entry points. Example: { '.': { 'import': './dist/esm/index.js', 'require': './dist/cjs/index.js', 'types': './dist/esm/index.d.ts' } }. Bundlers and Node.js 12+ read exports to select the correct format automatically.
Legacy main and module Fields
Older tooling does not understand the exports field. For compatibility, also set: main pointing to the CJS output (legacy Node require fallback), and module pointing to the ESM output (a hint for webpack/rollup, non-standard but widely supported). Modern tooling prefers exports, legacy tooling falls back to main/module.
type: module Implications
Setting "type": "module" in package.json makes all .js files in the package treated as ESM. If you ship dual format, you then need explicit extensions: use .mjs for ESM files and .cjs for CJS files when type is module, or vice versa. tsup handles this automatically.
.mjs and .cjs Extensions
Using .mjs (ESM) and .cjs (CJS) file extensions explicitly marks the format regardless of the type field. This avoids ambiguity. tsup can output: index.js (ESM) and index.cjs (CJS) when format is ['esm', 'cjs'] and no type field is set, matching the most common convention.
The Dual Package Hazard
When a package provides both ESM and CJS formats, a consumer's bundler might load both versions in the same process — for example, the ESM version for the main app and the CJS version for a Jest test. If the package has module-level state (like a React context), both instances have independent state. This is the dual package hazard.
Mitigating the Dual Package Hazard
To mitigate the hazard: keep module-level state out of your library (no singleton patterns), use the exports conditions precisely to ensure only one format loads, and document that tests should configure their bundler to use ESM. The hazard is a concern mainly for libraries with shared singletons.
Testing Dual Output
Verify both formats work after building. Test CJS: node -e "const lib = require('./dist/cjs/index.js'); console.log(lib)". Test ESM: node --input-type=module --eval "import { Component } from './dist/esm/index.js'; console.log(Component)". Both should resolve without errors before publishing.
Exports for Multiple Entry Points
The exports field supports multiple entry points: { '.': { import: './dist/esm/index.js', require: './dist/cjs/index.js' }, './utils': { import: './dist/esm/utils.js', require: './dist/cjs/utils.js' } }. Consumers then import from 'your-lib' or 'your-lib/utils' and get the correct format.
package.json exports Field
What is the primary purpose of the exports field in a library's package.json?
Lesson Recap: Dual Package Output
ESM uses import/export and enables tree-shaking. CJS uses require() for Node.js compatibility. Ship both via the package.json exports field with import/require conditions. Legacy fallbacks: main (CJS) and module (ESM). Use .mjs/.cjs extensions or type: module for explicit format marking. Test both formats with node CLI after building. Beware the dual package hazard with singleton state.
Frequently asked questions
Is the “ESM and CJS Dual Package Output” lesson free?
Yes — the full text of “ESM and CJS Dual Package Output” is free to read here on the web, and the React Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the React Academy course, upgrade to CoddyKit PRO.
What will I learn in “ESM and CJS Dual Package Output”?
Generate both ES module and CommonJS builds with correct package.json exports field configuration. You practise React Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start React Academy?
No prior experience is required. React Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “ESM and CJS Dual Package Output” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this React Academy lesson?
Yes. Every React Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Bundling with Rollup and tsup for Libraries
- ESM and CJS Dual Package Output
- Peer Dependencies and Tree Shaking
- Publishing to npm and Semantic Versioning