esbuild в Nx и Turborepo

В современных монорепозиториях основная нагрузка при сборке ложится не только на транспиляцию исходного кода, но и на управление зависимостями между проектами, кэширование результатов, параллельное выполнение задач и оптимизацию времени обратной связи для разработчиков. Именно поэтому такие инструменты, как Nx и Turborepo, не заменяют сборщики, а выступают надстройками над ними.

esbuild занимает особое место в этой экосистеме благодаря чрезвычайно высокой скорости работы. Написанный на Go, он способен выполнять сборку проектов в десятки и сотни раз быстрее традиционных JavaScript-инструментов, что особенно заметно в крупных монорепозиториях.

Основные сценарии использования esbuild внутри Nx и Turborepo:

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

Архитектура монорепозитория и место esbuild

Типичная структура проекта может выглядеть следующим образом:

repo/
├── apps/
│   ├── api/
│   └── web/
├── packages/
│   ├── shared/
│   ├── ui/
│   └── utils/
├── nx.json
├── turbo.json
└── package.json

В такой архитектуре:

  • Nx или Turborepo управляют зависимостями между проектами;
  • esbuild отвечает за непосредственную компиляцию кода;
  • менеджер пакетов обеспечивает установку зависимостей.

Получается следующая цепочка:

Исходный код
       ↓
Nx/Turborepo определяет граф зависимостей
       ↓
Запуск задачи build
       ↓
esbuild компилирует проект
       ↓
Результат попадает в кэш
       ↓
Повторные сборки используют кэш

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


Использование esbuild в Nx

Генерация проекта с поддержкой esbuild

Nx предоставляет собственные executors для работы с esbuild.

Создание Node.js-приложения:

nx g @nx/node:application api

Создание библиотеки:

nx g @nx/js:library shared

В конфигурации проекта можно использовать executor esbuild:

{
  "targets": {
    "build": {
      "executor": "@nx/esbuild:esbuild",
      "options": {
        "main": "src/main.ts",
        "outputPath": "dist/apps/api",
        "tsConfig": "tsconfig.app.json"
      }
    }
  }
}

Здесь Nx становится оркестратором задачи, а непосредственную компиляцию выполняет esbuild.


Executor @nx/esbuild

Executor представляет собой интеграционный слой между системой задач Nx и API esbuild.

Пример расширенной конфигурации:

{
  "build": {
    "executor": "@nx/esbuild:esbuild",
    "options": {
      "main": "src/main.ts",
      "outputPath": "dist/apps/api",
      "tsConfig": "tsconfig.app.json",
      "bundle": true,
      "platform": "node",
      "format": ["cjs"],
      "generatePackageJson": true,
      "thirdParty": false
    }
  }
}

Основные параметры:

Параметр Назначение
main Точка входа
outputPath Каталог результата
bundle Объединение модулей
platform browser или node
format esm или cjs
generatePackageJson Генерация package.json
thirdParty Включение внешних зависимостей

Сборка библиотек

Библиотеки являются важнейшим элементом любого монорепозитория.

Пример библиотеки:

libs/
└── shared/
    ├── src/
    │   └── index.ts
    └── project.json

Конфигурация сборки:

{
  "targets": {
    "build": {
      "executor": "@nx/esbuild:esbuild",
      "options": {
        "main": "libs/shared/src/index.ts",
        "outputPath": "dist/libs/shared",
        "bundle": true,
        "format": ["esm"]
      }
    }
  }
}

После выполнения:

nx build shared

будет создан пакет:

dist/libs/shared/
├── index.js
└── package.json

Работа с графом зависимостей Nx

Одним из ключевых преимуществ Nx является автоматическое построение графа проектов.

Пример:

api
 ├── shared
 └── utils

При запуске:

nx build api

Nx определяет:

shared → utils → api

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

Если библиотека shared не изменилась, ее артефакты могут быть взяты из кэша.

Это особенно эффективно в сочетании с высокой скоростью esbuild.


Инкрементальная сборка

Nx хранит информацию о:

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

Если ничего не изменилось:

nx build api

может завершиться практически мгновенно благодаря восстановлению результата из локального или удалённого кэша.

При использовании esbuild скорость выполнения увеличивается ещё сильнее, поскольку даже при необходимости пересборки время компиляции минимально.


Генерация package.json

Для публикации библиотек полезно автоматически создавать package.json.

Пример:

{
  "generatePackageJson": true
}

После сборки:

dist/
└── shared/
    ├── index.js
    └── package.json

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

Это уменьшает размер публикуемого пакета.


Поддержка нескольких форматов

Современные библиотеки часто распространяются одновременно в ESM и CommonJS.

Конфигурация:

{
  "format": ["esm", "cjs"]
}

Результат:

dist/
├── index.mjs
├── index.cjs
└── package.json

Подобный подход обеспечивает совместимость со старыми и новыми экосистемами Node.js.


Пользовательская конфигурация esbuild в Nx

Иногда стандартного executor недостаточно.

Можно использовать собственный конфигурационный файл.

const esbuild = require("esbuild");

esbuild.build({
  entryPoints: ["src/main.ts"],
  outfile: "dist/main.js",
  bundle: true,
  platform: "node"
});

Далее создается кастомный target:

{
  "targets": {
    "build": {
      "command": "node build.js"
    }
  }
}

Nx продолжает управлять зависимостями и кэшированием, а логика сборки полностью контролируется разработчиком.


Использование esbuild в Turborepo

Концепция Turborepo

Turborepo использует иной подход.

Если Nx предоставляет собственные executors, то Turborepo практически не вмешивается в процесс сборки.

Основная задача Turborepo:

  • вычисление зависимостей;
  • распределение задач;
  • локальное кэширование;
  • удалённое кэширование;
  • параллельное выполнение.

Сборщик выбирается самостоятельно.

Очень часто этим сборщиком становится именно esbuild.


Типичная структура проекта

apps/
├── api/
└── web/

packages/
├── shared/
├── ui/
└── config/

turbo.json
package.json

Каждый пакет может иметь собственную сборочную конфигурацию.

Например:

{
  "scripts": {
    "build": "node build.js"
  }
}

Настройка turbo.json

Базовая конфигурация:

{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    }
  }
}

Параметр:

"dependsOn": ["^build"]

означает:

Сначала собрать зависимости
Потом собрать текущий пакет

Конфигурация esbuild для пакета

Файл build.js:

const esbuild = require("esbuild");

esbuild.build({
  entryPoints: ["src/index.ts"],
  outdir: "dist",
  bundle: true,
  format: "esm",
  platform: "node"
});

package.json:

{
  "scripts": {
    "build": "node build.js"
  }
}

Запуск:

turbo run build

Turborepo самостоятельно определяет порядок выполнения задач.


Параллельная сборка

Предположим, имеются пакеты:

shared
utils
ui
docs

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

shared
utils
docs

После завершения:

ui

если ui зависит от shared.

esbuild здесь обеспечивает быстрые отдельные сборки, а Turborepo максимизирует параллелизм.


Удалённое кэширование

Одной из сильнейших возможностей Turborepo является Remote Cache.

Процесс выглядит следующим образом:

CI сервер
      ↓
Выполнил build
      ↓
Загрузил артефакт
      ↓
Remote Cache
      ↓
Другой разработчик
      ↓
Получил готовый результат

При использовании esbuild это создаёт практически мгновенные повторные сборки.


Сборка npm-пакетов в Turborepo через esbuild

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

const esbuild = require("esbuild");

esbuild.build({
  entryPoints: ["src/index.ts"],
  outdir: "dist",
  bundle: true,
  minify: true,
  sourcemap: true,
  target: "es2022",
  format: "esm"
});

package.json:

{
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts"
}

Сборка становится частью общего графа задач Turborepo.


Сравнение интеграции esbuild в Nx и Turborepo

Возможность Nx Turborepo
Встроенный executor esbuild Да Нет
Граф зависимостей Да Да
Локальный кэш Да Да
Удалённый кэш Да Да
Генераторы проектов Да Нет
Интеграция из коробки Высокая Минимальная
Гибкость настройки Средняя Очень высокая
Контроль над сборкой Средний Полный

Практики использования esbuild в монорепозиториях

Вынос общей конфигурации

Часто создаётся общий файл:

module.exports = {
  bundle: true,
  sourcemap: true,
  target: "es2022"
};

Далее он импортируется всеми пакетами:

const common = require("../. ./build/esbuild.config");

Это упрощает сопровождение больших репозиториев.


Использование внешних зависимостей

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

esbuild.build({
  bundle: true,
  external: [
    "express",
    "pg",
    "typeorm"
  ]
});

Размер сборки уменьшается, а запуск ускоряется.


Разделение приложений и библиотек

Распространённая схема:

apps/
packages/

Где:

  • приложения собираются отдельно;
  • библиотеки публикуются независимо;
  • esbuild отвечает за упаковку каждого артефакта;
  • Nx или Turborepo контролируют порядок выполнения задач.

Генерация source maps

Для отладки рекомендуется включать карты исходников:

esbuild.build({
  sourcemap: true
});

При возникновении ошибки стек вызовов будет ссылаться на исходный TypeScript-код.


Оптимизация CI/CD

Типичный конвейер:

Git Push
    ↓
Turbo/Nx анализирует изменения
    ↓
Запускаются только изменённые проекты
    ↓
esbuild собирает нужные пакеты
    ↓
Кэшируются результаты

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


Использование watch-режима

Во время разработки esbuild может работать в режиме наблюдения:

await esbuild.context({
  entryPoints: ["src/index.ts"],
  bundle: true,
  outdir: "dist"
}).then(ctx => ctx.watch());

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


Комбинирование esbuild с другими инструментами

В реальных проектах esbuild нередко используется совместно с другими средствами:

Nx / Turborepo
        ↓
esbuild
        ↓
TypeScript
        ↓
Jest
        ↓
ESLint

Либо:

Turborepo
       ↓
esbuild
       ↓
Vite
       ↓
React

Подобная архитектура позволяет сохранить преимущества каждого инструмента: скорость esbuild, интеллектуальное управление задачами Nx или Turborepo и специализированные возможности остальных компонентов экосистемы.