ES Modules: сохранение и трансформация

Модульная система ECMAScript Modules (ESM) определяет статическую структуру зависимостей JavaScript-кода через конструкции import и export. В отличие от CommonJS, где загрузка модулей происходит динамически через require, ESM фиксирует граф зависимостей на этапе парсинга. Это свойство критично для инструментов трансформации, включая SWC, поскольку позволяет проводить анализ кода до его исполнения.

Ключевые характеристики ESM:

  • статическая структура импортов;
  • явное разделение экспортов;
  • поддержка top-level await;
  • возможность tree-shaking на уровне сборщика.

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


Архитектура обработки модулей в SWC

SWC реализует конвейер преобразования AST, где этап работы с модулями встроен в стадия трансформации:

  1. Парсинг исходного файла в AST с распознаванием ESM-синтаксиса.
  2. Построение внутреннего представления модульных связей.
  3. Применение трансформаций module.type.
  4. Генерация кода под целевую среду.

При этом SWC не ограничивается простым синтаксическим переписыванием import/export. Он учитывает:

  • режим вывода (esm, cjs, systemjs);
  • стратегию сохранения структуры модулей;
  • необходимость вставки runtime-хелперов;
  • особенности interop между системами модулей.

Сохранение структуры ES Modules

Режим сохранения ESM-структуры применяется, когда исходный код должен остаться максимально близким к оригинальному модульному графу.

В конфигурации SWC это выражается через:

  • module.type: “es6” или “esm” (в зависимости от конфигурационного слоя);
  • отключение преобразования import/export в require/module.exports;
  • сохранение статических импортов без оборачивания в функции.

Поведение в этом режиме:

  • import остается неизменным;
  • export сохраняется как есть;
  • динамические импорты import() не трансформируются;
  • код остается совместимым с нативными ESM-окружениями (браузеры, Node.js ESM).

Особенность SWC заключается в том, что даже при сохранении структуры он может выполнять минимальные корректировки:

  • нормализация путей;
  • добавление расширений файлов при необходимости;
  • устранение TypeScript-специфичных конструкций.

Трансформация ESM → CommonJS

Одним из наиболее часто используемых сценариев является преобразование модулей ECMAScript в CommonJS для сред выполнения без нативной поддержки ESM.

При включении module.type: “commonjs” SWC выполняет следующие преобразования:

Преобразование импортов

import fs from "fs";

превращается в:

const fs = require("fs");

Для именованных импортов:

import { readFile } from "fs";

становится:

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

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

export const value = 1;

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

const value = 1;
exports.value = value;

или через объект module.exports при default-экспортах:

export default function () {}

module.exports = function () {};

Семантика live bindings

Одной из сложностей трансформации ESM является сохранение live bindings — поведения, при котором импортированная переменная отражает актуальное значение экспортера.

SWC при генерации CommonJS-кода может использовать промежуточные геттеры или ссылки на объект экспорта:

exports.__esModule = true;
exports.value = void 0;

Object.defineProperty(exports, "value", {
  enumerable: true,
  get: () => value
});

Это позволяет приблизить поведение CommonJS-кода к ESM-модели.


Dynamic import и его обработка

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

import("module");

в ESM остается функцией, возвращающей Promise.

При трансформации в CommonJS SWC сохраняет асинхронную природу через обертку:

Promise.resolve().then(() => require("module"));

Такой подход обеспечивает:

  • ленивую загрузку;
  • сохранение асинхронного интерфейса;
  • совместимость с кодом, ожидающим import().

Сохранение модульного графа

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

SWC не выполняет bundling на уровне трансформации, но поддерживает структуру:

  • каждый файл остается отдельным модулем;
  • зависимости не инлайнятся;
  • порядок выполнения определяется runtime-средой.

Это особенно важно при использовании ESM → CJS трансформации в больших проектах, где граф модулей должен оставаться стабильным.


Interop между ESM и CommonJS

При смешанном окружении возникает необходимость в межмодульной совместимости.

SWC добавляет вспомогательные конструкции:

Пометка ESModule

Object.defineProperty(exports, "__esModule", { value: true });

Обработка default import из CommonJS

import pkg from "cjs-module";

const pkg = require("cjs-module");
const _default = pkg.__esModule ? pkg.default : pkg;

Это позволяет корректно обрабатывать:

  • CJS-модули с module.exports;
  • ESM-модули с default export;
  • гибридные пакеты.

Оптимизация импортов и удаление неиспользуемого кода

Хотя tree-shaking обычно выполняется на уровне bundler-а, SWC может участвовать в подготовке кода:

  • удаление TypeScript-only конструкций;
  • устранение типовых импортов;
  • упрощение re-export цепочек.

Пример:

export { a } from "./a";

может быть развернут в промежуточную форму, удобную для последующей оптимизации:

Object.defineProperty(exports, "a", {
  enumerable: true,
  get: function () {
    return require("./a").a;
  }
});

Режимы генерации модулей

SWC поддерживает несколько стратегий генерации:

ESM output

  • сохранение import/export;
  • минимальная трансформация;
  • ориентирован на современные runtime.

CommonJS output

  • преобразование в require/module.exports;
  • поддержка Node.js без ESM флага;
  • максимальная совместимость.

SystemJS output

  • генерация регистрационных модулей;
  • использование System.register;
  • применение в legacy-браузерах.

Влияние конфигурации на трансформацию

Поведение трансформации модулей зависит от набора параметров:

  • module.type
  • module.strict
  • module.noInterop
  • jsc.target

Комбинации этих параметров определяют:

  • глубину преобразований;
  • использование helper-функций;
  • способ обработки default exports.

Inline-хелперы и runtime-обвязка

SWC при трансформации может вставлять вспомогательные функции:

  • __importDefault
  • __importStar
  • __exportStar

Они обеспечивают корректную эмуляцию ESM-поведения в CJS-среде.

Пример:

var __importDefault = function (mod) {
  return mod && mod.__esModule ? mod : { default: mod };
};

Обработка re-export сценариев

Конструкции вида:

export * from "./module";

в CommonJS требуют особой обработки:

Object.keys(require("./module")).forEach(function (key) {
  if (key !== "default") {
    exports[key] = require("./module")[key];
  }
});

Такая трансформация сохраняет:

  • полное перенаправление экспортов;
  • поддержку динамического расширения API модуля;
  • совместимость с ESM-логикой.

Особенности поведения при top-level await

При наличии:

await fetchData();

SWC в ESM-режиме сохраняет выражение как есть, тогда как в CommonJS может потребоваться оборачивание в async IIFE:

(async function () {
  await fetchData();
})();

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


Стабильность модульного вывода

Цель трансформации ESM в SWC заключается в сохранении:

  • идентичности интерфейсов модулей;
  • предсказуемости порядка инициализации;
  • корректного поведения импортов и экспортов при разных runtime.

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