Back to core workflows Fix dependency resolution Tune package metadata Jump to monorepo patterns

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 Module
  • SyntaxError: Cannot use import statement outside a module
  • Module 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:

  1. Set Baseline ESM: Add "type": "module" to treat .js files as ESM by default.
  2. Define Conditional Exports: Map "import" to ESM artifacts and "require" to CJS artifacts. Include "types" for TypeScript consumers and "default" as a safe fallback.
  3. Retain Legacy Fields: Keep "main" (CJS) and "module" (ESM) for older toolchains that bypass the exports map, adhering to established Core JavaScript Package Workflows for backward compatibility.
  4. Align Build Pipeline: Configure Rollup, esbuild, or TypeScript to output distinct .cjs and .mjs files (or separate cjs/ and esm/ directories) matching the exports paths.

πŸ–₯️ 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 .js files are ambiguously parsed. Use explicit .cjs/.mjs.
  • exports Syntax: Verify JSON validity, correct path casing, and absence of trailing commas.
  • Node Version: Run node -v to 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" in exports to prevent metadata read errors during ESM/CJS resolution.
  • Automate Compatibility Testing: Run pre-publish scripts that instantiate both import and require consumers against the built artifacts.
  • Enforce Extension Discipline: Never mix .js for dual formats. Route explicitly via .cjs/.mjs or 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.