notes/node/cjs-esm-interop.md
How Node bridges CommonJS and ES modules.
When ESM imports CJS, Node synthesizes an ESM namespace from static CJS source analysis before evaluating the CJS module.
cjsPreparseModuleExports before building the wrapper (translators.js#L212).translators.js#L393-L418).kIsCachedByESMLoader (translators.js#L368); the CJS loader uses that marker during circular loads (loader.js#L1297-L1308).'module.exports' namespace key (v23.0.0 translators.js#L187).The lexer implementation changed without changing the supported grammar:
cjs-module-lexer (grammar).internalBinding('cjs_lexer') through nodejs/node#61456, backed by Merve (grammar).| Node behavior | PR | Verified releases |
|---|---|---|
The ESM load hook can return source for format === 'commonjs'. | nodejs/node#50825 | v20.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).
| Node behavior | PR | Verified releases or window |
|---|---|---|
CJS require() can load eligible ESM instead of throwing ERR_REQUIRE_ESM. | nodejs/node#55085 | v20.19.0, v22.12.0, v23.0.0 |
Normal require(esm) stops printing the experimental warning. | nodejs/node#56194 | v20.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#53848 | v23.0.0 |