Интеграция с esbuild через плагин

Связка SWC и esbuild применяется там, где требуется объединить высокую скорость сборки esbuild с гибкостью трансформаций SWC. Основная идея интеграции заключается в том, что esbuild берёт на себя граф модулей, резолвинг зависимостей и управление сборкой, а SWC выполняет трансформацию кода: TypeScript, JSX, современные ECMAScript-синтаксические конструкции, а также кастомные преобразования AST.

SWC выступает как высокопроизводительный компилятор, написанный на Rust, и способен заменять Babel в задачах трансформации. Esbuild, в свою очередь, обеспечивает быстрый bundling на Go-движке. В связке они дополняют друг друга, но требуют аккуратной настройки границ ответственности.

Роль esbuild в цепочке сборки

Esbuild отвечает за:

  • построение графа зависимостей;
  • объединение модулей в один или несколько бандлов;
  • резолвинг импортов (ESM, CommonJS);
  • генерацию sourcemap на уровне бандла;
  • минимизацию кода (при необходимости).

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

  • экспериментальные предложения ECMAScript;
  • сложные Babel-плагины в legacy-проектах;
  • нестандартные макросы;
  • продвинутая работа с JSX трансформацией;
  • специфические правила для TypeScript.

В таких случаях подключается SWC как внешний трансформер.

Роль SWC в интеграции

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

  • преобразование TypeScript → JavaScript;
  • компиляция JSX в JavaScript runtime (React, Preact, Solid и др.);
  • транспиляция современных ECMAScript-фич в совместимый JS;
  • применение SWC-плагинов (если используются);
  • генерация source maps на уровне файла.

Важно, что SWC не участвует в bundling-процессе. Это ключевое разделение обязанностей.


Плагин SWC для esbuild

Интеграция реализуется через esbuild plugin API. Плагин перехватывает загрузку файлов и передаёт содержимое в SWC для трансформации.

Типичная схема работы:

  1. esbuild обнаруживает файл (например, .ts, .tsx, .jsx).
  2. Плагин перехватывает onLoad.
  3. Исходный код передаётся в SWC.
  4. SWC возвращает трансформированный JavaScript + sourcemap.
  5. esbuild продолжает сборку уже с готовым JS.

Базовая структура плагина

import { transform } from "@swc/core";

export function swcPlugin(options = {}) {
  return {
    name: "swc-plugin",
    setup(build) {
      build.onLoad({ filter: /\.[cm]?[jt]sx?$/ }, async (args) => {
        const fs = await import("fs/promises");
        const source = await fs.readFile(args.path, "utf8");

        const result = await transform(source, {
          filename: args.path,
          sourceMaps: true,
          jsc: {
            parser: {
              syntax: "typescript",
              jsx: true,
              decorators: true,
            },
            transform: {
              react: {
                runtime: "automatic",
              },
            },
            target: "es2020",
          },
          ...options,
        });

        return {
          contents: result.code,
          loader: "js",
          resolveDir: new URL(".", `file://${args.path}`).pathname,
        };
      });
    },
  };
}

Конфигурация esbuild с подключением SWC

Использование плагина выглядит стандартно для esbuild:

import esbuild from "esbuild";
import { swcPlugin } from "./swc-plugin.js";

esbuild.build({
  entryPoints: ["src/index.tsx"],
  bundle: true,
  outfile: "dist/bundle.js",
  sourcemap: true,
  plugins: [swcPlugin()],
});

Ключевое изменение архитектуры заключается в том, что esbuild перестаёт использовать встроенные трансформации TypeScript и JSX.


Настройка парсинга SWC

SWC требует явного описания синтаксиса, иначе возможны ошибки при обработке современных конструкций.

TypeScript + JSX

jsc: {
  parser: {
    syntax: "typescript",
    tsx: true,
    decorators: true,
  }
}

Только JavaScript

jsc: {
  parser: {
    syntax: "ecmascript",
    jsx: true,
  }
}

Поддержка decorators

Decorators требуют отдельного включения:

jsc: {
  transform: {
    legacyDecorator: true,
    decoratorMetadata: true,
  }
}

Работа с source maps

Одной из проблем интеграции является корректная синхронизация sourcemap между SWC и esbuild.

SWC генерирует map на уровне файла:

const result = await transform(code, {
  sourceMaps: true,
});

Esbuild затем объединяет эти карты в итоговую карту бандла.

Критический момент: необходимо избегать двойной генерации или конфликтующих inline maps.

Рекомендуемая стратегия:

  • SWC: sourceMaps: true
  • esbuild: sourcemap: true
  • избегать inlineSourcesContent дублирования

Производительность интеграции

Комбинация SWC и esbuild даёт интересный компромисс:

  • esbuild остаётся сверхбыстрым bundler’ом;
  • SWC добавляет небольшую задержку на этапе трансформации;
  • итоговая скорость часто выше, чем у Babel + webpack.

Узкие места:

  • частые вызовы transform() на каждый файл;
  • отсутствие кеширования;
  • синхронный FS read в onLoad;
  • обработка больших monorepo.

Кэширование трансформаций

Без кэша SWC становится узким местом. Простая оптимизация — in-memory cache:

const cache = new Map();

build.onLoad({ filter: /\.[cm]?[jt]sx?$/ }, async (args) => {
  const cached = cache.get(args.path);
  if (cached) return cached;

  const source = await fs.readFile(args.path, "utf8");

  const result = await transform(source, {
    filename: args.path,
    sourceMaps: true,
  });

  const output = {
    contents: result.code,
    loader: "js",
  };

  cache.set(args.path, output);
  return output;
});

Для продакшена часто добавляют:

  • hash-based cache invalidation;
  • LRU-кэш;
  • диск-кэш (например, через .swc/ директорию).

Обработка TS без type-checking

Важно учитывать, что SWC не выполняет type-checking. В связке с esbuild это означает:

  • трансформация происходит быстро;
  • проверка типов должна выполняться отдельно (tsc –noEmit).

Типичный pipeline:

  1. tsc –noEmit (валидация типов)
  2. esbuild + SWC (сборка)

JSX runtime и React интеграция

SWC позволяет переключать JSX runtime:

Automatic runtime

transform: {
  react: {
    runtime: "automatic",
  },
}

Classic runtime

transform: {
  react: {
    runtime: "classic",
    pragma: "React.createElement",
  },
}

В связке с esbuild важно избегать двойной обработки JSX:

  • либо SWC полностью отвечает за JSX;
  • либо esbuild JSX отключён (jsx: “preserve”).

Конфликты трансформаций

Частая ошибка интеграции — двойная трансформация одного и того же синтаксиса.

Примеры конфликтов:

  1. TypeScript stripping

  • SWC удаляет types
  • esbuild тоже пытается обработать TS

Решение: отключить TS loader в esbuild через loader: “js” после SWC.

  1. JSX duplication

Если esbuild и SWC оба обрабатывают JSX — результат ломается.

Решение: JSX должен обрабатываться только SWC.


Monorepo и пути резолвинга

В монорепозиториях важно корректно передавать resolveDir.

return {
  contents: result.code,
  loader: "js",
  resolveDir: path.dirname(args.path),
};

Ошибки здесь приводят к:

  • неправильному разрешению relative imports;
  • конфликтам alias;
  • проблемам с workspace пакетами.

Асинхронные плагины и ограничения

Esbuild поддерживает async onLoad, что позволяет интегрировать SWC без блокировок. Однако важно учитывать:

  • параллельность трансформаций;
  • отсутствие глобального shared state без защиты;
  • потенциальные race conditions при кэше.

Минификация: SWC vs esbuild

Обе системы способны минифицировать код:

  • esbuild: встроенный minifier (очень быстрый)
  • SWC: @swc/minify (более гибкий)

В интеграции обычно выбирают один вариант:

  • либо esbuild minification (minify: true);
  • либо SWC minify на этапе трансформации.

Смешивание редко оправдано.


Расширенные сценарии

Условная трансформация

Можно применять SWC только к определённым файлам:

build.onLoad({ filter: /src\/.*\.(ts|tsx)$/ }, handler);

Остальные файлы esbuild обрабатывает самостоятельно.


Интеграция с макросами

SWC может выполнять AST-трансформации, что позволяет реализовать макросы:

  • compile-time replacement;
  • инлайнинг функций;
  • условная генерация кода.

В связке с esbuild это превращается в полноценный compile pipeline.


Watch mode

В watch режиме важно очищать кэш:

esbuild.context({
  plugins: [swcPlugin()],
  watch: true,
});

При изменении файлов кэш SWC должен инвалидироваться, иначе возможны устаревшие результаты.


Типичные проблемы интеграции

Медленный build при первом запуске

Причина: отсутствие кэша и последовательная обработка файлов.

Утечки памяти

Причина: бесконтрольный Map-кэш трансформаций.

Некорректные sourcemap

Причина: несогласованность SWC map и esbuild chain.

Ошибки JSX runtime

Причина: дублирование трансформаций или неправильный runtime.


Практическая архитектура production-пайплайна

Устойчивый pipeline обычно выглядит так:

  • esbuild:

    • bundling
    • tree shaking
    • chunking
  • SWC:

    • TS → JS
    • JSX transform
    • experimental syntax
  • tsc:

    • type-checking (отдельно)
  • optional:

    • linting (ESLint)

Такое разделение снижает связность инструментов и повышает предсказуемость сборки.