How to Configure Node.js for Native ESM Support
Enforce strict ESM resolution in Node.js via package.json and CLI flags, polyfill __dirname and __filename with import.meta.url, and bridge legacy CJS dependencies with createRequire.
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=nodeflag 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_OPTIONSfor 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:
- Convert the
file://URL to an absolute path usingurl.fileURLToPath(). - Pass the result to
path.dirname()to extract the parent directory. - Avoid synchronous
fsreads 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
-
Declare native ESM scope in package.json Add
"type": "module"to the rootpackage.jsonand remove any conflicting"main": "index.cjs"entries. -
Configure strict ESM runtime flags
export NODE_OPTIONS="--no-warnings=ExperimentalWarning --trace-warnings" && node --input-type=module -e "import './app.mjs'"
- Implement __dirname polyfill for path resolution
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __dirname = dirname(fileURLToPath(import.meta.url));
- Isolate CJS dependency loading
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const legacyPkg = require('legacy-cjs-dep');