27 дек. 2025 г.·6 мин. чтения

Исправление поломок зависимостей ESM vs CommonJS в Node-приложениях

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

Исправление поломок зависимостей ESM vs CommonJS в Node-приложениях

Как проявляется несовместимость 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.js vs 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)

Получите быстрый фикс
Большинство фиксов выполняются за 48–72 часа, включая рефакторы, которые предотвращают регрессии.

Многие ошибки можно починить без переписывания всего кода. Цель — однозначно указать, 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 или ESNextimport.
  • 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).
  • Удаляйте старые артефакты перед билдом (сталые файлы могут всё ещё импортироваться).
  • Используйте консистентные пути импортов, указывающие на собранные файлы.

Исправления через зависимости: фиксация, замена, обход

Аудит настройки модулей
Мы проверим `package.json` (`exports`, `main`, `type`) и команду старта, чтобы убрать неожиданные сюрпризы.

Иногда самое быстрое исправление — в выборе зависимости, а не в коде приложения. Рассматривайте зависимость как переменную: зафиксируйте совместимую версию, переключитесь на другой пакет или используйте поддерживаемую точку входа.

Замените на альтернативу, дружелюбную к 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 часов.