Опция bundle и разрешение модулей

Опция bundle включает режим сборки, при котором esbuild не просто трансформирует отдельный файл, а строит граф зависимостей и объединяет все импортируемые модули в единый (или несколько) выходных файлов. Включение этого режима принципиально меняет поведение резолвинга и обработки импортов.

При значении:

bundle: true

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

При bundle: false (значение по умолчанию) каждый файл обрабатывается изолированно: import/export остаются в выходном коде (если формат это поддерживает), а реальное объединение не происходит.


Построение графа модулей

В режиме bundling esbuild формирует граф модулей, начиная с entry point:

  1. Анализируется входной файл
  2. Извлекаются все import и require
  3. Каждый импорт резолвится в конкретный файл
  4. Для каждого найденного файла процесс повторяется
  5. Формируется единый граф зависимостей без циклического дублирования модулей

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


Разрешение модулей (module resolution)

Механизм разрешения модулей в esbuild опирается на несколько источников информации:

1. Относительные и абсолютные пути

Простейший случай — относительные импорты:

import utils from "./utils.js";
import helper from "../core/helper.js";

Алгоритм:

  • определяется путь относительно текущего файла
  • проверяется существование файла
  • при отсутствии расширения выполняется попытка подстановки .js, .ts, .jsx, .tsx, .json (в зависимости от контекста)

2. Разрешение пакетов из node_modules

Импорт вида:

import express from "express";

запускает поиск в node_modules:

  • поиск ближайшего node_modules вверх по дереву директорий

  • чтение package.json

  • определение entry point через поля:

    • main
    • module
    • exports (приоритетный современный механизм)

3. Поле exports и conditional exports

Современные пакеты часто используют:

{
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    }
  }
}

esbuild учитывает условия экспорта в зависимости от режима сборки:

  • import — для ESM-сборки
  • require — для CJS
  • browser — при target: browser
  • дополнительные условия (node, default)

Если поле exports присутствует, оно имеет приоритет над main.


4. Учет типа модуля (ESM / CommonJS)

Разрешение также зависит от типа пакета:

  • "type": "module" в package.json.js трактуется как ESM
  • отсутствие type.js считается CommonJS

Это влияет на:

  • способ трансляции импортов
  • оборачивание require
  • совместимость с tree-shaking

5. Browser field

При сборке под браузер учитывается:

{
  "browser": {
    "fs": false,
    "./node.js": "./browser.js"
  }
}

Это позволяет:

  • заменять Node.js-зависимости
  • исключать модули (через false)
  • подменять реализации

Роль опции bundle в трансформации импортов

В режиме bundling esbuild выполняет преобразование импортов:

ESM импорты

import { readFile } from "fs";
  • либо заменяются на встроенные полифилы (если предусмотрено)
  • либо помечаются как external (если указано external: ["fs"])
  • либо удаляются при browser-target

CommonJS

const lib = require("lib");

переводится в единый модульный формат, совместимый с выбранным output format.


Связь bundle и external

Опция external напрямую влияет на процесс сборки:

bundle: true,
external: ["react", "react-dom"]

В этом случае:

  • модули react и react-dom исключаются из графа
  • import остаётся в выходном коде
  • зависимости не попадают в финальный бандл

Это используется для:

  • библиотек, где зависимости поставляются отдельно
  • серверных приложений с node_modules runtime

Tree-shaking при включенном bundle

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

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

Однако есть ограничения:

  • динамические require() ухудшают анализ
  • side effects модулей сохраняются, если не указано sideEffects: false

Особенности резолвинга в esbuild

1. Отсутствие полноценного TypeScript path mapping

esbuild не интерпретирует paths из tsconfig.json без плагинов. Для этого используются плагины резолвинга:

  • алиасы
  • виртуальные модули
  • кастомные схемы импорта

2. Строгая файловая модель

Резолвинг всегда привязан к реальной файловой системе:

  • виртуальные импорты возможны только через plugin API
  • отсутствует встроенная поддержка runtime module federation

3. Приоритет расширений

При поиске файла порядок может включать:

  1. .tsx
  2. .ts
  3. .jsx
  4. .js
  5. .json

Приоритет зависит от контекста сборки и платформы (platform: node | browser).


Поведение при platform

Опция platform сильно влияет на resolution:

platform: node

  • активируется поддержка Node.js core modules
  • сохраняются require
  • используется main и exports как в Node

platform: browser

  • Node core modules считаются внешними или заменяются заглушками
  • применяется browser field
  • усиливается tree-shaking

Влияние format на bundle

bundle: true работает совместно с format:

  • iife → единый скрипт для браузера
  • esm → единый ESM-бандл
  • cjs → CommonJS сборка для Node
  • umd → универсальный формат

Формат влияет на:

  • структуру обёрток модулей
  • способ экспорта
  • обработку динамических импортов

Обработка циклических зависимостей

При наличии циклов:

// a.js
import { b } from "./b.js";

// b.js
import { a } from "./a.js";

esbuild:

  • строит граф с детектированием циклов
  • избегает бесконечной рекурсии
  • сохраняет поведение спецификации ES modules (live bindings)

Дедупликация модулей

При сборке:

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

Ленивая загрузка и code splitting (в связке с bundle)

При использовании динамического импорта:

import("./module.js");

esbuild:

  • при включённом code splitting разделяет граф
  • создаёт отдельные чанки
  • сохраняет runtime загрузчик

Опция bundle является обязательной для code splitting.


Практическое влияние на архитектуру проекта

Включение bundle приводит к следующим изменениям:

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

Поведение при ошибках резолвинга

Если модуль не найден:

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

Типичные причины:

  • отсутствие расширения при нестандартной конфигурации
  • неправильный main/exports
  • несовместимость ESM/CJS
  • отсутствие пакета в node_modules