noInterop и его последствия

Проблема взаимодействия CommonJS и ESM

Экосистема JavaScript исторически разделена на две модульные системы: CommonJS и ECMAScript Modules. CommonJS использует require() и module.exports, тогда как ESM опирается на import и export. Эти системы несовместимы на уровне синтаксиса и имеют различную семантику загрузки и экспорта.

При компиляции TypeScript или современного JavaScript в целевую среду SWC вынужден решать задачу интероперабельности: как корректно импортировать CommonJS-модуль как ESM и наоборот. Именно здесь появляется механизм interop и опция noInterop.


Что делает noInterop в SWC

В конфигурации SWC параметр noInterop управляет генерацией вспомогательного кода для совместимости модулей.

Типичная конфигурация:

{
  "module": {
    "type": "es6",
    "noInterop": true
  }
}

Смысл параметра:

  • noInterop: false — SWC добавляет слой совместимости между CommonJS и ESM
  • noInterop: true — SWC отключает этот слой и сохраняет «сырую» модульную семантику

Ключевой эффект заключается в том, будет ли SWC автоматически подставлять обёртки для default import из CommonJS.


Поведение при noInterop: false

При включённой интероперабельности SWC стремится сделать импорт CommonJS максимально похожим на ESM.

Пример CommonJS модуля:

// cjs-module.js
module.exports = {
  value: 42
};

Импорт в ESM стиле:

import mod from "./cjs-module";

При noInterop: false SWC генерирует код, который фактически преобразует CommonJS экспорт в объект с default:

const mod = require("./cjs-module");
const _mod = mod && mod.__esModule ? mod : { default: mod };

_mod.default;

Поведение:

  • import x from “cjs” работает как ожидается
  • default автоматически синтезируется
  • уменьшается количество ошибок при смешанных модулях

Поведение при noInterop: true

При отключённой интероперабельности SWC перестаёт «угадывать» структуру CommonJS-модулей.

{
  "module": {
    "type": "es6",
    "noInterop": true
  }
}

Тот же импорт:

import mod from "./cjs-module";

Теперь результат становится прямолинейным:

const mod = require("./cjs-module");

Ключевое изменение:

  • никакой автоматической обёртки default
  • доступ к экспорту зависит от фактической структуры module.exports

Последствия для доступа к экспортам

CommonJS модуль

module.exports = {
  value: 10
};

При noInterop: true

import mod from "./cjs";
mod.value; // работает
mod.default; // undefined

При noInterop: false

import mod from "./cjs";
mod.default.value; // работает через обёртку

Разница критична: меняется точка доступа к данным.


Сравнение моделей поведения

Сценарий noInterop: false noInterop: true
CommonJS default import обёртка default прямой require()
ESM import/export без изменений без изменений
Совместимость с Babel-style interop высокая низкая
Прозрачность структуры модулей низкая высокая
Риск runtime ошибок ниже выше при смешанных модулях

Влияние на Tree Shaking и сборку

Отключение interop-обёрток влияет на то, как сборщик анализирует зависимости.

При noInterop: false

  • добавляются дополнительные объекты-обёртки
  • ухудшается статический анализ экспорта
  • tree-shaking может быть менее точным из-за «лишнего слоя»

При noInterop: true

  • сохраняется исходная структура require
  • упрощается граф зависимостей
  • потенциально улучшается tree-shaking в связке с ESM-бандлерами

Влияние на runtime поведение

  1. Устранение синтетического default

Многие ошибки в JavaScript-проектах возникают из-за:

import lib from "lib";
lib.default(); // runtime error

При noInterop: true такой слой просто отсутствует, и ошибка проявляется сразу на этапе доступа к свойствам.

  1. Прозрачность зависимостей

Код начинает отражать реальную структуру пакета:

  • CommonJS остаётся CommonJS
  • ESM остаётся ESM
  • никакой скрытой трансформации на уровне импорта

Влияние на TypeScript-проекты

В экосистеме TypeScript часто используется параметр esModuleInterop. SWC noInterop концептуально связан, но работает на уровне трансформации, а не типов.

С noInterop: false

  • поведение ближе к esModuleInterop: true
  • импорт по умолчанию безопасен даже для CommonJS

С noInterop: true

  • требуется более точное понимание источника модуля
  • типизация может расходиться с runtime поведением
  • возрастает необходимость явных импортов:
import * as mod from "./cjs";

Типовые сценарии поломок при включении noInterop

  1. Ожидание default export у CommonJS

import express from "express";
express(); // может перестать работать

Если express — CommonJS модуль, структура экспорта может требовать:

import * as express from "express";

  1. Плагины и библиотеки с mixed exports

Библиотеки, которые частично используют module.exports и export default, начинают вести себя неоднозначно.


  1. Ошибки доступа к свойствам

import lib from "lib";
lib.default.fn(); // ломается

Архитектурные последствия выбора noInterop

Система становится ближе к «чистому ESM»

  • уменьшается магия совместимости
  • поведение становится предсказуемым
  • требуется дисциплина в зависимостях

Но возрастает стоимость интеграции старых пакетов

  • CommonJS-библиотеки начинают требовать явного обращения к структуре экспорта
  • возрастает количество ручных адаптаций импортов

Практическая модель принятия решения

Сценарий отключения interop оправдан, если:

  • проект полностью ESM
  • отсутствуют legacy CommonJS зависимости
  • используется современный bundler с ESM-first стратегией
  • требуется максимальная прозрачность модульной системы

Сценарий включённого interop предпочтителен, если:

  • активно используется npm-экосистема с CommonJS
  • есть смешанные зависимости
  • важна совместимость без ручной корректировки импортов

Поведение в связке SWC и сборщиков

При использовании SWC вместе с bundlers (например, Webpack, Vite, Rollup) noInterop влияет на раннюю стадию трансформации.

  • bundler получает либо «обёрнутый» ESM-совместимый код
  • либо «чистый» require/esm код без адаптации

Это меняет:

  • анализ зависимостей
  • оптимизацию импортов
  • поведение code splitting

Тонкие эффекты в больших кодовых базах

В масштабных проектах влияние noInterop проявляется не сразу, а через накопление различий:

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

На уровне архитектуры модулей это создаёт разделение между:

  • «interop-safe» кодом
  • «strict ESM» кодом

Итоговая модель поведения трансформации

  • noInterop: false — слой совместимости, сглаживание различий модульных систем
  • noInterop: true — прямое отображение исходной модульной системы без синтетических преобразований

Разница определяется не только синтаксисом импорта, но и тем, как именно формируется объект модуля на этапе выполнения.