Back to Tsx

CJS/ESM interop

notes/node/cjs-esm-interop.md

4.23.115.5 KB
Original Source

CJS/ESM interop

How Node bridges CommonJS and ES modules.

CJS preparse and synthetic namespaces

When ESM imports CJS, Node synthesizes an ESM namespace from static CJS source analysis before evaluating the CJS module.

The lexer implementation changed without changing the supported grammar:

Default import contract

Node's synthetic default export for a CommonJS module is always the complete module.exports value. The translator skips a detected CommonJS property named default and unconditionally assigns the synthetic default and module.exports namespace exports to the complete value (v26.7.0 implementation, contract).

__esModule does not affect this direction of interop. For a CommonJS module that assigns a callable to exports.default, a native ESM default import is the containing object and the callable remains at .default. Node locks that result for canonical Babel and TypeScript output (fixtures, assertions).

ESM importing CJS

Node behaviorPRVerified releases
The ESM load hook can return source for format === 'commonjs'.nodejs/node#50825v20.11.0, v21.3.0
CJS source returned by a load hook can be preparsed into a namespace.nodejs/node#50825, #54769[20.11.0, 21.0.0) and >=21.3.0

The load hook receives import-attributes context and reads CJS source at the boundary (v20.11.0 load.js#L113-L145). The translator then preparses CJS before namespace creation and evaluates via CJSModule._load (v20.11.0 translators.js#L190-L203).

CJS requiring ESM

Node behaviorEvidenceVerified releases or window
CJS require() can load eligible ESM instead of throwing ERR_REQUIRE_ESM.nodejs/node#55085v20.19.0, v22.12.0, v23.0.0
Without require(esm), CJS require() rejects ESM with ERR_REQUIRE_ESM and recommends import().v18.20.8 docs, loaderv18.20.8
An ESM graph containing top-level await throws ERR_REQUIRE_ASYNC_MODULE; callers must use import().v20.20.2 docs, v24.15.0 loaderv20.20.2, v24.15.0
Normal require(esm) stops printing the experimental warning.nodejs/node#56194v20.19.0, v22.13.0, v23.5.0
Extensionless .mjs lookup was broken.nodejs/node#55085, #55590[20.19.0, 20.19.5), [22.12.0, 22.14.0)
Synthetic CJS namespaces expose 'module.exports'.nodejs/node#53848v23.0.0

Node checks the async graph before synchronous evaluation unless --experimental-print-required-tla is enabled (v24.15.0). process.features.require_module reports whether the runtime has require(esm) enabled (v24.15.0 docs, implementation).