CommonJS vs ESM
CJS и ESM имеют фундаментально разную семантику: CJS — динамический runtime require с копированием значений, ESM — статический compile-time граф с live bindings — это объясняет разницу в tree-shaking, circular deps и interop.
// CommonJS: динамический, синхронный, кэшируется по realpathconst b = require('./b.cjs'); // синхронный вызовconsole.log('a loaded');module.exports = { name: 'a', bName: b.name };
// require() можно вызывать условно / в функции:if (process.env.DEBUG) { const debug = require('./debug'); // lazy loading — OK в CJS}
// Circular deps в CJS: частично заполненный объект (не ошибка!)// a.cjs: exports.fn = () => b.fn()// b.cjs: const a = require('./a'); → a = {} (пустой на момент вызова!)// → тихий баг: a.fn === undefined во время инициализации b// ESM: статический, async, live bindings// Статические imports анализируются ДО выполнения кода:import { name } from './b.mjs';// Нельзя: if (cond) import { x } from '...' ← SyntaxError// Для динамики: const m = await import('./b.mjs')
// Live bindings — ключевое отличие от CJS:// counter.mjs:export let count = 0;export const increment = () => count++;
// main.mjs:import { count, increment } from './counter.mjs';console.log(count); // 0increment();console.log(count); // 1 ← live binding! count — это alias, не копия
// В CJS: const { count } = require('./counter') → копия примитива → всегда 0// Interop CJS ↔ ESM в Node.js
// ESM может импортировать CJS (только default export):import cjsModule from './lib.cjs'; // cjsModule = module.exports// Named imports из CJS: экспериментально (Node 22+ --experimental-require-module)
// CJS НЕ может require() ESM синхронно:// require('./esm.mjs') → ERR_REQUIRE_ESM// Только через dynamic import():async function loadEsm() { const { fn, default: main } = await import('./esm.mjs'); return main;}
// Dual package паттерн (правильный способ):// package.json:// { "exports": {// "import": "./dist/index.mjs", // для ESM// "require": "./dist/index.cjs" // для CJS// } }Итог: CJS — динамический require с копированием значений; ESM — статический граф с live bindings; различие в семантике экспортов объясняет почему tree-shaking работает только с ESM и почему circular deps ведут себя по-разному.