Skip to content

Что такое peerDependencies

Nikita Elfimov edited this page May 1, 2022 · 9 revisions

Дисклеймер

Чтобы понять что тут написано настоятельно рекомендуется ознакомиться (если не знакомы) с:

Какую проблему они решают?

Абстрактное определение peerDependencies которое можно встретить на просторах интернета никак не поможет понять что это такое и зачем оно надо, так что начать с ними знакомиться следует с проблемы, которую они решают

Реальный пример использования

У нас есть 3 воркспейса: @dashboard/app, @dashboard/index, @dashboard/history

У нас есть пакет react-intl. Из этого пакета нас интересует функционал основанный на работе реакт контекстов:

  • Есть IntlProvider (Context.Provider)
  • Есть хук useIntl (useContext)

Наш проект работает следующим образом:

@dashboard/app "собирает" остальные 2 фрагмента воедино. Так как оба из них используют react-intl и им необходим одинаковый объект intl, то IntlProvider инициализируется именно там. Т.е. абстрактно это выглядит как-то так:

@dashboard/app:

<IntlProvider>
   {route === '/' && <IndexPage />}
   {route === '/history' && <HistoryPage />}
</IntlProvider>

@dashboard/index:

const IndexPage = () => {
   const intl = useIntl()

   return (
      <Text>
         {intl.formatMessage(messages.indexPage)}
      </Text>
   )
}

@dashboard/history:

const HistoryPage = () => {
   const intl = useIntl()

   return (
      <Text>
         {intl.formatMessage(messages.historyPage)}
      </Text>
   )
}

Что может пойти не так?

Сначала обратим внимание на то, как выглядят package.json у всех воркспейсов:

@dashboard/app:

{
  "name": "@dashboard/app",
  "version": "0.0.0",
  "license": "BSD-3-Clause",
  "main": "src/index.ts",
  "dependencies": {
    "react-intl": "5.20.10"
  }
}

@dashboard/index:

{
  "name": "@dashboard/index",
  "version": "0.0.0",
  "license": "BSD-3-Clause",
  "main": "src/index.ts",
  "dependencies": {
    "react-intl": "5.20.10"
  }
}

@dashboard/history:

{
  "name": "@dashboard/history",
  "version": "0.0.0",
  "license": "BSD-3-Clause",
  "main": "src/index.ts",
  "dependencies": {
    "react-intl": "5.20.10"
  }
}

В общем, идентичные. А теперь попробуем визуализировать древо зависимостей для такого проекта:

@dashboard/app
|_ react-intl

@dashboard/index
|_ react-intl

@dashboard/history
|_ react-intl

Пояснение: у каждого воркспейса своя копия пакета react-intl. Чем это чревато:

  • У каждого воркспейса будет свой собственный "мир", в котором существует свой IntlProvider и не существует IntlProvider который мы уже инициализировали в @dashboard/app
  • useIntl будет знать о существовании только своего IntlProvider, который, как известно, нигде не используется

А если поконкретнее и с логами то такой косяк выглядит примерно так:

Invariant Violation: [React Intl] Could not find required intl object. <IntlProvider> needs to exist in the component ancestry

Если упрощать до одного предложения: useIntl стучится в IntlProvider которого не существует. Но почему? Потому что он не знает о существовании IntlProvider в @dashboard/app

Наконец, решение - peerDependencies

Получается наша проблема свелась к тому, что нам нужно сообщить воркспейсам @dashboard/index, @dashboard/history о том, что существует react-intl в @dashboard/app, и useIntl нужно брать именно оттуда, чтобы получать тот самый объект intl, а не создавать свой

Для этого воспользуемся peerDependencies, и пекеджи у воркспейсов выглядят теперь так:

@dashboard/app:

{
  "name": "@dashboard/app",
  "version": "0.0.0",
  "license": "BSD-3-Clause",
  "main": "src/index.ts",
  "dependencies": {
    "react-intl": "5.20.10"
  }
}

@dashboard/index:

{
  "name": "@dashboard/index",
  "version": "0.0.0",
  "license": "BSD-3-Clause",
  "main": "src/index.ts",
  "peerDependencies": {
    "react-intl": "*"
  }
}

@dashboard/history:

{
  "name": "@dashboard/history",
  "version": "0.0.0",
  "license": "BSD-3-Clause",
  "main": "src/index.ts",
  "peerDependencies": {
    "react-intl": "*"
  }
}

Теперь дерево по факту выглядит так:

@dashboard/app
|_ react-intl

@dashboard/index

@dashboard/history

Да, react-intl просто отсутствует как "копия" в index и history, а peerDependencies говорит, что нужно использовать родительскую копию react-intl (в нашем случае это тот, что в @dashboard/app)

И...все, в целом это вся суть peerDependencies.

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

Еще их можно использовать как регламент для вашей либы. То есть если ваша либа совместима с реактом версии 16, в peerDependencies это можно указать так:

"peerDependencies" :
   "react": "16"

И теперь при попытке установить вашу либу в проект где используется 14 версия реакта будет ошибка

Для тех, кто все еще ничего не понял

Давайте представим что у нас есть пакет @fragments/navigation со следующим списком зависимостей:

Screenshot from 2022-05-01 15-03-13

Как можно заметить в пакете фигурируют только обычные dependencies. В связи с этим пакеты установятся следующим образом:

Screenshot from 2022-05-01 15-05-28

То есть внутри нашего фрагмента появится по одному новому экземпляру каждой либы. Теперь ближе к делу: наш фрагмент нужно отрендерить, для этого мы будем использовать условный renderer-entrypoint, и теперь зависимости в нашем проекте будут выглядеть следующим образом:

Screenshot from 2022-05-01 15-09-26

То есть теперь в каждом из присутствующих воркспейсов будут свои экземпляры либ. А теперь переходим к самому интересному: попробуем воспользоваться библиотекой react-intl. Для этого мы загрузим все локали (переводы текста на другие языки, если проще) в IntlProvider внутри @site/renderer-entrypoint.

Почему мы делаем это в рендерер энтрипоинте? - Потому что мы хотим шейрить логику работы с локалями по всему проекту. Если по всему проекту будет один IntlProvider - мы можем быть уверены, что во всех фрагментах логика смены локали будет одна, и не будет инцидентов по типу "в одном фрагменте язык поменялся, а в другом - нет".

Теперь, в нашем фрагменте нам нужно взять локали из IntlProvider который был инициализирован в рендерер энтрипонте, на схеме это выглядит примерно так:

Screenshot from 2022-05-01 15-17-32

Но, думаю уже почувствовали подвох: так работать не будет. Все произойдет так:

Screenshot from 2022-05-01 15-23-40

То есть наш фрагмент будет обращаться к экземпляру react-intl в котором нету провайдера и который не знает о наших локалях, которые мы описывали в рендерер энтрипоинте

Попробуем peerDependencies:

Давайте теперь добавим пиры и посмотрим что произойдет:

Screenshot from 2022-05-01 15-27-48

Таким образом мы сказали фрагменту о том, чтобы он взял react-intl от своего родителя, а не устанавливал свой.

Примечание: так как либа не будет физически присутствовать в нашем воркспейсе все референсы на нее будут не валидны:

import { useIntl } from 'react-intl'

Этот код даст нам TypeError, который скажет что либы react-intl не существует. Чтобы этого избежать мы добавим react-intl как дев зависимость (devDependencies), таким образом в бандле наш фрагмент будет по-прежнему ссылаться на экземпляр либы родителя, но при этом сможет ссылаться на ее копию установленную у себя же, но только для типизации:

Screenshot from 2022-05-01 15-33-39