Поле peerDependencies и его связь с external

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

Поле peerDependencies в package.json описывает зависимости, которые:

  • не устанавливаются автоматически как обычные зависимости (в классическом npm v6 и ниже),
  • должны быть предоставлены проектом-потребителем,
  • должны соответствовать определённому диапазону версий.

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

{
  "peerDependencies": {
    "react": ">=17 <19"
  }
}

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

Такой подход решает проблему дублирования рантайм-библиотек. Например, если каждая библиотека будет поставлять собственную копию React, это приведёт к:

  • нарушению внутреннего состояния React,
  • конфликтам контекста,
  • увеличению размера итогового бандла.

Роль external в Rollup

Rollup оперирует другой, но концептуально связанной абстракцией — external.

В конфигурации Rollup:

export default {
  input: 'src/index.js',
  output: {
    file: 'dist/index.js',
    format: 'esm'
  },
  external: ['react']
};

Поле external говорит сборщику:

  • не включать указанный модуль в бандл,
  • оставить import как есть в итоговом коде.

То есть:

import React from 'react';

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

Принципиальная связь peerDependencies и external

Хотя peerDependencies и external относятся к разным уровням системы (npm и bundler), их логика совпадает: исключение зависимости из сборки с переносом ответственности на потребителя.

Общая модель ответственности

Механизм Уровень Что означает
peerDependencies package.json / npm «Эта зависимость должна быть установлена в проекте»
external Rollup «Не включать эту зависимость в бандл»

Вместе они формируют согласованную контрактную модель библиотеки:

  • peerDependencies описывает требования к окружению,
  • external реализует это требование на уровне сборки.

Почему одного peerDependencies недостаточно

Наличие peerDependencies само по себе не влияет на результат сборки. Rollup не читает это поле автоматически (без плагинов).

Рассмотрим пример:

{
  "peerDependencies": {
    "lodash": "^4.17.0"
  }
}

Если в Rollup не указать:

external: ['lodash']

то итоговая сборка может:

  • встроить lodash внутрь бандла,
  • несмотря на то, что библиотека заявляет его как peer-зависимость.

Это приводит к нарушению контракта: потребитель устанавливает lodash, но библиотека использует собственную копию.

Почему одного external недостаточно

Обратная ситуация также возможна. Если указать:

external: ['react']

но не объявить react в peerDependencies, возникают проблемы:

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

Таким образом:

  • external влияет на сборку,
  • peerDependencies влияет на установку и совместимость.

Они не взаимозаменяемы.

Типовые сценарии использования вместе

Библиотека UI-компонентов

{
  "peerDependencies": {
    "react": ">=18",
    "react-dom": ">=18"
  }
}
export default {
  input: 'src/index.js',
  external: ['react', 'react-dom']
};

Здесь важно, что React:

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

Плагин для фреймворка

Например, плагин для Vue:

{
  "peerDependencies": {
    "vue": "^3.0.0"
  }
}
export default {
  external: ['vue']
};

Такая связка гарантирует, что:

  • плагин использует тот же Vue-инстанс, что и приложение,
  • реактивность и DI-контейнеры не ломаются.

Автоматизация external через peerDependencies

В реальных сборках часто используется автоматическое преобразование peerDependencies в external.

Пример подхода:

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

export default {
  input: 'src/index.js',
  external: Object.keys(pkg.peerDependencies || {})
};

Здесь достигается синхронизация:

  • добавили peer-зависимость,
  • она автоматически исключается из бандла.

Это снижает риск рассинхронизации конфигурации.

Отличие peerDependencies от dependencies и devDependencies в контексте Rollup

dependencies

Обычно включаются в бандл, если не указаны в external.

{
  "dependencies": {
    "lodash": "^4.17.0"
  }
}

Если Rollup не исключит lodash, он попадёт в итоговый файл.

devDependencies

Используются только в процессе сборки:

  • Rollup,
  • плагины,
  • тестовые утилиты.

Они никогда не должны попадать в итоговый бандл.

peerDependencies

Представляют особый контракт:

  • библиотека не владеет зависимостью,
  • но требует её наличия извне,
  • и не должна включать её в сборку.

Проблема дублирования рантайма

Связка peerDependencies + external решает одну из самых критичных проблем JavaScript-экосистемы — дублирование рантайм-библиотек.

Пример проблемы

Если библиотека A и библиотека B обе включают React:

  • создаются два React-контекста,
  • хуки перестают работать корректно,
  • возникает ошибка invalid hook call.

Решение

  • React объявляется в peerDependencies,
  • React добавляется в external,
  • React существует в единственном экземпляре в приложении.

Особенности поведения в монорепозиториях

В монорепозиториях (pnpm, yarn workspaces) связь становится ещё более важной.

Даже если зависимость физически присутствует в node_modules, peerDependencies всё равно:

  • определяет совместимость версий,
  • фиксирует контракт между пакетами,
  • предотвращает неявное расхождение версий.

Rollup в таких проектах:

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

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

Забыто external при наличии peerDependencies

Симптомы:

  • увеличенный размер бандла,
  • дублирование React или Vue,
  • странные runtime-ошибки.

Причина:

  • dependency не исключена из сборки.

external без peerDependencies

Симптомы:

  • npm не предупреждает об отсутствии зависимости,
  • пользователь получает runtime error: module not found.

Причина:

  • отсутствует контракт на уровне package.json.

Несовпадение версий

"peerDependencies": {
  "react": "^18"
}

но в проекте используется React 17.

Симптомы:

  • предупреждения npm,
  • потенциальная несовместимость API.

Rollup в этой ситуации корректен, но контракт нарушен.

Архитектурная роль связки

Связка peerDependencies + external формирует фундаментальную архитектурную границу библиотеки:

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

Эта модель позволяет библиотекам:

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