CommonJS: трансформация и особенности

CommonJS остаётся одной из ключевых модульных систем в экосистеме Node.js, несмотря на доминирование ESM в современном JavaScript. SWC, как высокопроизводительный транспайлер, реализует трансформацию модулей CommonJS через преобразование синтаксиса import/export в классическую форму require/module.exports, а также через эмуляцию поведения ESM в CJS-окружении.

Базовая задача трансформации заключается не только в механической замене синтаксиса, но и в сохранении семантики модулей: порядка инициализации, кеширования, живых связей экспортов и корректной обработки циклических зависимостей.


Принципы преобразования модулей

При включении режима CommonJS SWC преобразует ESM-конструкции в следующие примитивы:

  • importrequire
  • exportexports или module.exports
  • export defaultmodule.exports.default или прямое присваивание module.exports

Ключевая сложность заключается в том, что ESM и CommonJS имеют различную модель исполнения:

  • ESM — статический граф модулей, живые привязки
  • CommonJS — динамическое выполнение с кешированием результата экспорта

SWC компенсирует эти различия через генерацию вспомогательных обёрток и функций интеропа.


Трансформация import

Базовый импорт

import fs from "fs";

Преобразуется в:

const fs = require("fs");

Такой случай является наиболее простым и не требует дополнительных обёрток.


Деструктурирующий импорт

import { readFile } from "fs";

Трансформируется в:

const { readFile } = require("fs");

Здесь сохраняется семантика выбора конкретных экспортов, однако исчезает концепция “живых” связей: значение фиксируется в момент require.


Импорт с переименованием

import { readFile as rf } from "fs";

Результат:

const { readFile: rf } = require("fs");

Namespace import

import * as fs from "fs";

SWC генерирует:

const fs = require("fs");

Однако в зависимости от конфигурации может добавляться интероп-обёртка:

const fs = _interopRequireWildcard(require("fs"));

Трансформация export

Именованные экспорты

export const a = 1;
export function foo() {}

Преобразуется в:

const a = 1;
function foo() {}

exports.a = a;
exports.foo = foo;

При этом SWC может группировать экспорты в конце файла для сохранения структуры исполнения.


Export после объявления

const x = 10;
export { x };

Результат:

const x = 10;
exports.x = x;

Переименование экспортов

export { x as y };
exports.y = x;

export default и его особенности

export default является наиболее сложным случаем, так как CommonJS не имеет прямого аналога.

Простое значение

export default 42;

Преобразуется в:

module.exports = 42;

Объектный экспорт

export default function foo() {}

Результат:

function foo() {}
module.exports = foo;

Смешанный экспорт

export default foo;
export const bar = 1;

SWC в этом случае создаёт структуру:

module.exports = foo;
exports.bar = 1;
exports.__esModule = true;

Флаг __esModule используется для обозначения того, что модуль изначально был ESM и позволяет корректно работать с интеропом в системах, ожидающих ESModule-совместимость.


Интероп между ESM и CommonJS

SWC генерирует вспомогательные функции для согласования различий между системами модулей.

_interopRequireDefault

Используется при импорте default-экспорта из CJS:

import foo from "cjs-module";

Преобразуется концептуально в:

const _mod = require("cjs-module");
const foo = _mod && _mod.__esModule ? _mod.default : _mod;

Эта проверка позволяет корректно извлекать default даже из модулей, которые были транспилированы из ESM.


_interopRequireWildcard

Используется для import * as:

import * as ns from "mod";

Логика:

  • если модуль уже ESM (__esModule), возвращается как есть
  • иначе оборачивается в объект с default и перечислением свойств

Пример логики:

function _interopRequireWildcard(obj) {
  if (obj && obj.__esModule) return obj;
  const newObj = {};
  if (obj != null) {
    for (const key in obj) {
      newObj[key] = obj[key];
    }
  }
  newObj.default = obj;
  return newObj;
}

Обработка циклических зависимостей

CommonJS использует кеш модулей, что позволяет разрешать циклы, но с частично инициализированными значениями.

SWC сохраняет оригинальную модель Node.js:

  • модуль помещается в кеш до полного выполнения
  • require() внутри цикла возвращает текущую версию exports

Пример проблемы:

// a.js
const b = require("./b");
exports.a = 1;

// b.js
const a = require("./a");
exports.b = 2;

В момент выполнения один из модулей может получить неполный exports, что является допустимым поведением CommonJS и сохраняется при трансформации.


Динамический require

const mod = require(someVariable);

SWC не может статически анализировать такой импорт, поэтому:

  • оставляет require без изменений
  • не применяет оптимизации
  • не преобразует в ESM-import

Это важно для сохранения динамической природы CommonJS.


Hoisting и порядок выполнения

В CommonJS порядок исполнения критичен:

const a = require("./a");
const b = require("./b");

SWC сохраняет строгий порядок:

  • все require остаются на месте вызова
  • объявления exports могут быть перемещены только если это не меняет семантику

Это особенно важно для модулей с побочными эффектами.


Объект module и exports

В CommonJS существуют две основные сущности:

  • module.exports — итоговый экспорт модуля
  • exports — ссылка на module.exports

SWC учитывает следующее правило:

exports.a = 1;
module.exports = {};

После присваивания module.exports ссылка exports больше не синхронизируется. Поэтому транспиляция избегает смешивания стилей, либо явно переопределяет экспорт:

module.exports = { a: 1 };

Конфигурация трансформации CommonJS в SWC

Поведение трансформации управляется через параметры компиляции:

  • включение типа модулей commonjs
  • настройки интеропа
  • целевая версия JavaScript

Типичная конфигурационная модель:

{
  "module": {
    "type": "commonjs"
  }
}

Дополнительно могут использоваться опции управления интеропом и генерацией вспомогательных функций, влияющие на:

  • вставку helper-функций
  • поведение __esModule
  • стратегию преобразования default-import

Особенности генерации кода

SWC стремится минимизировать накладные расходы:

  • переиспользование helper-функций
  • инлайнинг простых require
  • устранение лишних временных переменных
  • сохранение читаемой структуры вывода в development-режиме

В production-режиме при агрессивной оптимизации возможны:

  • объединение экспортов
  • перемещение объявлений
  • сокращение промежуточных переменных

Ограничения трансформации

Несмотря на высокую точность, остаются ограничения:

  • невозможность полностью эмулировать live bindings ESM
  • частичная несовместимость с динамическими экспортами
  • невозможность статического анализа runtime-условных export
  • поведение циклических зависимостей зависит от Node.js runtime, а не трансформера

SWC не изменяет фундаментальную модель CommonJS, а лишь обеспечивает совместимость синтаксиса ESM с ней.