Внешние зависимости: поле peerDependencies и externals

В JavaScript-проектах существует несколько уровней зависимости: зависимости приложения, транзитивные зависимости и зависимости, которые библиотека ожидает получить от окружения потребителя. В контексте сборщика Parcel ключевое значение приобретают два механизма — peerDependencies и externals, которые определяют границы включения кода в итоговую сборку.

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


peerDependencies: смысл и контракт между библиотекой и приложением

peerDependencies в package.json задаёт зависимость не на установку, а на совместимость.

Основная идея

peerDependencies описывает:

  • какие пакеты должны быть установлены в проекте-потребителе
  • но не должны быть установлены автоматически внутри самой библиотеки
  • и не должны дублироваться в node_modules библиотеки

Иначе говоря, это контракт на окружение, а не на поставку кода.


Семантика peerDependencies

Пример:

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

Это означает:

  • библиотека не поставляет react
  • библиотека предполагает, что react уже существует в приложении
  • несовпадение версий приводит к предупреждениям npm/yarn/pnpm

Почему peerDependencies критичны в UI-библиотеках

Наиболее частый сценарий — библиотеки, работающие поверх React или Vue.

Если библиотека случайно включает собственную копию React:

  • ломается reconciliation
  • появляются ошибки hooks
  • возникает дублирование контекста
  • увеличивается размер бандла

peerDependencies предотвращают эту проблему на уровне менеджера пакетов.


Поведение Parcel с peerDependencies

Parcel не интерпретирует peerDependencies как инструкцию для бандлинга напрямую. Он опирается на следующие принципы:

  • зависимости резолвятся через node_modules
  • если модуль доступен, он включается в граф зависимостей
  • если модуль помечен как внешний (externals), он исключается из бандла

Важно различать:

  • peerDependencies — декларация npm-уровня
  • поведение Parcel — решение на уровне сборки

Parcel не исключает автоматически peerDependencies из бандла, если они импортируются.


Типичный сценарий ошибки

import React from "react";

Если react указан только в peerDependencies, Parcel всё равно:

  • найдёт его в node_modules проекта
  • включит в граф
  • потенциально встроит в bundle, если не настроено внешнее исключение

Это создаёт риск дублирования при библиотечной сборке.


externals: управление включением модулей в бандл

externals в Parcel — механизм явного исключения зависимостей из итогового bundle.

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


Концепция externals

Если модуль объявлен как external:

  • Parcel не включает его код в сборку
  • импорт остаётся как runtime-зависимость
  • предполагается, что он будет доступен извне (CDN, host app, runtime)

Настройка externals в Parcel

В Parcel v2 externals задаются через package.json в секции targets:

{
  "name": "my-ui-lib",
  "peerDependencies": {
    "react": ">=18",
    "react-dom": ">=18"
  },
  "targets": {
    "default": {
      "external": ["react", "react-dom"]
    }
  }
}

Эффект применения externals

После настройки:

import React from "react";

Parcel:

  • не включает React в bundle библиотеки
  • оставляет импорт как внешний
  • предполагает, что React будет предоставлен приложением

Различие peerDependencies и externals

Несмотря на схожую цель, механизмы решают разные задачи.

peerDependencies

  • управляются пакетным менеджером
  • контролируют установку зависимостей
  • влияют на node_modules
  • работают на этапе install

externals

  • управляются сборщиком Parcel
  • контролируют включение кода в bundle
  • работают на этапе build

Таблица различий

Механизм Уровень Назначение
peerDependencies npm install предотвращение дублирования пакетов
externals bundling исключение кода из сборки

Совместное использование peerDependencies и externals

Корректная библиотечная конфигурация обычно требует комбинации:

{
  "peerDependencies": {
    "react": ">=18"
  },
  "targets": {
    "default": {
      "external": ["react"]
    }
  }
}

Такой подход обеспечивает:

  • отсутствие установки React внутри библиотеки
  • отсутствие включения React в bundle
  • единый экземпляр React в приложении

Пример архитектуры UI-библиотеки

package.json

{
  "name": "ui-kit",
  "version": "1.0.0",
  "peerDependencies": {
    "react": ">=18",
    "react-dom": ">=18"
  },
  "dependencies": {
    "clsx": "^2.0.0"
  },
  "targets": {
    "default": {
      "external": ["react", "react-dom"]
    }
  }
}

Поведение зависимостей

  • clsx попадает в bundle (обычная зависимость)
  • react и react-dom остаются внешними
  • потребитель библиотеки обязан предоставить React

Parcel и транзитивные зависимости

Parcel не различает семантику peerDependencies при обходе графа зависимостей:

  • если модуль импортирован, он анализируется
  • если он не external — он может попасть в bundle
  • peerDependencies не влияют напрямую на граф сборки

Это часто приводит к ошибке:

“peer dependency установлен, но всё равно попал в bundle”

Решение — явное использование external.


Сборка библиотек и проблема дублирования React

Без externals возможны следующие проблемы:

  • два React в одном приложении
  • нарушение правил hooks
  • неконсистентное состояние контекста

Причина:

  • библиотека и приложение получают разные экземпляры пакета

Monorepo и внешние зависимости

В monorepo структурах (например, pnpm workspace):

  • зависимости часто hoisted
  • Parcel может видеть общий node_modules
  • риск случайного включения shared пакетов выше

В таких условиях externals становится обязательным инструментом стабилизации сборки.


Интеракция с tree-shaking

Важно учитывать:

  • externals полностью исключаются из графа
  • tree-shaking к ним не применяется
  • оптимизация Parcel не влияет на внешний код

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


Debugging внешних зависимостей

Типичные признаки проблем:

  • дублирование React runtime
  • ошибки invalid hook call
  • увеличение размера bundle
  • неожиданные runtime import errors

Диагностика включает:

  • проверку package.json targets
  • анализ итогового bundle
  • проверку node_modules resolution

Практическая модель взаимодействия

Корректная архитектура библиотечного пакета в Parcel строится по следующей логике:

  • npm гарантирует наличие peer-зависимостей
  • Parcel исключает их из bundle через externals
  • остальные зависимости собираются как часть кода
  • итоговый bundle остаётся лёгким и совместимым с host-приложением