modules-packagingAnswer last reviewed July 2026

Let's talk interop. ESM loading CommonJS, CommonJS loading ESM, how does each direction actually work?

A good answer gets into the actual mechanisms, cjs-module-lexer synthesizing named exports and the top-level-await limit on require(esm).

What an AI-prepared candidate might say

Importing CJS from ESM has basically always worked. The module's module.exports becomes the default export, and Node can usually expose named exports too, so import pkg from 'cjs-pkg' works and import { thing } from 'cjs-pkg' tends to work as well. The other direction was forbidden for years, require() on an ES module threw ERR_REQUIRE_ESM, and the workaround was dynamic import(), which gives you back a promise and works from CJS. But modern Node has largely removed that restriction, I think, so require() of an ES module works now in current versions. The friction that's left is mostly around default exports. Depending on how the package was authored or transpiled, you sometimes have to go through .default to reach the actual function, and that's where the double-default shape comes from, or something like that.

Senior
Locked

How cjs-module-lexer conjures named exports out of CJS, what require(esm) actually hands you back, and the one thing that still makes it throw.

Unlock the depth
Staff
Locked

A playbook for the day a dependency goes ESM-only, the TypeScript setting that silently compiles away your import() bridge, and the boundary-file habit that keeps interop from spreading.

Unlock the depth
Follow-up chain
Let's talk interop. ESM loading CommonJS, CommonJS loading ESM, how does each direction actually work? | NodeBook