Fixing require() Errors in Pure ESM Packages
Resolve ERR_REQUIRE_ESM when CommonJS consumers load pure ESM packages. Configure conditional exports, create dynamic import wrappers, and validate with Node.js --conditions flags.
Diagnosing ERR_REQUIRE_ESM in Node.js Runtime
When a CommonJS consumer attempts synchronous loading of an ESM-only dependency, Node.js terminates execution with:
ERR_REQUIRE_ESM: require() of ES Module /path/to/pkg/index.js from /path/to/cjs.js not supported.
Trace the stack to the initiating require() call in legacy tooling or Node scripts. This failure occurs when mismatched resolution paths trigger silent state duplication or require() failures in hybrid environments. Understanding Navigating the Dual-Package Hazard clarifies how these resolution mismatches manifest across package boundaries. Identify missing or misordered exports conditions in the target package. Differentiate between Node.js v18+ native ESM enforcement, which strictly blocks require() of .mjs or "type": "module" packages, and older polyfill behaviors that may mask these failures with inconsistent fallbacks.
Implementing Conditional Exports for Safe Routing
Configure package.json to explicitly block ESM resolution for CJS consumers while maintaining pure ESM distribution. The underlying Node.js module resolution algorithm evaluates the exports field before falling back to legacy fields. Referencing Module System Fundamentals & Dual-Package Resolution demonstrates how exports fields strictly override legacy fallbacks and enforce deterministic routing.
Apply these configuration rules:
- Prioritize the
requirecondition overimportin fallback chains to ensure deterministic routing. - Remove the legacy
mainfield to prevent implicit CJS fallback into ESM code. - Use
./subpath exports to restrict deep imports and enforce strict boundary checks.
The exports.require condition mapping must explicitly point to a valid CJS artifact or explicitly throw if pure ESM distribution is enforced.
Runtime Patching via Dynamic Import Wrappers
When downstream CJS environments attempt to load pure ESM code without proper routing, they trigger:
SyntaxError: Cannot use import statement outside a module
Provide a synchronous-compatible interface for downstream CJS environments without altering the core ESM package:
- Wrap ESM entry points in async factory functions to defer module evaluation until runtime execution.
- Cache
dynamic import()results in module-level variables to prevent redundant module loading and state fragmentation. - Avoid top-level
awaitin wrapper modules to maintain CJS compatibility. Expose an explicitinit()method instead, allowing consumers to control initialization timing.
CI/CD Validation with Node.js Condition Flags
Automate pre-publish verification of dual-resolution paths using native Node.js testing utilities. Execute test suites with explicit condition flags to force the resolver to evaluate specific export maps:
node --conditions=import test/esm.test.js && node --conditions=require test/cjs.test.js
The --conditions=import / --conditions=require flags validate that both resolution paths resolve correctly before publication. Integrate publint and attw into prepack lifecycle hooks to validate package structure and catch misconfigured routing. Enforce strict module boundary checks in platform deployment pipelines to prevent runtime resolution failures in production environments.
Step-by-Step Resolution
- Audit package.json for conflicting type and exports fields
grep -E '"type"|"exports"|"main"' package.json && rm -rf node_modules && npm install
- Implement explicit conditional routing in exports
{
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./package.json": "./package.json"
}
}
- Create dynamic import wrapper for legacy CJS entry
module.exports = async () => { const mod = await import('./dist/index.mjs'); return mod; };
- Validate resolution paths with Node.js condition flags
node --conditions=import test/esm.test.js && node --conditions=require test/cjs.test.js