Это важно для SDK, API clients, SSR, Node.js-сервисов и shared libraries. Частая ошибка - считать, что одинаковый TS-код в
.mjs и .cjs даст общий runtime.Где ломается
const cjs = require('@acme/sdk')
const esm = await import('@acme/sdk')Если
require попал в dist/index.cjs, а import - в dist/index.mjs, Node загрузит два разных модуля.Итог:
* два singleton-инстанса
* две registry/cache/map
* разные подключения
* сломанный
instanceof* расходящееся состояние
Рабочая схема
Один runtime source of truth, второй формат - только фасад. Например, состояние живет в
core.cjs, а ESM лишь импортирует его:// package.json
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/core.cjs"
}
}
// core.cjs
const registry = new Registry()
module.exports = { Registry, registry }
// index.mjs
import api from './core.cjs'
export const Registry = api.Registry
export const registry = api.registry
export default api
Теперь
require и import смотрят на один singleton:cjs.registry === esm.registry // true
cjs.Registry === esm.Registry // true
Типичная ошибка
Не делайте две независимые реализации:
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
Если оба файла содержат состояние, hazard почти гарантирован. Conditional exports только маршрутизируют загрузку, но не объединяют объекты в памяти.
Практический чеклист
* закрывайте deep imports вроде
@acme/sdk/dist/index.mjs* для каждого subpath, например
@acme/sdk/cache, повторяйте ту же схему* для чистых функций риск ниже, для DI, логгеров, метрик, БД-клиентов и кэшей - критичен
Вывод:
Dual ESM/CJS-пакету нужен один общий runtime-модуль и фасадные entrypoint, а не две сгенерированные копии реализации.
