peerDependencies — механизм в экосистеме Node.js и
JavaScript, предназначенный для описания внешних зависимостей, которые
должны быть установлены в проекте-потребителе, а не внутри самой
библиотеки. Особенно активно используется при разработке:
Основная задача peerDependencies — избежать появления
нескольких несовместимых копий одной и той же библиотеки в итоговом
приложении.
Пример типичной ситуации:
{
"peerDependencies": {
"react": "^18.0.0"
}
}
Это означает:
react;react должен быть установлен в проекте
пользователя;Обычные runtime-зависимости.
Устанавливаются автоматически.
{
"dependencies": {
"lodash": "^4.17.21"
}
}
Webpack включает такие зависимости в dependency graph.
Инструменты разработки.
{
"devDependencies": {
"webpack": "^5.0.0",
"typescript": "^5.0.0"
}
}
Не нужны конечному пользователю библиотеки.
Ожидаемые внешние зависимости.
{
"peerDependencies": {
"react": "^18.0.0"
}
}
Не устанавливаются внутрь пакета как локальная копия зависимости.
Webpack строит единый граф зависимостей приложения.
Если библиотека содержит собственную копию критически важной зависимости, возникают проблемы:
Особенно опасны дубли:
{
"dependencies": {
"react": "^18.2.0"
}
}
Если библиотека публикуется с React внутри dependencies,
возможно появление двух копий React:
app
├── react
└── my-library
└── react
В результате:
instanceof начинает вести себя непредсказуемо;Invalid hook call
{
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
}
{
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"my-library": "^1.0.0"
}
}
Теперь используется единый экземпляр React.
Сам Webpack напрямую не интерпретирует поле
peerDependencies.
Webpack работает с уже установленным деревом
node_modules.
Однако peerDependencies косвенно влияют на:
Webpack использует механизм module resolution, похожий на Node.js.
Когда библиотека импортирует:
import React from 'react';
Webpack ищет модуль:
node_modules.При корректном использовании peerDependencies:
project
├── node_modules
│ ├── react
│ └── my-library
Используется одна общая версия.
До npm v7 peerDependencies только предупреждали:
warning "my-library" requires a peer of react@^18 but none is installed
Начиная с npm v7 peerDependencies могут устанавливаться автоматически.
Это изменило поведение многих проектов:
ERESOLVE unable to resolve dependency tree
Появились строгие проверки совместимости версий.
Поле позволяет делать peer dependency необязательной.
{
"peerDependencies": {
"sass": "^1.0.0"
},
"peerDependenciesMeta": {
"sass": {
"optional": true
}
}
}
Теперь библиотека может работать без Sass.
Часто применяется в:
Пример:
{
"peerDependencies": {
"webpack": "^5.0.0"
},
"peerDependenciesMeta": {
"webpack": {
"optional": true
}
}
}
Webpack plugin обычно требует конкретную версию Webpack.
{
"peerDependencies": {
"webpack": "^5.0.0"
}
}
Иначе возможна установка:
Это приводит к runtime-ошибкам.
Loaders также обычно объявляют Webpack как peer dependency.
{
"peerDependencies": {
"webpack": "^5.0.0"
}
}
Иногда дополнительно:
{
"peerDependencies": {
"webpack": "^5.0.0",
"webpack-cli": "^5.0.0"
}
}
Классический пример:
{
"peerDependencies": {
"@babel/core": "^7.0.0",
"webpack": ">=5"
}
}
babel-loader ожидает:
Но не включает их внутрь себя.
Пример:
{
"peerDependencies": {
"typescript": ">=4",
"webpack": "^5.0.0"
}
}
Loader использует TypeScript compiler из проекта пользователя.
Главная проблема — дублирование.
Пример:
{
"dependencies": {
"react": "^18"
},
"peerDependencies": {
"react": "^18"
}
}
Это частично ломает саму идею peer dependency.
В старых пакетах подобная схема встречалась часто.
Иногда библиотека использует dependency и как runtime-зависимость, и как peer dependency.
Например:
{
"peerDependencies": {
"react": "^18"
},
"devDependencies": {
"react": "^18"
}
}
Это нормальная практика.
devDependencies нужны для разработки библиотеки.
При разработке библиотек Webpack часто комбинируется с
externals.
module.exports = {
externals: {
react: 'react',
'react-dom': 'react-dom'
}
};
Webpack не включает React в bundle.
Это идеально сочетается с peerDependencies.
Обычно используется следующая схема:
{
"peerDependencies": {
"react": "^18",
"react-dom": "^18"
}
}
module.exports = {
externals: {
react: 'react',
'react-dom': 'react-dom'
}
};
Результат:
Если peer dependency случайно попала внутрь bundle, размер сборки резко увеличивается.
Типичный пример:
| Библиотека | Размер |
|---|---|
| Без React | 15 KB |
| С React | 150+ KB |
Peer dependencies улучшают tree shaking косвенно.
Если библиотека поставляется без встроенной копии React/Vue:
В Module Federation peerDependencies особенно важны.
new ModuleFederationPlugin({
shared: {
react: {
singleton: true
}
}
});
Без singleton возможны:
Module Federation фактически развивает идею peer dependencies.
Shared modules:
Пример:
shared: {
react: {
singleton: true,
eager: true
}
}
eager заставляет модуль загружаться сразу.
Но singleton остается критически важным.
Module Federation поддерживает строгую проверку версии.
shared: {
react: {
singleton: true,
strictVersion: true
}
}
При несовместимых версиях возможно исключение.
В monorepo peer dependencies используются постоянно.
Особенно в:
Менеджеры пакетов поднимают зависимости вверх.
root
├── node_modules
│ └── react
└── packages
├── ui
└── app
Peer dependencies помогают корректному hoisting.
pnpm гораздо строже относится к peer dependencies.
Ошибки появляются быстрее:
Unmet peer dependency
Это помогает раньше обнаруживать несовместимости.
Yarn Plug’n’Play практически требует корректной работы с peer dependencies.
Некорректные зависимости могут полностью ломать resolution.
Типичный конфликт:
Package A requires React 17
Package B requires React 18
npm не может выбрать совместимую версию.
Основные подходы:
{
"peerDependencies": {
"react": ">=17 <20"
}
}
Если библиотека несовместима:
v1 -> React 17
v2 -> React 18
Иногда используется alias:
npm install react18@npm:react@18
Но это редкий и сложный сценарий.
Peer dependencies должны максимально точно описывать совместимость.
Плохой пример:
{
"peerDependencies": {
"webpack": "*"
}
}
Это бесполезно.
{
"peerDependencies": {
"webpack": "^5.0.0"
}
}
{
"peerDependencies": {
"react": ">=18 <19"
}
}
Плохой пример:
{
"peerDependencies": {
"react": "18.2.0"
}
}
Это вызывает ненужные конфликты.
Тоже опасно:
{
"peerDependencies": {
"webpack": ">=1"
}
}
Webpack 1 и Webpack 5 несовместимы.
Типичная конфигурация:
{
"peerDependencies": {
"react": "^18",
"react-dom": "^18"
}
}
Дополнительно:
{
"devDependencies": {
"react": "^18",
"react-dom": "^18"
}
}
{
"peerDependencies": {
"vue": "^3.0.0"
}
}
ESLint ecosystem активно использует peerDependencies.
{
"peerDependencies": {
"eslint": "^9.0.0"
}
}
{
"peerDependencies": {
"@babel/core": "^7.0.0"
}
}
При сборке библиотеки особенно важно:
Популярная практика:
const pkg = require('./package.json');
module.exports = {
externals: Object.keys(pkg.peerDependencies || {})
};
Webpack автоматически исключает peer dependencies из bundle.
Хотя Rollup работает иначе, концепция аналогична.
Часто используется:
external: ['react', 'react-dom']
Peer dependencies и external обычно синхронизируются.
В Vite также важно исключать peer dependencies:
build: {
rollupOptions: {
external: ['react']
}
}
Полезные инструменты:
Они помогают обнаруживать:
Самая распространенная проблема.
Peer dependency объявлена, но попала в bundle.
Вызывают конфликты установки.
Приводят к runtime-несовместимости.
Например:
{
"peerDependencies": {
"react": "^18"
}
}
Но библиотека фактически использует API React 19.
Полезно тестировать библиотеку:
npm pack
Затем:
npm install ../my-library-1.0.0.tgz
Это помогает выявлять:
Хотя прямой связи нет, библиотеки часто одновременно настраивают:
{
"sideEffects": false
}
и корректные peer dependencies.
Это улучшает:
Наиболее распространенная схема:
Только реальные внутренние runtime-зависимости.
Все framework/runtime singleton-зависимости:
Инструменты сборки и тестирования.
{
"name": "my-ui-kit",
"main": "dist/index.js",
"peerDependencies": {
"react": "^18",
"react-dom": "^18"
},
"devDependencies": {
"react": "^18",
"react-dom": "^18",
"webpack": "^5",
"babel-loader": "^9"
}
}
module.exports = {
mode: 'production',
externals: {
react: 'react',
'react-dom': 'react-dom'
},
output: {
library: {
type: 'module'
}
},
experiments: {
outputModule: true
}
};
Правильная обработка peer dependencies обеспечивает: