Resolving ERR_REQUIRE_ESM in Node.js CJS Consumers

When CommonJS consumers attempt to dynamically import ESM-only artifacts, Node.js throws ERR_REQUIRE_ESM. To prevent runtime crashes, enforce dual-format emission in your build pipeline. Configure tsup with format: ['cjs', 'esm'] to generate parallel outputs. Ensure the root package.json either omits the "type" field or explicitly sets it to "module" only if conditional exports are strictly ordered. Always validate the exports field to prioritize the require condition, guaranteeing synchronous resolution for legacy consumers.

Enforcing Strict Dual-Format Emission via tsup.config.ts

Module graph traversal requires deterministic output paths to prevent tree-shaking conflicts and resolution collisions. When evaluating bundler trade-offs for dual-format output, understanding how Modern Build Tools: tsup, Rollup, and esbuild handle module graph traversal is critical for avoiding resolution deadlocks.

Configure tsup.config.ts to emit both formats simultaneously:

  • Use format: ['esm', 'cjs'] array syntax (equivalent to the --format esm,cjs CLI flag).
  • Apply splitting: true to enable dynamic import chunking without duplicating shared logic.
  • Set clean: true to purge stale artifacts before each build, preventing cross-format file collisions.

Polyfilling Node Globals for Cross-Environment Bundles

ESM targets lack implicit CommonJS globals, triggering ReferenceError: __dirname is not defined or process.env is undefined in Node.js environments. Resolve missing references by enabling shims: true in your tsup configuration. This injects __dirname, __filename, and require polyfills directly into the ESM output.

Inject environment variables at compile time using:

define: { 'process.env.NODE_ENV': JSON.stringify('production') }

Avoid --no-bundle unless targeting pure passthrough, as it bypasses shim injection and leaves runtime globals unresolved.

Optimizing Declaration File Generation with dts-resolve

Publishing dual-format libraries requires isolated type resolution to prevent broken references and circular dependency warnings. Activate --dts-resolve to trace external type definitions and inline them correctly. Exclude node_modules via external: ['*'] to prevent bundling third-party declarations. For isolated type builds, configure dts: { resolve: true }. Foundational compiler settings that dictate module emission behavior are documented in the broader TypeScript Configuration & Build Tooling reference, which aligns with tsup’s underlying esbuild compiler.

Validating package.json Exports for Dual-Format Resolution

Node.js module resolution fails with ERR_MODULE_NOT_FOUND when conditional exports lack explicit extension mapping. Ensure deterministic resolution by mapping import to ./dist/index.mjs and require to ./dist/index.cjs. Always include a types field pointing to ./dist/index.d.ts to satisfy TypeScript’s module resolution algorithm without fallback ambiguity.

Step-by-Step Resolution

  1. Initialize tsup configuration with explicit dual-format array
import { defineConfig } from 'tsup';

export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
splitting: true,
clean: true
});
  1. Configure package.json exports map for deterministic resolution
"exports": {
  ".": {
    "import": "./dist/index.mjs",
    "require": "./dist/index.cjs",
    "types": "./dist/index.d.ts"
  }
}
  1. Execute build with strict external dependency resolution
npx tsup --external react,react-dom --dts-resolve --minify
  1. Verify output integrity and module resolution
node --check dist/index.cjs && node --check dist/index.mjs