CommonJS vs ES Modules в Node

Node.js поддерживает две системы модулей: CommonJS (CJS) — историческая система Node.js с require/module.exports, и ES Modules (ESM) — стандарт JavaScript с import/export, поддерживаемый с Node.js 12+.

Зачем нужно

Выбор между CJS и ESM влияет на совместимость пакетов, синтаксис импортов, возможность использования top-level await и инструментов (Jest, TypeScript, Vite). Понимание различий позволяет избежать ошибок при смешении модулей и правильно настроить проект.

Где используется

  • CJS — legacy Node.js проекты, большинство npm-пакетов до 2022
  • ESM — современные проекты, TypeScript (moduleResolution: node16), пакеты только с ESM (chalk v5+, got v12+)
  • Dual CJS+ESM — npm-библиотеки, которые поддерживают оба формата

Основной контент

CommonJS (CJS)

// Экспорт
module.exports = { add, multiply };
// или
exports.greet = (name) => `Hello, ${name}`;

// Импорт
const { add } = require('./math');
const fs = require('fs');
const path = require('path');

// Динамический импорт
const plugin = require(`./plugins/${name}`);

// CJS — синхронный, модуль кешируется после первого require

ES Modules (ESM)

// package.json
{ "type": "module" }
// или расширение .mjs

// Экспорт
export function add(a, b) { return a + b; }
export const PI = 3.14;
export default class Calculator {}

// Импорт — обязательно указывать расширение .js
import { add } from './math.js';
import Calculator from './calculator.js';
import * as utils from './utils.js';

// Динамический импорт (асинхронный)
const { default: plugin } = await import(`./plugins/${name}.js`);

// Top-level await (только в ESM)
const data = await fetch('https://api.example.com/data').then(r => r.json());

Ключевые отличия

Характеристика CommonJS ES Modules
Синтаксис require/exports import/export
Загрузка Синхронная Асинхронная
Top-level await Нет Да
__dirname Есть Нет (см. ниже)
this в модуле module.exports undefined
Расширение файла .js, .cjs .mjs или .js при "type":"module"
Динамический импорт require import (промис)

__dirname и __filename в ESM

// В ESM эти переменные недоступны, используем import.meta
import { fileURLToPath } from 'url';
import { dirname } from 'path';

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

Импорт CJS из ESM и наоборот

// ESM может импортировать CJS
import cjsModule from './legacy.cjs'; // работает

// CJS НЕ МОЖЕТ синхронно require ESM
// Только через динамический import
async function loadESM() {
  const { add } = await import('./math.mjs');
  return add(1, 2);
}

package.json конфигурация

{
  "type": "module",
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    }
  }
}

Частые ошибки

  • require is not defined — файл определён как ESM ("type":"module"), но используется CJS синтаксис
  • Cannot use import statement — файл .js в CJS-проекте содержит import; переименовать в .mjs или добавить "type":"module"
  • Пропустить .js в ESM-импортахimport { x } from './math' → ошибка; нужно './math.js'
  • Top-level await в CJS — не поддерживается, обернуть в async function main() {}

Связанные темы

Ресурсы


🎓 Источник: Модули, слои, структура проекта, песочницы

  • 📅 2018-10-02 · YouTube · [Marp](../../Documents/TimurShemsedinov/2018-10-02 — Модули, слои, структура проекта, песочницы в JavaScript и Node.js (O7A9chb573E).md)
  • Тезисы:
    • require исполняется СИНХРОННО — модуль кешируется в require.cache после первого вызова
    • require возвращает один и тот же объект → паттерн синглтон
    • Можно сбросить кеш: delete require.cache[require.resolve('./mod')] — для hot reload
    • ESM с .mjs несовместим с require — нужен динамический import
    • package.json указывает точку входа в поле main (CJS) или module/exports (ESM)
    • Сборка модулей через Object.assign(exports, require('./a'), require('./b')) — антипаттерн, перекрытие ключей

🎓 Источник: Node.js модули ECMA, Common.js, Module API

  • 📅 2021-09-14 · YouTube · [Marp](../../Documents/TimurShemsedinov/2021-09-14 — 💻 Node.js модули ECMA, Common.js, Module API (CJr2vS3hjMU).md)
  • Тезисы: обзор обеих систем модулей, package.json "type", conditional exports

🎓 Источник: Node.js modules and packages in ECMA and CommonJS

  • 📅 2021-09-15 · YouTube
  • Тезисы: dual packages, "exports" field, разрешение путей в ESM

🎓 Источник: Как избавиться от кучи require/import

  • 📅 2023-06-21 · YouTube
  • Тезисы:
    • Загрузка папки целиком через рекурсивный обход + объект-namespace
    • В Metarhia подход: одна application загружает все слои lib/api/domain как namespaces
    • Не нужно дублировать imports в каждом файле — вся библиотека доступна как app.lib.X
    • Подход не для всех — теряет статический анализ, требует продуманной архитектуры