Diagnosing ERR_REQUIRE_ESM in Legacy Node.js Loaders

Legacy tooling triggers ERR_REQUIRE_ESM: require() of ES Module not supported when synchronous require() calls intersect with modern ESM boundaries. Isolate and resolve the fault by:

  • Tracing Stack Traces: Pinpoint dynamic require() invocations in pre-ESM loaders. Use --trace-sync-io or node --inspect to halt execution at the exact resolution boundary.
  • Differentiating Loader Failures: Distinguish native Node.js ESM loader rejections from third-party bundler fallbacks. Native failures throw synchronously at startup; bundler fallbacks typically manifest during tree-shaking or runtime chunk evaluation.
  • Mapping Incompatible Dependencies: Execute npx depcruise --include-only "^src" --output-type json src/ | jq '.[] | select(.moduleSystem == "esm" and .dependencyType == "require")' to map legacy dependency trees against ESM-incompatible packages.

Overriding tsconfig.json Module Resolution for Mixed Codebases

Bridge legacy CommonJS outputs with modern ESM type declarations without breaking downstream consumers. Address TS2307: Cannot find module or its corresponding type declarations by:

  • Enforcing Strict Resolution: Set moduleResolution to node16 or nodenext. This forces the compiler to evaluate package.json exports and types fields accurately, preventing implicit .d.ts resolution failures.
  • Preventing Default Import Mismatches: Enable esModuleInterop: true and allowSyntheticDefaultImports: true to align legacy transpilation output with modern import syntax.
  • Isolating Ambient Declarations: Override typeRoots to point exclusively to your project’s node_modules/@types and local types/ directory. This bypasses ambient conflicts from globally installed or hoisted legacy type definitions.

Patching package.json Exports for Browser/Node Divergence

Runtime resolution collisions occur when legacy monorepos lack explicit conditional exports. Misconfigured fallbacks frequently trigger ERR_UNSUPPORTED_DIR_IMPORT: Directory import is not supported. Resolve this by:

  • Defining Explicit Conditions: Map import and require keys directly to their respective build artifacts. This bypasses the Browser vs Node.js Module Resolution divergence that causes legacy bundlers to misinterpret Node-specific conditional exports.
  • Removing Ambiguous Fallbacks: Delete top-level main and module keys. Modern resolvers prioritize exports; retaining legacy fallbacks forces older toolchains into unpredictable resolution paths.
  • Aligning Export Paths: Ensure exports paths match exact build output directories (e.g., ./dist/esm/, ./dist/cjs/). Path aliasing failures occur when virtual paths diverge from physical artifact locations. Apply Module System Fundamentals & Dual-Package Resolution principles to prevent module duplication when CJS and ESM entry points coexist in shared workspaces.

CI/CD Pipeline & DevOps Cache Invalidation for Dual Outputs

Stale CJS/ESM artifacts cause environment-specific type conflicts during deployment, manifesting as MODULE_NOT_FOUND: Cannot resolve package.json exports condition. Enforce deterministic builds with:

  • Strict Pre-Build Cleanup: Execute rm -rf dist/ .tscache/ node_modules/.cache/ before compilation to eliminate cross-pollinated artifacts from previous pipeline runs.
  • Environment-Specific Resolution Flags: Inject NODE_OPTIONS="--experimental-vm-modules --no-warnings" or NODE_OPTIONS="--conditions=node" into legacy CI runners to force explicit module resolution paths.
  • Artifact Integrity Validation: Generate SHA-256 checksums for dist/esm/ and dist/cjs/ outputs post-build. Compare against deployment manifests to verify artifact parity before publishing.

Step-by-Step Resolution Workflow

  1. Isolate legacy entry points with explicit extensions
mv legacy-entry.js legacy-entry.cjs && sed -i 's/require("\.\/legacy-entry")/require("\.\/legacy-entry.cjs")/g' src/bootstrap.js
  1. Configure strict conditional exports in package.json
{
  "exports": {
    ".": {
      "node": {
        "import": "./dist/esm/index.mjs",
        "require": "./dist/cjs/index.cjs"
      },
      "browser": "./dist/browser/index.js",
      "types": "./dist/types/index.d.ts"
    }
  }
}
  1. Apply TypeScript module resolution override
{
  "compilerOptions": {
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "esModuleInterop": true,
    "verbatimModuleSyntax": true,
    "resolveJsonModule": true
  }
}
  1. Validate dual-package integrity without emitting
tsc --noEmit --project tsconfig.json && node --check dist/esm/index.mjs && node --check dist/cjs/index.cjs