Опция external для peer dependencies

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

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

Опция:

  • позволяет исключать модули из бандла;
  • заменяет их на require() или import в результирующем коде;
  • предотвращает дублирование зависимостей;
  • сохраняет совместимость с системой peerDependencies.

Связь external и peerDependencies

peerDependencies в package.json описывает зависимости, которые должны быть предоставлены конечным проектом. Библиотека лишь объявляет совместимость, но не устанавливает их самостоятельно.

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

{
  "name": "my-ui-lib",
  "peerDependencies": {
    "react": ">=18",
    "react-dom": ">=18"
  }
}

Если при сборке не использовать external, esbuild может встроить react и react-dom внутрь бандла библиотеки. Это приводит к проблемам:

  • дублирование React в приложении;
  • нарушение правил хуков;
  • увеличение размера бандла;
  • конфликт версий React в runtime.

Базовое использование external

Опция задаётся в конфигурации esbuild:

import esbuild from 'esbuild';

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

В этом случае любые импорты:

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

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

Связь с peerDependencies на практике

Часто external формируется автоматически на основе package.json:

import pkg from './package.json' assert { type: 'json' };

const peerDeps = Object.keys(pkg.peerDependencies || {});

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

Такой подход гарантирует, что:

  • все peer-зависимости исключены из бандла;
  • конфигурация синхронизирована с метаданными пакета;
  • нет ручного рассинхрона между package.json и сборкой.

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

В зависимости от format (например, esm или cjs) поведение внешних зависимостей сохраняется, но способ подключения меняется:

CommonJS

const react = require('react');

ESM

import * as react from 'react';

Esbuild не внедряет код библиотеки, а оставляет ссылку на модуль для разрешения в рантайме.

Паттерны исключения зависимостей

Полное исключение peerDependencies

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

external: Object.keys(pkg.peerDependencies || {})

Используется в библиотеках UI, дизайн-системах, плагинах.

Комбинированное исключение

Иногда требуется исключить не только peerDependencies, но и крупные runtime-библиотеки:

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

Такой подход оправдан, когда библиотека позиционируется как надстройка над экосистемой.

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

esbuild поддерживает паттерны через external с использованием regex-совместимых строк:

external: [
  /^react/,
  /^@mui\//
]

Это позволяет исключить целые семейства пакетов:

  • все subpath импорты react/*
  • все пакеты scoped namespace @mui/*

Влияние на tree-shaking и размер бандла

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

  • код исключённых модулей не анализируется;
  • tree-shaking внутри них не выполняется;
  • размер итогового файла уменьшается;
  • зависимость переносится на runtime-резолвинг.

При неправильной конфигурации возможны проблемы:

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

Использование в библиотечной сборке

Типичный сценарий для библиотеки:

import esbuild from 'esbuild';
import pkg from './package.json' assert { type: 'json' };

const external = [
  ...Object.keys(pkg.peerDependencies || {}),
  ...Object.keys(pkg.dependencies || {})
];

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

Однако включение dependencies в external требует осторожности: это превращает библиотеку в тонкую обёртку над внешними модулями.

Различие между dependencies и peerDependencies в контексте external

  • dependencies — устанавливаются автоматически вместе с библиотекой
  • peerDependencies — должны быть предоставлены потребителем

Следовательно:

  • peerDependencies почти всегда должны быть external
  • dependencies — только если есть архитектурная причина не включать их в бандл

Ошибки конфигурации

Отсутствие external для peerDependencies

// ошибка: react попадёт в бандл
esbuild.build({
  bundle: true,
  external: []
});

Последствия:

  • дублирование React
  • несовместимость хуков
  • увеличение bundle size

Чрезмерное external

external: ['*']

Последствия:

  • фактически отсутствует бандлинг;
  • сборка теряет смысл;
  • код превращается в набор импортов.

Взаимодействие с ESM/CJS интеропом

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

  • ESM для современных сборок;
  • CommonJS для Node.js-окружений.

Практика организации конфигурации

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

export function createExternal(pkg) {
  return Object.keys(pkg.peerDependencies || {});
}

И применение:

external: createExternal(pkg)

Это обеспечивает:

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

Влияние на плагины и резолвинг

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

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

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

Стратегии для монорепозиториев

В монорепозиториях часто встречается комбинация:

  • workspace зависимости остаются external;
  • внешние npm-пакеты также external;
  • локальные пакеты иногда включаются в бандл.

Пример:

external: [
  ...Object.keys(pkg.peerDependencies || {}),
  /^@my-org\//
]

Это предотвращает дублирование внутренних пакетов монорепозитория.

Итоговая роль external в архитектуре сборки

Механизм external формирует границу между:

  • тем, что принадлежит библиотеке;
  • тем, что принадлежит окружению потребителя.

В связке с peerDependencies он обеспечивает предсказуемость, совместимость и отсутствие дублирования, позволяя библиотекам оставаться лёгкими и независимыми от конкретного runtime-окружения.