Dual ESM/CJS Package
Publishing & TypeScript
Start Here
Learn how Node.js evaluates ESM and CJS at runtime, why static analysis matters for bundlers, and when each format applies.
Core Problem Navigating the Dual-Package HazardUnderstand why a package can be instantiated twice in one process, how singleton state breaks, and the exports field pattern that prevents it.
Bundle Size Implementing sideEffects CorrectlyConfigure sideEffects in package.json to unlock aggressive dead code elimination across Webpack, Rollup, and esbuild.
Compare the three dominant bundlers for library authors, choose the right one for your project, and emit correct dual ESM/CJS outputs.
Package Surface Subpath Exports and Deep Import PatternsDesign the entry points consumers can import, close dist/ to deep imports, and keep internal specifiers working after publish.
Measure the install footprint consumers actually inherit, classify each dependency correctly, and keep them external to your build output.
Why dual-format publishing matters
JavaScript's module ecosystem is split between CommonJS (CJS) — the legacy synchronous format powering decades of Node.js tooling — and ECMAScript Modules (ESM), the modern static format that enables tree-shaking, top-level await, and native browser support. Library authors must ship both to avoid breaking consumers.
A misconfigured package.json exports field can silently instantiate your
library twice in the same process, corrupting singleton state and breaking framework registries.
Getting this right requires understanding resolution mechanics, TypeScript declaration alignment,
and bundler-specific behaviour.
This site is a practical reference covering every layer of the stack — from Node.js module caches and conditional exports to tsconfig settings, build tooling, and CI validation pipelines.
What you'll learn
- How ESM and CJS execution models differ at runtime
- The exact
exportsfield pattern that prevents dual-package hazards - Tree-shaking: why
sideEffects: falseisn't enough on its own - Barrel file anti-patterns and how they destroy bundle optimisation
- tsconfig options for library authors (
moduleResolution: "bundler", declaration maps) - Build tooling: tsup, esbuild, Rollup for dual ESM/CJS output
- CI/CD matrix testing across ESM and CJS consumer harnesses
- Automated npm publishing with
--provenance, OIDC trusted publishing, and Sigstore attestation - Subpath exports, wildcard patterns, and the
importsfield for internal specifiers - Dependency weight: peer vs regular dependencies, externalising, and auditing the transitive tail
- Source maps and declaration maps that make your package debuggable for consumers
- Publishing to GitHub Packages, private registries, and a local registry for rehearsals
Explore the Guides
ESM vs CJS execution models, Node.js resolution mechanics, the dual-package hazard,
and mastering the package.json exports field.
- Understanding ESM vs CJS
- Navigating the Dual-Package Hazard
- Mastering the exports Field
- Browser vs Node.js Resolution
- Subpath Exports & Deep Imports
Dead code elimination, sideEffects flag configuration, barrel file anti-patterns,
and measuring real bundle impact.
- Implementing sideEffects Correctly
- Eliminating Barrel File Anti-Patterns
- Advanced DCE Techniques
- Optimising Bundle Size
- Reducing Dependency Weight
tsconfig for library distribution, declaration file generation, path aliases, and modern build tools — tsup, esbuild, and Rollup.
- Optimising tsconfig.json
- Modern Build Tools
- Declaration File Generation
- Path Mapping Strategies
- Source Maps & Debugging
Automated release pipelines with GitHub Actions, Sigstore --provenance attestation,
OIDC trusted publishing, and pre-publish validation with publint and attw.
- npm Provenance & Sigstore Attestation
- Automating npm Releases with GitHub Actions
- Validating Packages Before Publish
- Versioning & Changelog Automation
- Private & Alternative Registries
Featured Guides
-
How to Configure Node.js for Native ESM Support
— Enable
"type": "module", pick the right file extension, and avoid the commonERR_REQUIRE_ESMtrap. -
Fixing
require()Errors in Pure ESM Packages — Step-by-step resolution forERR_REQUIRE_ESMwhen consuming a pure-ESM dependency from CJS code. -
Conditional Exports for Development vs Production
— Use the
exportsmap conditions to serve debug builds in dev and optimised builds in prod. -
Why Barrel Files Break Tree-Shaking in Webpack
— Understand why
index.tsre-exports force Webpack to include every module, and how to fix it. - Configuring sideEffects for CSS and Asset Imports — Correctly whitelist CSS and asset files so bundlers don't eliminate them as dead code.
-
Using tsup to Bundle Dual ESM and CJS Outputs
— Zero-config tsup setup that produces correct
.mjs/.cjsoutputs with matching declaration files. -
Generating Accurate
.d.tsFiles with TypeScript 5.4 — Avoid the common declaration-emit pitfalls introduced byisolatedDeclarationsand the new emit modes. - Migrating from tsc to esbuild for Faster Builds — Drop-in migration guide: keep type-checking with tsc, hand transpilation to esbuild, and cut build times by 10–50×.
-
Publishing with npm Provenance in GitHub Actions
— Wire
id-token: writeandnpm publish --provenanceto emit a verifiable Sigstore attestation on every release. -
Configuring OIDC Trusted Publishing for npm
— Publish from GitHub Actions with short-lived
id-tokencredentials instead of a long-livedNPM_TOKEN. - Rollup vs esbuild vs webpack Bundle Size — A measured head-to-head on the same ESM library, with the tree-shaking trade-offs behind each tool's output.
-
Publishing Packages from a pnpm Workspace
— Resolve
workspace:protocol dependencies, order builds, and ship correct exports for each published package. -
Fixing
ERR_PACKAGE_PATH_NOT_EXPORTED— The four causes of the not-exported error, and the manifest change each one needs. -
Top-Level await in Published Packages
— Why one
awaitat module scope makes a package unloadable by every CommonJS consumer. - Choosing Peer Dependencies vs Dependencies — The classification that decides whether a consumer's tree holds one copy of a framework or two.
- Shipping Declaration Maps for Go-to-Definition — Make a consumer's ⌘-click land in your TypeScript source instead of a bare declaration.
- Publishing Scoped Packages to GitHub Packages — The scope-must-match-owner rule behind the 403 that looks like an authentication failure.