How to Configure package.json for Dual Modules
π¨ Exact Symptoms & Pipeline Failures
Identify these errors during build, runtime execution, or type-checking phases:
ERR_REQUIRE_ESM: Must use import to load ES ModuleSyntaxError: Cannot use import statement outside a moduleModule not found: Error: Can't resolve 'package-name' (ESM/CJS mismatch)- Bundler warnings:
'package.json' does not have an 'exports' field or missing conditional resolution
π Root Cause Analysis
Node.js runtimes and modern bundlers strictly enforce the exports map for deterministic module resolution. When a library publishes dual formats without explicit conditional exports, consumers fall back to the legacy main field or default to a single syntax. This mismatch forces the execution context to load the wrong module format, triggering runtime crashes, tree-shaking inefficiencies, and TypeScript resolution failures. Properly aligning Understanding package.json Fields with Nodeβs ESM loader is mandatory to prevent these resolution collisions.
π οΈ Resolution & Configuration Patch
Apply the following package.json configuration to establish deterministic dual-module resolution:
{
"type": "module",
"exports": {
".": {
"import": {
"types": "./dist/esm/index.d.ts",
"default": "./dist/esm/index.js"
},
"require": {
"types": "./dist/cjs/index.d.cts",
"default": "./dist/cjs/index.cjs"
},
"default": "./dist/cjs/index.cjs"
},
"./package.json": "./package.json"
},
"main": "./dist/cjs/index.cjs",
"module": "./dist/esm/index.js",
"types": "./dist/esm/index.d.ts"
}
Implementation Steps:
- Set Baseline ESM: Add
"type": "module"to treat.jsfiles as ESM by default. - Define Conditional Exports: Map
"import"to ESM artifacts and"require"to CJS artifacts. Include"types"for TypeScript consumers and"default"as a safe fallback. - Retain Legacy Fields: Keep
"main"(CJS) and"module"(ESM) for older toolchains that bypass theexportsmap, adhering to established Core JavaScript Package Workflows for backward compatibility. - Align Build Pipeline: Configure Rollup, esbuild, or TypeScript to output distinct
.cjsand.mjsfiles (or separatecjs/andesm/directories) matching theexportspaths.
π₯οΈ CLI Validation & Debug Commands
Verify resolution in an isolated consumer environment before publishing:
# Validate ESM resolution
node -e "import('your-pkg').then(m => console.log('ESM OK:', typeof m.default))"
# Validate CJS resolution
node -e "console.log('CJS OK:', typeof require('your-pkg').default)"
# Debug exact resolution paths (Node.js 14+)
node --experimental-vm-modules -e "import.meta.resolve('your-pkg')"
Troubleshooting Checklist:
- File Extensions: Ensure no
.jsfiles are ambiguously parsed. Use explicit.cjs/.mjs. exportsSyntax: Verify JSON validity, correct path casing, and absence of trailing commas.- Node Version: Run
node -vto confirm LTS alignment; older runtimes may require--experimental-modules.
π‘οΈ Prevention & CI/CD Guardrails
- Pin Node.js Versions: Lock major/minor versions in CI/CD to prevent resolution algorithm shifts between LTS releases.
- Expose Package Metadata: Always include
"./package.json": "./package.json"inexportsto prevent metadata read errors during ESM/CJS resolution. - Automate Compatibility Testing: Run pre-publish scripts that instantiate both
importandrequireconsumers against the built artifacts. - Enforce Extension Discipline: Never mix
.jsfor dual formats. Route explicitly via.cjs/.mjsor directory-based exports to eliminate parser ambiguity.
β Frequently Asked Questions
Do I still need main and module if I use exports?
Yes. Legacy bundlers and Node.js versions (<12.16) ignore exports and rely on main (CJS) and module (ESM). Retaining these fields prevents resolution failures in older ecosystems.
How do I handle TypeScript type resolution with dual modules?
Map the "types" condition inside each exports branch. Point import to .d.ts files and require to .d.cts files to ensure type checkers resolve the correct declarations without cross-contamination.
Why does ERR_REQUIRE_ESM occur even with exports configured?
This happens when a CJS consumer attempts to require() an ESM-only file, or the exports map lacks a require condition. Ensure your exports explicitly defines a require path pointing to a valid CJS artifact.
Can I use a single .js file for both formats?
No. Node.js treats .js as CJS by default unless "type": "module" is set. Dual publishing requires separate artifacts (.cjs/.mjs) or distinct directories to prevent syntax parsing conflicts.