Peer dependencies и их обработка

peerDependencies — механизм в экосистеме Node.js и JavaScript, предназначенный для описания внешних зависимостей, которые должны быть установлены в проекте-потребителе, а не внутри самой библиотеки. Особенно активно используется при разработке:

  • UI-библиотек;
  • плагинов;
  • расширений;
  • loader’ов и plugin’ов для Webpack;
  • React/Vue/Angular компонентов;
  • интеграционных библиотек.

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

Пример типичной ситуации:

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

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

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

Отличие dependencies, devDependencies и peerDependencies

dependencies

Обычные runtime-зависимости.

Устанавливаются автоматически.

{
  "dependencies": {
    "lodash": "^4.17.21"
  }
}

Webpack включает такие зависимости в dependency graph.


devDependencies

Инструменты разработки.

{
  "devDependencies": {
    "webpack": "^5.0.0",
    "typescript": "^5.0.0"
  }
}

Не нужны конечному пользователю библиотеки.


peerDependencies

Ожидаемые внешние зависимости.

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

Не устанавливаются внутрь пакета как локальная копия зависимости.


Почему peerDependencies важны для Webpack

Webpack строит единый граф зависимостей приложения.

Если библиотека содержит собственную копию критически важной зависимости, возникают проблемы:

  • дублирование кода;
  • увеличение размера bundle;
  • конфликт singleton-библиотек;
  • несовместимость контекстов;
  • ошибки React hooks;
  • разные экземпляры runtime.

Особенно опасны дубли:

  • React;
  • Vue;
  • MobX;
  • Redux;
  • styled-components;
  • RxJS;
  • Angular core packages.

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

Неправильная конфигурация

{
  "dependencies": {
    "react": "^18.2.0"
  }
}

Если библиотека публикуется с React внутри dependencies, возможно появление двух копий React:

app
 ├── react
 └── my-library
      └── react

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

  • Hooks ломаются;
  • context API работает некорректно;
  • instanceof начинает вести себя непредсказуемо;
  • появляются ошибки вида:
Invalid hook call

Правильная схема

package.json библиотеки

{
  "peerDependencies": {
    "react": "^18.0.0",
    "react-dom": "^18.0.0"
  }
}

package.json приложения

{
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "my-library": "^1.0.0"
  }
}

Теперь используется единый экземпляр React.


Как Webpack обрабатывает peerDependencies

Сам Webpack напрямую не интерпретирует поле peerDependencies.

Webpack работает с уже установленным деревом node_modules.

Однако peerDependencies косвенно влияют на:

  • структуру dependency graph;
  • deduplication;
  • tree shaking;
  • module resolution;
  • singleton runtime;
  • externals;
  • federation;
  • bundle size.

Node.js resolution и peerDependencies

Webpack использует механизм module resolution, похожий на Node.js.

Когда библиотека импортирует:

import React from 'react';

Webpack ищет модуль:

  1. локально;
  2. выше по дереву;
  3. в корневом node_modules.

При корректном использовании peerDependencies:

project
 ├── node_modules
 │    ├── react
 │    └── my-library

Используется одна общая версия.


npm v7+ и автоматическая установка peerDependencies

До 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

Появились строгие проверки совместимости версий.


peerDependenciesMeta

Поле позволяет делать peer dependency необязательной.

{
  "peerDependencies": {
    "sass": "^1.0.0"
  },
  "peerDependenciesMeta": {
    "sass": {
      "optional": true
    }
  }
}

Теперь библиотека может работать без Sass.


Использование optional peer dependencies

Часто применяется в:

  • plugin ecosystem;
  • интеграциях;
  • адаптерах;
  • multi-framework библиотеках.

Пример:

{
  "peerDependencies": {
    "webpack": "^5.0.0"
  },
  "peerDependenciesMeta": {
    "webpack": {
      "optional": true
    }
  }
}

Peer dependencies для Webpack plugin

Webpack plugin обычно требует конкретную версию Webpack.

Правильная схема

{
  "peerDependencies": {
    "webpack": "^5.0.0"
  }
}

Иначе возможна установка:

  • webpack 4;
  • plugin под webpack 5.

Это приводит к runtime-ошибкам.


Peer dependencies для loader

Loaders также обычно объявляют Webpack как peer dependency.

{
  "peerDependencies": {
    "webpack": "^5.0.0"
  }
}

Иногда дополнительно:

{
  "peerDependencies": {
    "webpack": "^5.0.0",
    "webpack-cli": "^5.0.0"
  }
}

Babel-loader и peerDependencies

Классический пример:

{
  "peerDependencies": {
    "@babel/core": "^7.0.0",
    "webpack": ">=5"
  }
}

babel-loader ожидает:

  • установленный Webpack;
  • установленный Babel Core.

Но не включает их внутрь себя.


TypeScript loader и peerDependencies

Пример:

{
  "peerDependencies": {
    "typescript": ">=4",
    "webpack": "^5.0.0"
  }
}

Loader использует TypeScript compiler из проекта пользователя.


Почему нельзя класть peer dependency в dependencies

Главная проблема — дублирование.

Пример:

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

Это частично ломает саму идею peer dependency.

В старых пакетах подобная схема встречалась часто.


Когда допустимо дублирование

Иногда библиотека использует dependency и как runtime-зависимость, и как peer dependency.

Например:

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

Это нормальная практика.

devDependencies нужны для разработки библиотеки.


peerDependencies и externals

При разработке библиотек Webpack часто комбинируется с externals.

Пример

module.exports = {
  externals: {
    react: 'react',
    'react-dom': 'react-dom'
  }
};

Webpack не включает React в bundle.

Это идеально сочетается с peerDependencies.


Связка peerDependencies + externals

Обычно используется следующая схема:

package.json

{
  "peerDependencies": {
    "react": "^18",
    "react-dom": "^18"
  }
}

webpack.config.js

module.exports = {
  externals: {
    react: 'react',
    'react-dom': 'react-dom'
  }
};

Результат:

  • React не попадает в bundle;
  • используется React приложения;
  • отсутствуют дубликаты.

Проверка размера bundle

Если peer dependency случайно попала внутрь bundle, размер сборки резко увеличивается.

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

Библиотека Размер
Без React 15 KB
С React 150+ KB

Tree shaking и peerDependencies

Peer dependencies улучшают tree shaking косвенно.

Если библиотека поставляется без встроенной копии React/Vue:

  • Webpack анализирует единый dependency graph;
  • уменьшается дублирование;
  • оптимизация работает эффективнее.

Module Federation и peerDependencies

В Module Federation peerDependencies особенно важны.

shared singleton

new ModuleFederationPlugin({
  shared: {
    react: {
      singleton: true
    }
  }
});

Без singleton возможны:

  • две копии React;
  • разные contexts;
  • сломанные hooks.

Совместное использование shared modules

Module Federation фактически развивает идею peer dependencies.

Shared modules:

  • используются совместно;
  • не дублируются;
  • имеют единую версию runtime.

eager и peer dependencies

Пример:

shared: {
  react: {
    singleton: true,
    eager: true
  }
}

eager заставляет модуль загружаться сразу.

Но singleton остается критически важным.


strictVersion

Module Federation поддерживает строгую проверку версии.

shared: {
  react: {
    singleton: true,
    strictVersion: true
  }
}

При несовместимых версиях возможно исключение.


Peer dependencies и monorepo

В monorepo peer dependencies используются постоянно.

Особенно в:

  • Nx;
  • Turborepo;
  • Lerna;
  • pnpm workspace;
  • Yarn workspace.

Hoisting

Менеджеры пакетов поднимают зависимости вверх.

root
 ├── node_modules
 │    └── react
 └── packages
      ├── ui
      └── app

Peer dependencies помогают корректному hoisting.


pnpm и peerDependencies

pnpm гораздо строже относится к peer dependencies.

Ошибки появляются быстрее:

Unmet peer dependency

Это помогает раньше обнаруживать несовместимости.


Yarn PnP

Yarn Plug’n’Play практически требует корректной работы с peer dependencies.

Некорректные зависимости могут полностью ломать resolution.


Peer dependency conflict

Типичный конфликт:

Package A requires React 17
Package B requires React 18

npm не может выбрать совместимую версию.


Разрешение конфликтов

Основные подходы:

Расширение диапазона версий

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

Публикация новой major версии

Если библиотека несовместима:

v1 -> React 17
v2 -> React 18

Alias

Иногда используется alias:

npm install react18@npm:react@18

Но это редкий и сложный сценарий.


SemVer и peerDependencies

Peer dependencies должны максимально точно описывать совместимость.

Плохой пример:

{
  "peerDependencies": {
    "webpack": "*"
  }
}

Это бесполезно.


Хорошие диапазоны версий

Для Webpack plugin

{
  "peerDependencies": {
    "webpack": "^5.0.0"
  }
}

Для React library

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

Слишком узкие версии

Плохой пример:

{
  "peerDependencies": {
    "react": "18.2.0"
  }
}

Это вызывает ненужные конфликты.


Слишком широкие версии

Тоже опасно:

{
  "peerDependencies": {
    "webpack": ">=1"
  }
}

Webpack 1 и Webpack 5 несовместимы.


Peer dependencies для UI-kit

Типичная конфигурация:

{
  "peerDependencies": {
    "react": "^18",
    "react-dom": "^18"
  }
}

Дополнительно:

{
  "devDependencies": {
    "react": "^18",
    "react-dom": "^18"
  }
}

Peer dependencies для Vue library

{
  "peerDependencies": {
    "vue": "^3.0.0"
  }
}

Peer dependencies для ESLint plugins

ESLint ecosystem активно использует peerDependencies.

{
  "peerDependencies": {
    "eslint": "^9.0.0"
  }
}

Peer dependencies для Babel plugins

{
  "peerDependencies": {
    "@babel/core": "^7.0.0"
  }
}

Webpack library mode и peerDependencies

При сборке библиотеки особенно важно:

  • не включать peer dependencies в bundle;
  • выносить их в externals;
  • проверять итоговый output.

Автоматизация externals

Популярная практика:

const pkg = require('./package.json');

module.exports = {
  externals: Object.keys(pkg.peerDependencies || {})
};

Webpack автоматически исключает peer dependencies из bundle.


Rollup и peerDependencies

Хотя Rollup работает иначе, концепция аналогична.

Часто используется:

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

Peer dependencies и external обычно синхронизируются.


Vite library mode

В Vite также важно исключать peer dependencies:

build: {
  rollupOptions: {
    external: ['react']
  }
}

Проверка итогового bundle

Полезные инструменты:

  • webpack-bundle-analyzer;
  • source-map-explorer;
  • rollup-plugin-visualizer.

Они помогают обнаруживать:

  • случайно встроенный React;
  • дубликаты пакетов;
  • oversized bundle.

Частые ошибки

React в dependencies

Самая распространенная проблема.


Отсутствие externals

Peer dependency объявлена, но попала в bundle.


Слишком строгие версии

Вызывают конфликты установки.


Слишком широкие версии

Приводят к runtime-несовместимости.


Несогласованные peer dependencies

Например:

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

Но библиотека фактически использует API React 19.


Проверка peer dependencies локально

Полезно тестировать библиотеку:

npm pack

Затем:

npm install ../my-library-1.0.0.tgz

Это помогает выявлять:

  • отсутствующие peer dependencies;
  • дублирование;
  • ошибки externals;
  • проблемы resolution.

Peer dependencies и sideEffects

Хотя прямой связи нет, библиотеки часто одновременно настраивают:

{
  "sideEffects": false
}

и корректные peer dependencies.

Это улучшает:

  • tree shaking;
  • оптимизацию bundle;
  • deduplication.

Стратегия разработки библиотек

Наиболее распространенная схема:

dependencies

Только реальные внутренние runtime-зависимости.


peerDependencies

Все framework/runtime singleton-зависимости:

  • react;
  • vue;
  • angular;
  • webpack;
  • eslint;
  • babel core.

devDependencies

Инструменты сборки и тестирования.


Практическая схема React-библиотеки

package.json

{
  "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"
  }
}

webpack.config.js

module.exports = {
  mode: 'production',

  externals: {
    react: 'react',
    'react-dom': 'react-dom'
  },

  output: {
    library: {
      type: 'module'
    }
  },

  experiments: {
    outputModule: true
  }
};

Результат корректной настройки

Правильная обработка peer dependencies обеспечивает:

  • отсутствие дублирования библиотек;
  • меньший размер bundle;
  • корректную работу singleton runtime;
  • совместимость React hooks;
  • стабильную работу context API;
  • корректный Module Federation;
  • предсказуемый dependency graph;
  • более эффективный tree shaking;
  • уменьшение конфликтов версий;
  • корректный package resolution в npm/pnpm/Yarn.