Гид по технологиям

ESM в Node.js: совместимость с CommonJS

9 min read Node.js Обновлено 24 Nov 2025
ESM в Node.js: совместимость с CommonJS
ESM в Node.js: совместимость с CommonJS

Логотип Node.js в векторной графике

Содержание

  • Введение
  • Основы
  • Импорт CommonJS из ESM
  • Импорт ESM из CommonJS
  • Получение пути текущего модуля в ESM
  • Почему такие различия
  • Стоит ли переходить на ESM
  • Практическое руководство по поддержанию совместимости
  • План миграции — пошагово
  • Рекомендации для авторов библиотек
  • Типичные ошибки и способы их устранения
  • Краткий глоссарий
  • Итог

Введение

На протяжении долгого времени в ECMAScript не было штатного способа описывать и загружать модули. Поскольку Node.js нуждался в модулях раньше, чем их стандартизовали, де-факто стандартом внутри экосистемы стал CommonJS (CJS) — система с синхронным require и module.exports.

ES2015 (ES6) ввёл собственную систему модулей — ES Modules (ESM). Она использует import и export. Node.js развивал поддержку ESM постепенно; с версии v16 поддержка ESM включена по умолчанию. Тем не менее две разные системы модулей в одном проекте создают сложности, поэтому важно понимать механики их взаимодействия.

Основы

Если вы начинаете новый проект и планируете использовать ESM, дела относительно просты: разбивайте код на файлы и используйте import / export. Но есть важные настройки, которые нужно сделать заранее.

По умолчанию Node трактует файлы с расширением .js как CJS. Есть два способа явно указать, что проект должен использовать ESM:

  • Использовать расширение файлов .mjs — тогда Node всегда будет рассматривать их как ESM.
  • В корневом package.json задать поле “type”: “module” — тогда все файлы с расширением .js будут обрабатываться как ESM по умолчанию.

Пример package.json для проекта, где .js — это ESM:

{
  "name": "example-package",
  "type": "module",
  "dependencies": {
    "...": "..."
  }
}

Если поле type не установлено, по умолчанию проект рассматривается как CommonJS для совместимости с существующим кодом. Файлы с расширениями .cjs и .mjs всегда будут интерпретироваться как CJS и ESM соответственно.

Импорт CommonJS из ESM

Импортировать CJS-модуль в ESM-файле можно обычным import:

// cjs-module.cjs
module.exports.helloWorld = () => console.log("Hello World");

// esm-module.mjs
import component from "./cjs-module.cjs";

component.helloWorld();

При импорте CJS в ESM результатом будет объект, равный module.exports из CJS-модуля. Это означает, что именованные “экспорты” CJS на самом деле — свойства объекта. Поэтому можно обращаться либо к свойствам объекта по имени, либо распаковывать их:

import { helloWorld } from "./cjs-module.cjs";

helloWorld();

Node пытается определить набор экспортов CJS с помощью статического анализа — он сканирует исходник CJS, чтобы понять, какие свойства выставляются на module.exports. Но анализ не всегда идеален: в редких случаях динамически формируемые экспорты могут быть не обнаружены. Тогда используйте доступ через default-импорт (в ESM это объект module.exports):

import * as cjs from "./cjs-module.cjs";

cjs.default.someProperty();

Если статический анализ не сработал, cjs.default будет содержать экспорт CJS как объект.

Важно: CJS не поддерживает концепцию “named exports” — у модуля один экспорт (module.exports), который может быть объектом с множеством свойств.

Импорт ESM из CommonJS

Обратный сценарий — использование ESM-модулей внутри CJS-кода — сложнее. В CJS нельзя писать import сверху файла, но можно использовать динамический import(), который возвращает промис. Это позволяет асинхронно загружать ESM-модули из CJS:

// esm-module.mjs
export const helloWorld = () => console.log("Hello World");

// esm-module-2.mjs
export default () => console.log("Hello World");

// cjs-module.cjs
const loadHelloWorld = async () => {
  const { helloWorld } = await import("./esm-module.mjs");
  return helloWorld;
};

(async () => {
  const helloWorld = await loadHelloWorld();
  helloWorld();

  const loadHelloWorld2 = async () => {
    const helloWorld2 = await import("./esm-module-2.mjs");
    // Для default-экспорта возвращается объект вида { default: <функция> }
    return helloWorld2.default;
  };

  const helloWorld2 = await loadHelloWorld2();
  helloWorld2();
})();

Из-за асинхронной природы ESM и отсутствия top-level await в CJS приходится оборачивать динамические импорты в async-функции. (Замечание: современные версии Node поддерживают top-level await в ESM; в CJS его нет.)

Получение пути текущего модуля в ESM

В CJS часто используются глобальные переменные dirname и filename. В ESM их нет. Вместо этого можно воспользоваться import.meta.url:

console.log(import.meta.url);
// Пример вывода: file:///home/demo/module.mjs

Если нужен путь в файловой системе, превратите URL в путь с помощью встроенного модуля URL:

import { fileURLToPath } from 'url';
import { dirname } from 'path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

console.log(__dirname, __filename);

Почему такие различия

CJS — синхронная система: require() загружает модуль с диска и выполняет его сразу. ESM — асинхронный и многоступенчатый: парсинг импортов, асинхронная загрузка зависимостей, затем выполнение в правильном порядке. Такая модель ESM сделана с прицелом на браузеры, где асинхронность и ленивые загрузки критичны.

Асинхронность ESM объясняет ограничения при использовании ESM в CJS-коде: CJS не поддерживает top-level await, поэтому статический import вверху файла невозможен, а динамический import() нужен именно из-за архитектуры ESM.

Стоит ли переходить на ESM

Короткий ответ: да. ESM — стандартная и совместимая с браузерами система модулей. Она даёт преимущества: динамические импорты, возможность импортировать по URL, и лучшую совместимость с модульными стандартами ECMAScript. Однако полная миграция в больших кодовых базах может быть болезненной — CJS останется надолго в экосистеме.

Если вы создаёте новую библиотеку, по возможности распространяйте ESM-совместимый код и делайте ESM своим системным выбором.

Практическое руководство по поддержанию совместимости

Ниже — стратегии, которые подходят авторам библиотек и приложениям, желающим оставаться совместимыми с обоими мирами.

Подход: два входа — ESM и CJS

Создайте тонкую обёртку ESM, которая реэкспортирует содержимое из существующего CJS-модуля. Структура может выглядеть так:

project/
  cjs-index.js      # ваше текущее CJS окончание
  esm/esm-index.js  # обёртка ESM
  package.json

Пример esm/esm-index.js:

import demoComponent from "../cjs-component-demo.js";
import exampleComponent from "../cjs-component-example.js";

export { demoComponent, exampleComponent };

В package.json укажите поле exports, чтобы Node отдавал соответствующий вход в зависимости от механизма импорта:

{
  "exports": {
    "require": "./cjs-index.js",
    "import": "./esm/esm-index.js"
  }
}

Когда пользователь делает require(‘your-package’), Node отдаст cjs-index.js. При import ‘your-package’ будет использоваться esm/esm-index.js. Обратите внимание: когда используется exports, доступ к отдельным внутренним файлам библиотеки через пути типа “your-package/path/to/file.js” может быть запрещён — это ожидаемый побочный эффект.

Подход: скомпилированные бандлы

Некоторые проекты поставляют код в нескольких форматах на стороне сборки: ESM (для современных сред), CJS (для обратной совместимости), и иногда UMD или IIFE для браузеров без сборщика. Это обычно делается через инструмент сборки (Rollup, Babel, esbuild) и указывается в package.json через поля main, module и exports.

Подход: single source, dual publish

Содержимое пишется в ESM, затем при публикации автоматически генерируются версии для CJS (транспиляция/бандлинг) и ESM. CI/CD публикует оба набора артефактов.

План миграции — пошагово

Ниже приведён проверяемый план (SOP) для команды, планирующей миграцию библиотеки или приложения с CJS на ESM.

  1. Оценка покрытия зависимостей
  • Составьте список внутренних и внешних зависимостей. Отметьте, какие зависимости уже поддерживают ESM, какие — только CJS.
  1. Выработайте стратегию публикации
  • Решите, будете ли вы поддерживать оба формата либо переключитесь только на ESM (если потребители это позволяют).
  1. Локальная экспериЕНта
  • Попробуйте переименовать узкий набор файлов в .mjs или установить “type”: “module” в отдельной тестовой ветке.
  1. Исправление кода
  • Замените require/module.exports на import/export. Используйте файлы как модули ES.
  • Учтите: замена не всегда тривиальна — иногда логика зависит от синхронной загрузки.
  1. Тесты
  • Обновите тестовую среду (например, Jest требует дополнительных настроек для ESM); прогоните все тесты.
  1. Библиотеки и тулчейн
  • Убедитесь, что bundler, linter и CI поддерживают ESM. Настройте трансформацию для обратной совместимости, если нужно.
  1. Публикация
  • Если вы выпускаете dual format, настройте этап сборки в CI для генерации CJS-артефакта.
  1. Документация
  • Обновите README с примерами импорта для обоих типов потребителей.
  1. Мониторинг
  • Отслеживайте ошибки после релиза, особенно ошибки, связанные с import/require и путями модулей.

Критерии приёмки

  • Все unit/integration тесты проходят в среде ESM и CJS (если вы выпускаете оба).
  • Документация обновлена.
  • Пользователи не испытывают регрессий при установке и импортировании модуля.

Рекомендации для авторов библиотек

  • Если библиотека новая — делайте ESM форматом по умолчанию.
  • Для существующих библиотек рассмотрите публикацию dual format или создание тонкой ESM-обёртки.
  • Используйте поле “exports” в package.json для контроля, какие входные точки доступны пользователям.
  • Тщательно тестируйте edge cases: динамическая генерация экспорта в CJS, использование require() внутри функций, reliance на dirname и filename.

Типичные ошибки и способы их устранения

Важно: перечисленные проблемы — самые часто встречающиеся при совместном использовании ESM и CJS.

  1. Ошибка: import из CJS возвращает объект с ключом default
  • Причина: ESM default экспорт при импортировании через import() из CJS приходит в виде { default: <значение> }.
  • Решение: обращайтесь к .default или используйте деструктуризацию в месте, где ожидали named-экспорт.
  1. Ошибка: статический импорт CJS-модуля не показывает именованные экспорты
  • Причина: статический анализ не смог определить свойства module.exports.
  • Решение: используйте import * as cjs или обращайтесь через default-объект.
  1. Ошибка: path/URL проблемная конвертация
  • Причина: в ESM нет dirname/filename.
  • Решение: используйте fileURLToPath(import.meta.url) и path.dirname.
  1. Ошибка: экспорт map ограничивает доступ к файлам внутри пакета
  • Причина: использование поля exports в package.json.
  • Решение: либо явно перечислите разрешённые внутренние пути в exports, либо не используйте exports (но это снижает контроль над API пакета).
  1. Ошибка: тесты ломаются после смены module system
  • Причина: тестовый раннер может требовать специальных настроек для ESM (jest, mocha, ava и пр.).
  • Решение: обновите конфигурацию тестов, используйте поддерживающие ESM версии раннеров, либо запускайте тесты в среде, где проект удобен.

Decision flowchart — как выбрать стратегию миграции

flowchart TD
  A[Начало: проект на CJS или новый проект?] --> B{Это новая библиотека?}
  B -- Да --> C[Использовать ESM по умолчанию]
  B -- Нет --> D{Пользователи требуют CJS?}
  D -- Да --> E[Выпуск dual format: ESM + CJS]
  D -- Нет --> C
  E --> F[Настроить сборку: сгенерировать CJS из ESM]
  C --> G[Установить 'type': 'module' или .mjs]
  F --> H[Настроить exports и тесты]
  G --> H
  H --> I[Тесты и документация]
  I --> J[Релиз и мониторинг]

Рольные чек-листы при релизе библиотеки

Для Maintainer:

  • Проверить поддержку в CI и билд-пайплайне
  • Обновить package.json: name, exports, type
  • Обновить документацию по установке и использованию
  • Пропустить smoke-тесты для CJS и ESM

Для Reviewer PR:

  • Убедиться, что нет прямых require() в ESM модулях
  • Проверить корректность путей import.meta.url при необходимости
  • Проверить, что bundle size не вырос существенно (при dual publish)

Для User (потребителя библиотеки):

  • Проверить, доступен ли импорт в вашей среде (require или import)
  • В случае ошибок посмотреть, не возвращается ли объект с default

Ментальные модели и эвристики

  • Подумайте о модулях как о «контрактах»: exports — это публичный API. Экспортируйте минимально необходимое.
  • CJS = синхронный, ESM = асинхронный. Если ваш код зависит от синхронной инициализации, будьте осторожны при переходе.
  • Используйте exports в package.json, чтобы чётко контролировать публичные точки входа.

Когда миграция не рекомендуется

  • Если проект очень большой и критичен к времени отклика, и миграция потребует больших изменений в runtime-логике.
  • Если вы зависите от инструментов или библиотек, которые не поддерживают ESM и не планируют поддерживать.

Советы по совместимости для Node.js окружения

  • По возможности делайте тонкие ESM-обёртки рядом с существующим CJS-кодом.
  • Тестируйте оба варианта импорта при каждом изменении API.
  • Документируйте, какие публичные пути доступны для require и import.

Краткий глоссарий

  • ESM — ES Modules, стандартный механизм модулей (import/export).
  • CJS — CommonJS, старая система модулей Node.js (require/module.exports).
  • import.meta.url — URL текущего модуля в ESM.
  • exports (package.json) — маппинг входных точек пакета в Node.js.

Итог

ES Modules — правильное направление для JavaScript и Node.js: они стандартизованы, асинхронны и совместимы с браузерной моделью модулей. Node.js поддерживает ESM по умолчанию с v16, но из-за объёма существующего CJS-кода важно уметь работать с обоими подходами.

Если вы создаёте новый проект — выбирайте ESM. Если поддерживаете существующую библиотеку — рассмотрите dual publish или тонкую обёртку ESM вокруг CJS, настройте поле exports в package.json и тщательно протестируйте оба сценария. Следуя плану миграции и чек-листам, вы сведёте риски к минимуму и поможете экосистеме постепенно перейти на единый стандарт.

Важно: документируйте свои решения и информируйте пользователей о том, какие точки входа поддерживаются через require и import.

Поделиться: X/Twitter Facebook LinkedIn Telegram
Автор
Редакция

Похожие материалы

Как изменить день рождения в Facebook
соцсети

Как изменить день рождения в Facebook

Как сохранить анонимность в Facebook
Конфиденциальность

Как сохранить анонимность в Facebook

Как отметить себя в безопасности в Facebook
Социальные сети

Как отметить себя в безопасности в Facebook

Изменить email и номер телефона в Facebook
Социальные сети

Изменить email и номер телефона в Facebook

Удаление группы в Facebook: инструкция
соцсети

Удаление группы в Facebook: инструкция

Обновление с .NET 5 до .NET 6 — руководство
Разработка

Обновление с .NET 5 до .NET 6 — руководство