Исправление поломок зависимостей ESM vs CommonJS в Node-приложениях
Устраните проблемы несовместимости ESM и CommonJS: быстро находите несоответствия модулей и выбирайте правильное исправление в package.json, сборке или через замену зависимости.

Как проявляется несовместимость ESM и CommonJS
В Node.js есть два способа загружать JavaScript: старый CommonJS с require() и module.exports и новый ESM с import/export.
Большинство приложений на практике не «чистые»: ваш код может быть CommonJS, зависимость — только ESM, а инструмент разработки может переписывать импорты при запуске в dev. Именно такое смешение даёт сбои.
Когда говорят о «несоответствии формата модуля», имеют в виду, что файл загружается как CommonJS, хотя на самом деле он ESM (или наоборот). Например: ваш код делает require('some-lib'), а some-lib — только ESM, и Node отказывает в загрузке через require().
Вот почему это часто ломается сразу после добавления зависимости или после деплоя. Многие dev-настройки скрывают несовпадение:
- TypeScript + ts-node/tsx могут исполнять ESM-стиль импортов в dev, даже если билд отдаёт CommonJS.
- Бандлеры «чинят» это локально, бандля зависимости, тогда как в проде используется неупакованное Node-разрешение.
- Serverless или Docker-билд может использовать другую версию Node или другую точку входа, чем локальный запуск.
Типичный сценарий: прототип работает через npm run dev, а в продакшене падает после добавления утилитной библиотеки. Локально dev-сервер транспилирует всё на лету. В проде вы запускаете скомпилированный dist/index.js, Node трактует его как CommonJS, и первый require() к ESM-only зависимости бросает ошибку.
Это часто встречается в командах, использующих TypeScript, Next/Nuxt-стиль инструментов и AI-генерированные стартеры. Сгенерированный код может смешивать сигналы (например, "type": "module" в package.json, но CommonJS-вывод в сборке), что порождает ошибку «на моём компьютере работает». Корень проблемы прост: рантайм и вывод сборки говорят на разных языках модулей.
Частые сообщения об ошибках и что они обычно означают
При смешении ESM и CommonJS сообщение об ошибке часто даёт самый быстрый подсказ. Формулировка обычно говорит, что Node подумал о файле (ESM или CJS) и что он пытался загрузить.
ERR_REQUIRE_ESM
Происходит, когда CommonJS-код вызывает require() для пакета, который доступен только как ESM.
Часто появляется после обновления зависимости: многие библиотеки перешли на ESM в новых мажорах. Типичные причины: ваш файл трактуется как CommonJS (нет "type": "module", или файл заканчивается на .cjs), вы делаете глубокий импорт, минующий точки входа пакета, или инструмент (тест-раннер, загрузчик конфигов) запускает код в CommonJS, даже если приложение в целом ESM.
Смысл ошибки: перестаньте использовать require() для этого импорта или выберите версию зависимости, которая ещё поддерживает CommonJS.
“Cannot use import statement outside a module”
Зеркальная ситуация: Node считает файл CommonJS, а в нём есть ESM-синтаксис import.
Частые причины: отсутствует "type": "module" в package.json, используется .js, где Node ожидает .mjs, или шаг сборки отдаёт ESM, а рантайм стартует как CommonJS.
“Named export ... not found” (или сюрпризы с default)
Обычно это из-за предположения, что формы экспорта ESM и CommonJS совпадают. CommonJS часто экспортирует один объект, а ESM ожидает именованные экспорты.
Простой тест: если import { thing } from "pkg" падает, но import pkg from "pkg" работает, вероятно, это вопрос interop с CommonJS.
“exports is not defined” и похожие рантайм-сюрпризы
Появляется, когда код, завязанный на CommonJS-галочки (exports, require, module), запускается в ESM-контексте, где их нет.
Типичная картина: в dev всё работало — dev-сервер трансформировал код, а в проде вы запускаете билд, где остались выражения вроде exports.foo = ..., Node загружает файл как ESM и падает.
Быстро перед тем как что-то менять
При появлении ошибок формата модулей легко кинуться менять "type": "module" или переписывать импорты. Не делайте этого сразу. Несколько быстрых проверок покажут, привели ли вы в проект ESM-only зависимость, стартуете ли не ту точку входа, или какой-то инструмент запускает код в неожиданном режиме.
Начните с подтверждения точного рантайма. Одинаковый код может вести себя по-разному под node, ts-node, тест-раннером или бандлером. Также проверьте версию Node в окружении, где ошибка проявляется (локально, CI, прод). По умолчанию и в краевых случаях поведение менялось между релизами Node, и многие хостинги отстают от локального окружения.
До правок проверьте:
- Версию Node и команду старта в каждом окружении (например,
node server.jsvs TypeScript-раннер). - Первый файл, который упал, и его расширение:
.cjs,.mjs,.jsили.ts. - Ближайший
package.jsonи установлено ли в нём"type": "module". - Название зависимости и путь файла, упомянутый в первой значимой строке стека.
- Происходит ли это только в dev или только в проде и чем отличаются окружения (вывод сборки, способ установки, переменные окружения).
Два частых паттерна:
- Если стек указывает в
node_modules/<pkg>/...и говорит, что нельзяrequire()-нуть, вероятно, вы подтянули ESM-only пакет в CommonJS-код. - Если стек указывает на ваш билд (
dist/index.js), значит сборка и рантайм думают о модульном формате по-разному.
Пример: прототип работает локально через ts-node (который может по-разному обрабатывать ESM), а в проде запускается plain node dist/server.js. Эта смена и обнаружит несоответствие.
Пошаговая диагностика несоответствия формата
Самый быстрый путь — перестать гадать и выяснить, где Node считает границу между ESM и CommonJS.
1) Начните с первого фрейма «вашего кода» в стеке
Откройте ошибку и найдите в стеке первую строку, которая указывает на файл вашего репозитория (не node_modules). Запишите имя файла и расширение, строку, которая вызвала загрузку (импорт/require или динамический import()), и ближайший package.json, который управляет этим файлом.
Эта точка обычно показывает, где принялось неправильное решение о формате модуля.
2) Подтвердите, как Node интерпретирует этот файл (ESM или CJS)
Node решает ESM vs CJS в основном по расширениям и package.json:
.mjs— ESM..cjs— CommonJS..js— зависит отtypeвpackage.json("module"— ESM, иначе — CommonJS).
Частая ошибка: вы считаете, что файл CommonJS, потому что написали require(), но в пакете стоит type: module, и .js файл действительно трактуется как ESM.
3) Посмотрите точки входа зависимости
Посмотрите node_modules/<pkg>/package.json у проблемного пакета и узнайте, что Node выбирает:
main(часто CommonJS)module(часто ESM, но Node не всегда использует его напрямую)exports(может маппить разные файлы дляimportиrequire)
Если есть exports, он часто решает всё. Пакет может экспортировать ESM для import, но не давать путь для require, и это приведёт к ERR_REQUIRE_ESM.
4) Воспроизведите минимальным сниппетом
Создайте маленький файл рядом и протестируйте только проблемный импорт.
// test-load.js
const pkg = require("the-problem-package");
console.log(pkg);
И ESM-версию:
// test-load.mjs
import pkg from "the-problem-package";
console.log(pkg);
Если один вариант работает, а другой падает — это форматное несоответствие, а не бизнес-логика.
5) Решите, что менять: приложение, сборку или зависимость
Выберите наименее рискованное исправление:
- Измените модульный режим приложения (расширения или
type), если контроль над кодом в основном у вас. - Измените вывод сборки/транспиляции, если вы компилируете TypeScript или используете бандлер.
- Измените зависимость (зафиксируйте версию, замените пакет или используйте другой путь), если пакет больше не поддерживает ваш формат.
Целевые правки в package.json (type, main, exports)
Многие ошибки можно починить без переписывания всего кода. Цель — однозначно указать, ESM ли ваш пакет, CommonJS или оба.
Начните с "type". Установка "type": "module" делает все .js файлы пакета ESM по умолчанию. Это удобно, если вы полностью переходите на ESM, но может вызвать цепочку require()-ошибок. Если в коде ещё есть CommonJS, лучше не менять глобально и указывать формат по файлам.
Когда нужен разный режим для разных файлов — предпочитайте расширения, а не глобальные переключатели:
.cjs— для обязательных CommonJS-файлов (require,module.exports)..mjs— для ESM-файлов (import,export)..js— только когдаtypeсоответствует нужному формату.
Далее проверьте точки входа: main традиционно для Node (обычно CJS). Некоторые бандлеры смотрят на module как на ESM-версию. Если нужно поддерживать оба, указывайте разные файлы (например, dist/index.cjs и dist/index.js).
exports — мощный, но часто неожиданно ломает. Как только он есть, глубокие импорты вроде some-lib/dist/internal.js могут перестать работать. Используйте exports чтобы экспонировать только то, что требуется, и явно указывайте и ESM, и CJS-таргеты, если поддерживаете оба.
Если меняете точки входа, делайте это аккуратно: сначала не ломайте main, вводите exports постепенно, экспортируйте и import, и require цели, и заменяйте глубокие пути на задокументированные публичные экспорты.
Исправления через настройки сборки и транспиляции
Многие ESM/CJS-ошибки на самом деле связаны не с зависимостью, а с тем, что вывод сборки не совпадает с тем, как Node запускает приложение.
Настройки TypeScript, которые решают, что Node загрузит
TypeScript может сгенерировать файлы, которые в редакторе выглядят ок, но эмиттят другой модульный формат. Если вы запускаете скомпилированный JS, проверьте:
compilerOptions.module:CommonJSгенерируетrequire(...);NodeNextилиESNext—import.compilerOptions.moduleResolution:NodeNextпонимает ESM-правила (расширения файлов иexports).compilerOptions.esModuleInteropиallowSyntheticDefaultImports: это может сделать компиляцию возможной, но рантайм interop всё ещё будет неверным.outDir: убедитесь, что весь рантайм-код приходит из одной папки (обычноdist).
Правило: компилируйте в тот же модульный формат, которого ожидает процесс Node. Если приложение ESM — эмиттите ESM. Если CommonJS — эмиттите CommonJS.
Когда бандлер «чинит» это в dev, а потом Node ломается
Бандлеры и dev-серверы часто переписывают или бандлят зависимости, поэтому в dev всё кажется работающим. В проде запускают plain Node против сборки и внезапно появляются ошибки ESM/CJS.
Чтобы избежать сюрпризов, запускайте production-команду локально против скомпилированного вывода, а не через dev-сервер.
Избегайте смешения src и dist
Ошибки часто появляются, когда рантайм импортирует часть файлов из src, а часть из dist. Это смешивает системы модулей и расширения.
Держите окружение чистым:
- В продакшене запускайте только
dist(или толькоsrc, если вы прямо запускаете TS). - Удаляйте старые артефакты перед билдом (сталые файлы могут всё ещё импортироваться).
- Используйте консистентные пути импортов, указывающие на собранные файлы.
Исправления через зависимости: фиксация, замена, обход
Иногда самое быстрое исправление — в выборе зависимости, а не в коде приложения. Рассматривайте зависимость как переменную: зафиксируйте совместимую версию, переключитесь на другой пакет или используйте поддерживаемую точку входа.
Замените на альтернативу, дружелюбную к CJS
Если приложение на CommonJS и зависимость перешла на ESM-only, часто проще заменить пакет, чем подкручивать сборку ради одной библиотеки. Это особенно верно для мелких утилит.
При выборе альтернативы убедитесь, что она поддерживает ваш модульный формат и версию Node и не требует глубоких импортов.
Зафиксируйте совместимую версию (с осторожностью)
Фиксация версии — быстрый способ остановить кровотечение, особенно если релиз внезапно поменял формат. Считайте это временным решением: можно выпустить, затем запланировать миграцию на ESM или заменить зависимость. Следите за правками безопасности в старых версиях.
Используйте документированный entry point, а не глубокий импорт
Много ошибок вызваны глубокими импортами вроде some-lib/dist/index.js. После обновления пакет мог добавить exports и заблокировать глубокие пути. Решение — импортировать из публичного entry point или из задокументированного пути.
Если зависимость ESM-only, а приложение CJS
У вас три практических варианта: перевести приложение на ESM, заменить зависимость или изолировать её. Изоляция хороша компромисс: загружайте ESM-пакет в небольшом обёрточном модуле (через динамический import()), оставляя остальной код CommonJS, пока планируете полноценную миграцию.
Частые ловушки, из-за которых поломка возвращается
Многие правки неустойчивы, потому что приложение остаётся несогласованным. Оно работает под одной командой (обычно dev), но ломается в тестах, CI или проде, где используется другой вход или toolchain.
Смешение require() и import на одном пути выполнения
Проблема не в том, что в репозитории есть оба стиля. Проблема — когда один и тот же путь выполнения может выполняться в обоих режимах.
Пример: вы сделали один маршрут с динамическим import() для ESM-зависимости, но CLI или тест всё ещё использует старый require()-путь.
Если нужно смешивать форматы временно, держите границу очевидной: один wrapper-модуль делает динамический импорт, а остальной код вызывает этот wrapper.
Поставка TypeScript-исходников вместо скомпилированного JS
Это проявляется, когда вы деплоите папку, в которой ещё есть .ts или ESM-файлы, тогда как рантайм ожидает CommonJS (или наоборот). Локально ts-node, dev-сервер или бандлер компилируют всё, а в проде остаётся несоответствие.
Проверьте, что действительно деплоите: если сервер стартует node dist/index.js, убедитесь, что dist существует и содержит ожидаемый формат. Удостоверьтесь, что main/exports указывают на собранные файлы, а не на исходники.
Dev-инструменты, которые патчат загрузку модулей
Тест-раннеры, dev-серверы и транспайлеры могут маскировать проблему, трансформируя импорты «на лету». В продакшене запускается plain Node.js и несоответствие проявляется.
Если dev использует кастомный раннер, а прод — node, считайте «работает в dev» непроверенным, пока вы не запустите production-команду локально.
Добавление "type": "module" для починки одного файла и сломав всё остальное
"type": "module" меняет смысл всех .js в пакете и может мгновенно сломать require()-вызовы, конфиги инструментов и старые зависимости. Если ESM нужен только в одном участке, подумайте о .mjs/.cjs или об изоляции в подпакете.
Опасности dual-package (разное поведение в ESM и CJS)
Некоторые библиотеки поставляют и ESM, и CJS-сборки. Node может выбрать разный entry в зависимости от import/require и exports-условий. Оба варианта могут «работать», но вести себя немного по-разному (форма default-экспорта, сайд-эффекты).
Если проблема возвращается, зафиксируйте версию зависимости и используйте документированный стиль импорта. Если библиотека непредсказуема, замена на более простую зависимость часто быстрее и надежнее.
Пример: прототип, который работает в dev, но ломается в проде
Типичная история: на ноутбуке всё ок, а при деплое логи рушатся. Локально вы запускали node server.js, нажимали на endpoint, всё отвечало. В продакшене процесс стартует, приходит первый запрос и падает.
Реалистичный набор: серверный файл CommonJS (server.js) использует require() повсеместно. Одна добавленная зависимость — только ESM.
Краш обычно выглядит так:
Error [ERR_REQUIRE_ESM]: require() of ES Module ... not supported.
Instead change the require of ... to a dynamic import()
Корень: CommonJS-файл пытается загрузить ESM-only пакет через require(). Node запрещает это.
Два рабочих варианта:
Вариант 1: переключить один файл (границу) на ESM
Если только серверный файл тянет ESM-пакет, перенесите границу в ESM.
Переименуйте server.js в server.mjs и замените require() на import, или сохраните server.js, но загрузите ESM-зависимость динамически:
const esmLib = await import('esm-only-lib');
Это позволяет сохранить большую часть кода без изменений и корректно загрузить ESM-пакет.
Вариант 2: заменить зависимость на CJS-дружелюбную
Если перевод ключевого файла в ESM тянет за собой массу изменений (тесты, конфиги), замена зависимости может быть быстрее. Выберите пакет, который поддерживает CommonJS или dual-build, и обновите место использования.
Чтобы убедиться в исправлении, делайте чистую пересборку и холодный старт: удалите старые артефакты и кеши, переустановите зависимости, стартуйте сервер «с нуля» и вызовите проблемный endpoint.
Что делать дальше для чистого production-готового решения
Примите одно решение и примените его последовательно: переводите приложение на тот формат, в котором преобладает код и инструменты; меняйте зависимость, если она — единственный аутсайдер; или корректируйте сборку, если исходники в порядке, а вывод — нет.
Если просите помощи, принесите чистый снэпшот:
- Точный текст ошибки и полный стек.
- Ваш
package.json(особенноtype,main,exports, зависимости). - Версию Node (локальную и production).
- Файл и строку, где падает ошибка.
- Точную команду запуска и шаги сборки.
Если это унаследованная AI-генерированная база, несовместимости модулей часто идут рядом с другими проблемами (сломанная аутентификация, утечка секретов, сложная структура). FixMyMess (fixmymess.ai) фокусируется на стабилизации таких кодовых баз: диагностике, применении минимальных безопасных правок и проверке результата человеком. Их бесплатный аудит быстро покажет, где неверная граница модулей и какой путь правки безопаснее.
Часто задаваемые вопросы
What’s the simplest difference between ESM and CommonJS in Node?
CommonJS использует require() и module.exports, а ESM — import и export. Практическая проблема в том, что Node в рантайме загружает файл только в одном формате, и если код или зависимость ожидает другой формат — приложение упадёт.
Why does it work in dev but fail after deploy?
Обычно ваш dev-инструмент как-то «склеивает» несовместимости: dev-сервер, ts-node или бандлер могут на лету переписывать импорты или бандлить зависимости. В продакшене же часто запускают plain node против dist-файлов, где несовместимость становится явной.
What does `Error [ERR_REQUIRE_ESM]` usually mean?
Это почти всегда означает, что CommonJS-файл вызывает require() к пакету, который доступен только как ESM. Быстрые варианты исправления: обернуть импорт через динамический import(); зафиксировать/заменить зависимость на CJS-совместимую версию; или мигрировать часть приложения на ESM.
How do I fix “Cannot use import statement outside a module”?
Node трактует файл как CommonJS, а в нём используется синтаксис ESM. Проверьте, не пропущено ли в package.json "type": "module", не используется ли расширение .js там, где Node ожидает .mjs, либо не получилось ли так, что сборка отдает ESM, а вы стартуете её как CJS.
Why do I get “Named export … not found” or weird default export behavior?
Чаще всего это несоответствие межформатным интерфейсам: вы импортируете именованные экспорты из модуля CommonJS. Попробуйте импортировать модуль целиком как дефолт и брать свойства оттуда, либо приведите сборку/рантайм к формату, который ожидает зависимость.
What’s the fastest way to find the real source of the mismatch?
Начните со первой строки стека, которая указывает на ваш код (а не node_modules). Посмотрите расширение файла, строку, где выполняется import/require, и ближайший package.json — именно он решает, считается ли .js ESM или CJS.
How do I tell if a dependency is ESM-only?
Откройте node_modules/<пакет>/package.json и проверьте exports, main и поля, относящиеся к ESM/CJS. Если в exports нет пути для require, то попытка require() упадёт даже при наличии исходных файлов.
Should I just add or remove `"type": "module"` in package.json?
Не делайте это первым шагом: "type": "module" меняет значение всех .js в пакете и может мгновенно сломать require()-вызовы и конфиги инструментов. Лучше использовать .mjs для ESM-файлов и .cjs для CommonJS, либо изолировать изменение в подпакете.
What TypeScript/build settings most often cause ESM/CJS breakage?
Синхронизируйте вывод сборки с тем, как вы запускаете приложение: если в продакшене node dist/server.js, убедитесь, что TypeScript/бандлер эмиттит тот же модульный формат. Избегайте смешивания файлов из src и dist в рантайме.
What should I collect before asking for help, and can FixMyMess fix this quickly?
Покажите полный текст ошибки со стеком, точный package.json (особенно type, main, exports, зависимости), версию Node (локально и в проде), файл и строку, где падает, и команду запуска (и шаг сборки). FixMyMess (fixmymess.ai) может быстро выполнить бесплатный аудит и предложить минимально рискованный план, обычно в течение 48–72 часов.