Опция external: исключение пакетов из бандла

Esbuild предоставляет механизм external, позволяющий исключать определённые зависимости из процесса бандлинга и оставлять их в виде импортов в итоговом коде. Эта опция критична при сборке библиотек, работе с внешними зависимостями, интеграции с CDN и управлении peer-dependencies.

При стандартной сборке esbuild анализирует граф импортов и стремится инлайнить все найденные модули внутрь результирующего бандла. Это поведение не всегда желательно: некоторые зависимости должны оставаться внешними и резолвиться в окружении выполнения.

Опция external изменяет поведение резолвера: указанные модули исключаются из графа бандлинга и сохраняются как import или require в выходном файле.

Ключевая особенность:

external не удаляет импорт — он запрещает его обработку как внутренней зависимости сборки.

Базовый синтаксис

JavaScript API

import esbuild from 'esbuild';

esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  outfile: 'dist/bundle.js',
  external: ['react', 'react-dom']
});

В этом примере react и react-dom не попадут в бандл, а останутся внешними зависимостями.

CLI

esbuild src/index.js --bundle --outfile=dist/bundle.js --external:react --external:react-dom

CLI-форма использует префикс --external: для каждого пакета.

Поведение при сборке

При использовании external esbuild:

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

Пример:

import React from 'react';
import { createRoot } from 'react-dom/client';

console.log(React.version);

При external: ['react'] результат будет:

import React from 'react';
import { createRoot } from 'react-dom/client';

console.log(React.version);

react не инлайнится, но импорт остаётся.

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

Один из наиболее распространённых сценариев — библиотеки.

package.json:

{
  "peerDependencies": {
    "react": "^18.0.0"
  }
}

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

esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  outfile: 'dist/index.js',
  external: ['react']
});

Смысл:

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

Массовое исключение node_modules

Часто требуется исключить все зависимости из node_modules.

Регулярное выражение

esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  outfile: 'dist/bundle.js',
  external: [/node_modules/]
});

Однако более корректный и распространённый подход:

external: ['*']

или более точечно:

external: ['react', 'react-dom', 'lodash']

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

External для Node.js-таргета

При сборке под Node.js часто требуется сохранить встроенные модули:

esbuild.build({
  entryPoints: ['src/server.js'],
  bundle: true,
  platform: 'node',
  outfile: 'dist/server.js',
  external: ['fs', 'path', 'http']
});

Хотя platform: 'node' уже автоматически помечает встроенные модули как внешние, явное указание используется для контроля поведения в сложных конфигурациях.

Взаимодействие с платформами

Browser

В браузерных сборках external используется для:

  • CDN-зависимостей
  • динамически подключаемых скриптов
  • разделения vendor-кода
external: ['jquery']

В результате:

import $ from 'jquery';

Остаётся как есть и предполагается глобальная загрузка jquery.

Node.js

Используется для сохранения require() или import на уровне рантайма.

Паттерны и расширенные случаи

1. Namespace-исключение

external: ['@company/*']

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

2. Смешанная стратегия

external: [
  'react',
  'react-dom',
  /^@internal\//
]

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

3. CDN-интеграция

При сборке библиотеки для браузера:

esbuild.build({
  entryPoints: ['src/widget.js'],
  bundle: true,
  format: 'esm',
  outfile: 'dist/widget.js',
  external: ['vue']
});

Ожидается, что Vue загружается отдельно:

<script src="https://unpkg.com/vue@3"></script>

Влияние на tree-shaking и оптимизацию

external полностью исключает модуль из анализа графа:

  • tree-shaking внутри external-пакета не выполняется
  • код не минифицируется
  • зависимости внешнего модуля не рассматриваются

Это важный поведенческий момент: external — это граница оптимизации.

Отличие от alias и resolve

Механизм Поведение
external исключает модуль из сборки
alias заменяет путь на другой
resolve управляет поиском модуля

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

Типичные ошибки

Ошибка 1: ожидание удаления кода

external: ['react']

Ожидание: React исчезнет из результата.

Фактическое поведение: React остаётся импортом.


Ошибка 2: чрезмерный external

external: ['*']

Результат: бандл перестаёт быть самодостаточным, теряется смысл сборки для фронтенда.


Ошибка 3: конфликт с ESM/CJS

При смешанных форматах может возникнуть ситуация, когда external приводит к несовместимым импортам в разных окружениях.

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

Использование external определяется архитектурной ролью модуля:

  • библиотека → external для peerDependencies
  • приложение → минимальный external (обычно только CDN/системные модули)
  • SSR → частичный external для node-API и платформенных модулей
  • микрофронтенды → selective external по namespace

Поведение при разных форматах вывода

ESM

import React from 'react';

CommonJS

const React = require('react');

external сохраняет синтаксис в зависимости от format, но не изменяет факт исключения из бандла.

Итоговая модель

external в esbuild — это механизм декларативного управления границами сборки. Он определяет, какие зависимости принадлежат текущему артефакту, а какие остаются ответственностью внешней среды выполнения, формируя чёткое разделение между компиляцией и рантаймом.