Enforcing Strict ESM Resolution via package.json and CLI Flags

Purpose: Eliminate ambiguous module type inference and force Node.js to parse all entry points as ECMAScript Modules without legacy fallback.

Node.js defaults to CommonJS parsing unless explicitly overridden. Adding "type": "module" to the root package.json establishes a strict package boundary, disabling legacy fallback mechanisms. This enforcement aligns with modern Understanding ESM vs CJS Module Formats parsing rules, where implicit resolution heuristics are removed to guarantee deterministic execution.

Configuration Requirements:

  • The --experimental-specifier-resolution=node flag is fully deprecated in v14.13.0+. All import statements must include explicit file extensions (.js, .mjs, .cjs).
  • Directory imports or missing extensions trigger ERR_UNSUPPORTED_DIR_IMPORT.
  • Use NODE_OPTIONS for CI/CD runtime flag isolation. Environment-level flags override local configurations, ensuring consistent pipeline behavior across environments.

Exact Error: ERR_UNSUPPORTED_DIR_IMPORT

Resolving __dirname and __filename Absence in ESM

Purpose: Provide exact runtime configuration and polyfill patterns to replace CJS path globals without external dependencies.

Native ESM removes synchronous CJS globals. import.meta.url returns a file:// protocol string, not a raw filesystem path. Direct string splitting or regex parsing fails across POSIX and Windows environments.

Resolution Pattern:

  1. Convert the file:// URL to an absolute path using url.fileURLToPath().
  2. Pass the result to path.dirname() to extract the parent directory.
  3. Avoid synchronous fs reads on these resolved paths within async ESM contexts to prevent event loop blocking.

Exact Error: ReferenceError: __dirname is not defined

Bridging Legacy CJS Dependencies via createRequire

Purpose: Configure safe, isolated CJS loading within native ESM environments to prevent require() scope pollution.

When consuming unmigrated packages, module.createRequire(import.meta.url) instantiates a localized CJS resolver scoped strictly to the current file. This prevents global require.cache contamination and maintains strict module boundaries. For standard consumption, prefer dynamic import() to leverage async resolution and enable static analysis for tree-shaking.

Isolation Rule: Instantiate createRequire per module. Do not export or share the generated require function across module boundaries. This configuration integrates directly into broader Module System Fundamentals & Dual-Package Resolution strategies, ensuring conditional exports map correctly without fallback ambiguity.

Exact Error: ReferenceError: require is not defined in ES module scope

Step-by-Step Configuration Execution

  1. Declare native ESM scope in package.json Add "type": "module" to the root package.json and remove any conflicting "main": "index.cjs" entries.

  2. Configure strict ESM runtime flags

export NODE_OPTIONS="--no-warnings=ExperimentalWarning --trace-warnings" && node --input-type=module -e "import './app.mjs'"
  1. Implement __dirname polyfill for path resolution
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __dirname = dirname(fileURLToPath(import.meta.url));
  1. Isolate CJS dependency loading
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const legacyPkg = require('legacy-cjs-dep');