Связка SWC и esbuild применяется там, где требуется объединить высокую скорость сборки esbuild с гибкостью трансформаций SWC. Основная идея интеграции заключается в том, что esbuild берёт на себя граф модулей, резолвинг зависимостей и управление сборкой, а SWC выполняет трансформацию кода: TypeScript, JSX, современные ECMAScript-синтаксические конструкции, а также кастомные преобразования AST.
SWC выступает как высокопроизводительный компилятор, написанный на Rust, и способен заменять Babel в задачах трансформации. Esbuild, в свою очередь, обеспечивает быстрый bundling на Go-движке. В связке они дополняют друг друга, но требуют аккуратной настройки границ ответственности.
Esbuild отвечает за:
Однако встроенный трансформер esbuild не всегда достаточен для сложных проектов, особенно если используются:
В таких случаях подключается SWC как внешний трансформер.
SWC берёт на себя исключительно этап трансформации исходного кода. Его задачи в связке:
Важно, что SWC не участвует в bundling-процессе. Это ключевое разделение обязанностей.
Интеграция реализуется через esbuild plugin API. Плагин перехватывает загрузку файлов и передаёт содержимое в SWC для трансформации.
Типичная схема работы:
.ts,
.tsx, .jsx).
onLoad.
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:
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 требует явного описания синтаксиса, иначе возможны ошибки при обработке современных конструкций.
jsc: {
parser: {
syntax: "typescript",
tsx: true,
decorators: true,
}
}
jsc: {
parser: {
syntax: "ecmascript",
jsx: true,
}
}
Decorators требуют отдельного включения:
jsc: {
transform: {
legacyDecorator: true,
decoratorMetadata: true,
}
}
Одной из проблем интеграции является корректная синхронизация sourcemap между SWC и esbuild.
SWC генерирует map на уровне файла:
const result = await transform(code, {
sourceMaps: true,
});
Esbuild затем объединяет эти карты в итоговую карту бандла.
Критический момент: необходимо избегать двойной генерации или конфликтующих inline maps.
Рекомендуемая стратегия:
sourceMaps: true
sourcemap: true
inlineSourcesContent дублирования
Комбинация SWC и esbuild даёт интересный компромисс:
Узкие места:
transform() на каждый файл;
Без кэша 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;
});
Для продакшена часто добавляют:
.swc/ директорию).
Важно учитывать, что SWC не выполняет type-checking. В связке с esbuild это означает:
Типичный pipeline:
tsc –noEmit (валидация типов)
SWC позволяет переключать JSX runtime:
transform: {
react: {
runtime: "automatic",
},
}
transform: {
react: {
runtime: "classic",
pragma: "React.createElement",
},
}
В связке с esbuild важно избегать двойной обработки JSX:
jsx: “preserve”).
Частая ошибка интеграции — двойная трансформация одного и того же синтаксиса.
Примеры конфликтов:
Решение: отключить TS loader в esbuild через loader: “js”
после SWC.
Если esbuild и SWC оба обрабатывают JSX — результат ломается.
Решение: JSX должен обрабатываться только SWC.
В монорепозиториях важно корректно передавать resolveDir.
return {
contents: result.code,
loader: "js",
resolveDir: path.dirname(args.path),
};
Ошибки здесь приводят к:
Esbuild поддерживает async onLoad, что позволяет
интегрировать SWC без блокировок. Однако важно учитывать:
Обе системы способны минифицировать код:
В интеграции обычно выбирают один вариант:
minify: true);
Смешивание редко оправдано.
Можно применять SWC только к определённым файлам:
build.onLoad({ filter: /src\/.*\.(ts|tsx)$/ }, handler);
Остальные файлы esbuild обрабатывает самостоятельно.
SWC может выполнять AST-трансформации, что позволяет реализовать макросы:
В связке с esbuild это превращается в полноценный compile pipeline.
В watch режиме важно очищать кэш:
esbuild.context({
plugins: [swcPlugin()],
watch: true,
});
При изменении файлов кэш SWC должен инвалидироваться, иначе возможны устаревшие результаты.
Причина: отсутствие кэша и последовательная обработка файлов.
Причина: бесконтрольный Map-кэш трансформаций.
Причина: несогласованность SWC map и esbuild chain.
Причина: дублирование трансформаций или неправильный runtime.
Устойчивый pipeline обычно выглядит так:
esbuild:
SWC:
tsc:
optional:
Такое разделение снижает связность инструментов и повышает предсказуемость сборки.