Peerdependencies npm что это
Перейти к содержимому

Peerdependencies npm что это

Peer Dependencies

npm is awesome as a package manager. In particular, it handles sub-dependencies very well: if my package depends on request version 2 and some-other-library , but some-other-library depends on request version 1, the resulting dependency graph looks like:

This is, generally, great: now some-other-library has its own copy of request v1 that it can use, while not interfering with my package's v2 copy. Everyone's code works!

The Problem: Plugins

There's one use case where this falls down, however: plugins. A plugin package is meant to be used with another "host" package, even though it does not always directly use the host package. There are many examples of this pattern in the Node.js package ecosystem already:

  • Grunt plugins
  • Chai plugins
  • LevelUP plugins
  • Express middleware
  • Winston transports

Even if you're not familiar with any of those use cases, surely you recall "jQuery plugins" from back when you were a client-side developer: little <script> s you would drop into your page that would attach things to jQuery.prototype for your later convenience.

In essence, plugins are designed to be used with host packages. But more importantly, they're designed to be used with particular versions of host packages. For example, versions 1.x and 2.x of my chai-as-promised plugin work with chai version 0.5, whereas versions 3.x work with chai 1.x. Or, in the faster-paced and less-semver–friendly world of Grunt plugins, version 0.3.1 of grunt-contrib-stylus works with grunt 0.4.0rc4, but breaks when used with grunt 0.4.0rc5 due to removed APIs.

As a package manager, a large part of npm's job when installing your dependencies is managing their versions. But its usual model, with a "dependencies" hash in package.json , clearly falls down for plugins. Most plugins never actually depend on their host package, i.e. grunt plugins never do require("grunt") , so even if plugins did put down their host package as a dependency, the downloaded copy would never be used. So we'd be back to square one, with your application possibly plugging in the plugin to a host package that it's incompatible with.

Even for plugins that do have such direct dependencies, probably due to the host package supplying utility APIs, specifying the dependency in the plugin's package.json would result in a dependency tree with multiple copies of the host package—not what you want. For example, let's pretend that winston-mail 0.2.3 specified "winston": "0.5.x" in its "dependencies" hash, since that's the latest version it was tested against. As an app developer, you want the latest and greatest stuff, so you look up the latest versions of winston and of winston-mail , putting them in your package.json as

But now, running npm install results in the unexpected dependency graph of

I'll leave the subtle failures that come from the plugin using a different Winston API than the main application to your imagination.

The Solution: Peer Dependencies

What we need is a way of expressing these "dependencies" between plugins and their host package. Some way of saying, "I only work when plugged in to version 1.2.x of my host package, so if you install me, be sure that it's alongside a compatible host." We call this relationship a peer dependency.

The peer dependency idea has been kicked around for literally years. After volunteering to get this done "over the weekend" nine months ago, I finally found a free weekend, and now peer dependencies are in npm!

Specifically, they were introduced in a rudimentary form in npm 1.2.0, and refined over the next few releases into something I'm actually happy with. Today Isaac packaged up npm 1.2.10 into Node.js 0.8.19, so if you've installed the latest version of Node, you should be ready to use peer dependencies!

As proof, I present you the results of trying to install jitsu 0.11.6 with npm 1.2.10:

As you can see, jitsu depends on two Flatiron-related packages, which themselves peer-depend on conflicting versions of Flatiron. Good thing npm was around to help us figure out this conflict, so it could be fixed in version 0.11.7!

Using Peer Dependencies

Peer dependencies are pretty simple to use. When writing a plugin, figure out what version of the host package you peer-depend on, and add it to your package.json :

Now, when installing chai-as-promised , the chai package will come along with it. And if later you try to install another Chai plugin that only works with 0.x versions of Chai, you'll get an error. Nice!

One piece of advice: peer dependency requirements, unlike those for regular dependencies, should be lenient. You should not lock your peer dependencies down to specific patch versions. It would be really annoying if one Chai plugin peer-depended on Chai 1.4.1, while another depended on Chai 1.5.0, simply because the authors were lazy and didn't spend the time figuring out the actual minimum version of Chai they are compatible with.

The best way to determine what your peer dependency requirements should be is to actually follow semver. Assume that only changes in the host package's major version will break your plugin. Thus, if you've worked with every 1.x version of the host package, use "

1.0" or "1.x" to express this. If you depend on features introduced in 1.5.2, use ">= 1.5.2 < 2" .

Now go forth, and peer depend!

© OpenJS Foundation. All Rights Reserved. Portions of this site originally © Joyent.

Управление зависимостями JavaScript

Управление зависимостями JavaScript

Всем привет! Меня зовут Слава Фомин, я ведущий разработчик в компании DomClick. За свою 16-ти летнюю практику я в первых рядах наблюдал за становлением и развитием JavaScript как стандарта и экосистемы. В нашей компании мы используем JavaScript, в первую очередь, для продвинутой front-end разработки и успели перепробовать достаточно большое количество различных технологий, инструментов и подходов, набить много шишек. Результатом этого кропотливого труда стал ценнейший опыт, которым я и хочу поделиться с вами.

Изначально, я хотел рассказать о том как мы работаем над сотнями пакетов с использованием монорепозиториев, но в результате пришел к выводу, что для углубления в эту довольно сложную тему стоило бы сначала объяснить более простые концепции. В итоге решил оформить весь материал в виде блога с небольшими постами, в которых буду подробнее раскрывать отдельные темы, плавно водя читателя от простых вопросов к сложным. Надеюсь, это сделает материал полезным не только состоявшимся профессионалам, но и новичкам.

Вот список тем, которые я планирую затронуть в этом блоге:

  • Что такое пакет, манифест пакета и зависимости.
  • Как правильно описывать зависимости для различных типов проектов.
  • Как работает semver и как правильно использовать диапазоны версий в манифесте проекта.
  • Как установленные зависимости могут быть представлены в файловой системе, плюсы и минусы разных решений.
  • Как работает поиск зависимостей (resolving).
  • Какие существуют инструменты для работы с зависимостями.
  • Как правильно обновлять зависимости.
  • Как следить за безопасностью, отслеживать и предупреждать угрозы.
  • Для чего нужны lock-файлы и как правильно ими пользоваться.
  • Как можно эффективно работать над сотнями пакетов одновременно, используя монорепозитории и специальные инструменты.
  • Что такое фантомные пакеты, откуда берется проблема дублирующихся пакетов и как с этим можно бороться.
  • Как эффективно и безопасно использовать менеджер пакетов в контексте CI/CD.
  • и многое другое.

Итак, не будем терять времени!

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

Несмотря на огромный прогресс в фундаментальных веб-технологиях, создание даже мало-мальски сложного приложения потребовало бы достаточно высокой квалификации разработчиков и написания огромного количества базового кода с нуля. Но, слава богу, существуют такие решения, как Angular, React, Express, Lodash, Webpack и многие другие, и нам не нужно каждый раз изобретать колесо.

Немного истории JavaScript

Мы можем вспомнить «дикие времена», когда код популярных библиотек (таких как jQuery) и плагины к ним разработчику нужно было напрямую скачивать с официального сайта, а затем распаковывать из архивов в директорию проекта. Разумеется, обновление таких библиотек происходило точно так же: вручную. Сборка такого приложения тоже требовала ручного и достаточно творческого, уникального подхода. Про оптимизацию сборки я даже не стану упоминать.

К огромному счастью, которое более молодые разработчики вряд ли смогут сейчас испытать, эти времена ручного управления зависимостями в прошлом. Сейчас мы имеем хоть и не самые совершенные, но вполне рабочие инструменты, которые позволяют взять под контроль управление зависимостями даже в довольно сложных проектах.

Node.js приходит на помощь

Современную веб-разработку уже совершенно невозможно представить без Node.js, технологии, которая изначально разрабатывалась для сервера, но впоследствии стала платформой для любых JavaScript-проектов, как front-end приложений, так и всевозможных инструментов, а с популяризацией SSR граница между средами начала окончательно стираться. Таким образом, менеджер пакетов для Node.js (Node Package Manager, или npm), постепенно стал универсальным менеджером пакетов для всех библиотек и инструментов, написанных на JavaScript.

Также стоит заметить, что до появления ESM стандарт языка JavaScript в принципе не имел концепции модулей и зависимостей: весь код просто загружался через тег script в браузере и выполнялся в одной большой глобальной области видимости. По этой причине разработчики Node внедрили собственный формат модулей. Он был основан на неофициальном стандарте CommonJS (от слов «распространенный/универсальный JavaScript», или CJS), который впоследствии стал де-факто стандартом в индустрии. Сам же алгоритм поиска зависимостей Node (Node.js module resolution algorithm) стал стандартом представления пакетов в файловой системе проекта, который сейчас используется всеми загрузчиками и инструментами сборки.

Пакет всему голова

Как было упомянуто выше, Node.js ввел свой формат представления и поиска зависимостей, который сейчас де-факто является общим стандартом для JavaScript-проектов.

В основе системы лежит концепция пакета: npm-пакет — это минимальная единица распространения кода на JavaScript. Любая библиотека или фреймворк представляются как один или несколько связанных пакетов. Ваше приложение также является пакетом.

Перед публикацией пакет, как правило, компилируется, а потом загружается в хранилище, которое называется npm registry. В основном используется централизованный официальный npm registry, который находится в публичном доступе на домене registry.npmjs.org. Однако использование частных закрытых npm registry также распространено (мы в ДомКлике активно используем такой для внутренних пакетов). Другие разработчики могут установить опубликованный пакет как зависимость в свой проект, загрузив его из registry. Это происходит автоматически при помощи менеджера пакетов (вроде npm).

Найти нужный пакет или изучить их список можно на официальном сайте npm.

Опубликованный пакет физически представляет собой версионированный архив с кодом, который готов к выполнению или внедрению в другой проект. Этот код может представлять собой библиотеку, которую вы можете включить в свое приложение (например, lodash), либо полноценную программу, которую можно напрямую вызвать у вас на компьютере (например, webpack).

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

На изображении показан результат команды npm ls — дерево зависимостей проекта, в котором установлено всего два пакета: HTTP-сервер Express (с множеством дочерних зависимостей) и библиотека Lodash (без зависимостей). Обратите внимание, что одна и та же зависимость debug встречается 4 раза в разных частях дерева. Надпись deduped означает, что npm обнаружил дублирующиеся зависимости и установил пакет только один раз (подробнее о дубликации мы поговорим в следующих постах).

Поскольку экосистема Node проповедует философию Unix, когда один пакет должен решать какую-то свою узкую задачу и делать это хорошо, то количество зависимостей в среднестатистическом проекте может быть очень велико и легко переваливает за несколько сотен. Это приводит к тому, что дерево зависимостей сильно разрастается как в ширину, так и в глубину. Наверное, только ленивый не шутил про размеры директории node_modules , в которой устанавливаются все эти зависимости. Нередко, люди со стороны критикуют JavaScript за это:

Манифест пакета

Что же является пакетом и как мы можем его создать? По сути, пакетом может являться любая директория, содержащая специальный файл-манифест: package.json. Он может содержать множество полезной информации о пакете, такой как:

  • название, версия и описание,
  • тип лицензии,
  • URL домашней страницы, URL git-репозитория, URL страницы для баг-репортинга,
  • имена и контакты авторов и мейнтейнеров,
  • ключевые слова, чтобы пакет можно было найти,
  • файловые пути к коду библиотеки или выполняемым файлам,
  • список зависимостей,
  • вспомогательные локальные команды (scripts) для работы над пакетом,
  • и др. (см. полный список).

Описание зависимостей пакета

Манифест пакета содержит ряд опциональных полей, которые позволяют задавать список зависимостей:

  • dependencies ,
  • devDependencies ,
  • peerDependencies ,
  • optionalDependencies .

Каждое из этих полей является JSON-объектом, где в качестве ключа указывается название пакета, а в качестве значения — диапазон версий, которые поддерживаются в вашем проекте.

Пример:

Давайте рассмотрим назначение каждого поля в отдельности.

dependencies

Поле dependencies определяет список зависимостей, без которых код вашего проекта не сможет корректно работать. Это главный и основной список зависимостей для библиотек и программ на Node.js. Если в вашем коде есть импорты каких-то сторонних зависимостей, например import < get >from ‘lodash’ , то эта зависимость должна быть прописана в поле dependencies . Ее отсутствие приведет к тому, что при выполнении ваша программа упадет с ошибкой, потому что нужная зависимость не будет найдена.

devDependencies

Поле devDependencies позволяет задать список зависимостей, которые необходимы только на этапе разработки пакета, но не для выполнения его кода в рантайме. Сюда можно отнести всевозможные инструменты разработки и сборки, такие как typescript, webpack, eslint и прочие. Если ваш пакет будет устанавливаться как зависимость для другого пакета, то зависимости из этого списка установлены не будут.

peerDependencies

Поле peerDependencies играет особую роль при разработке вспомогательных пакетов для некоторых инструментов и фреймворков. К примеру, если вы пишете плагин для Webpack, то в поле peerDependencies вы можете указать версию webpack, которую ваш плагин поддерживает.

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

При установке вашего пакета зависимости из поля peerDependencies автоматически не устанавливаются, разработчик должен сам установить их в свой родительский проект, прописав в манифесте своего пакета. Это также гарантирует, что в проекте будет использоваться только одна версия зависимости без дубликатов. Ведь если в проекте будет установлено несколько разных версий, скажем, Webpack, то это может привести к серьезным конфликтам.

optionalDependencies

И последнее поле optionalDependencies позволяет вам указать некритические зависимости, без которых ваше приложение сможет всё равно выполнять свою работу. Если менеджер пакетов не сможет найти или установить такую зависимость, то он просто проигнорирует её и не вызовет ошибки.

Важно понимать, что ваш код должен корректно реагировать на отсутствие таких зависимостей, например, используя связку try… require… catch .

Зависимости во front-end проектах

Выше мы рассмотрели четыре способа задания различных зависимостей для вашего проекта. Однако не стоит забывать, что эта система была изначально придумана для приложений и библиотек на Node.js, которые выполняются напрямую на машине пользователя, а не в особой песочнице, коей является браузер. Таким образом, стандарт никак не учитывает особенности разработки front-end приложений.

Если кто-то вам скажет, что один способ определения npm-зависимостей для front-end приложений является правильным, а другой нет, то не верьте: «правильного» способа не существует, потому что такой вариант использования просто не учтен в node и npm.

Однако для удобства работы над front-end приложениями я могу предложить вам проверенный временем и опытом формат определения зависимостей. Но для начала давайте попробуем разобраться, чем отличается front-end проект от проекта на Node.js.

Обычно конечная цель Node.js-разработчика заключается в том, чтобы опубликовать созданный им пакет в npm registry, а уже оттуда этот пакет скачивается, устанавливается и используется пользователем как готовое ПО или как библиотека в составе более сложного продукта. При этом зависимости из поля dependencies в манифесте пакета устанавливаются в проект конечного пользователя.

В случае же с front-end приложением оно не публикуется в npm registry, а собирается как самостоятельный артефакт (статика) и выгружается, например, на CDN. По-сути, npm во front-end проектах используется только для того, чтобы устанавливать сторонние зависимости. По этой причине в манифесте подобного проекта рекомендуется использовать опцию private: true , которая гарантирует, что файлы приложения не будут случайно отправлены в публичный npm-registry. Название же и версия самого пакета приложения не имеют смысла, т. к. «снаружи» нигде не используются.

Эта особенность front-end приложений позволяет нам использовать поле dependencies не совсем по его прямому назначению, а как категорию для того, чтобы разделить список зависимостей на две части: в поле dependencies вы пишете список прямых зависимостей, которые используются в коде приложения, например, lodash , react , date-fns и т. д., а в поле devDependencies — зависимости, которые нужны для разработки и сборки приложения: webpack , eslint , декларации из пакетов @types и т. д.

Постойте, но это ведь ничем не отличается от того, как прописываются зависимости для пакетов на Node.js! Да, однако некоторые особо педантичные разработчики могут заявить, что раз сторонние зависимости объединяются в бандл приложения при сборке и фактически не импортируются в рантайме, то они должны находиться в поле devDependencies . Теперь вы можете аргументировано защитить более практичный подход.

Семантическое версионирование

В экосистеме npm принят стандарт версионирования пакетов semver (от слов Semantic Versioning (семантическое версионирование)).

Суть стандарта заключается в том, что версия пакета состоит из трех чисел: основной (major) версии, младшей (minor) версии и patch-версии:

Например: 3.12.1.

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

Увеличение patch-версии означает, что в пакет были внесены незначительные исправления или улучшения, которые не добавляют новой функциональности и не нарушают обратную совместимость.

Увеличение minor-версии означает, что в пакет была добавлена новая функциональность, но совместимость сохранилась.

Увеличение же major-версии означает, что в пакет были внесены серьезные изменения API, которые привели к потере обратной совместимости и пользователю пакета, возможно, необходимо внести изменения в свой код, чтобы перейти на новую версию. О таких изменениях и порядке миграции на новую версию обычно можно прочитать в файле CHANGELOG в корне пакета.

Нестабильные версии

Версии пакетов до 1.0.0, например, 0.0.3 или 0.1.2, в системе semver также имеют определенный смысл: такие версии считаются нестабильными и повышение первого ненулевого числа версии должно расцениваться как изменение с потенциальным нарушением обратной совместимости.

Продолжение следует

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

npm Peer Dependencies

Understanding when and why to use npm peerDependencies

In this article I hope to clarify what npm Peer Dependencies are and especially when you should use them. Peer Dependencies are listed in the package.json file in the peerDependencies object.

To get the most out of this article you should have at least an introductory understanding of npm.

Contents

In this article:

  1. We will compare exactly how Dependencies work versus Peer Dependencies.
  2. We will look at some examples of both Dependencies and Peer Dependencies.
  3. Then, we will examine how npm handles version conflicts.
  4. Finally, having the fundamentals solidly in our grasp, we will lay out an approach to deciding when Peer Dependencies are appropriate.

The Scenario

To keep it real, let’s assume you’re creating an Angular Library or even just a simple JavaScript file that exports some functions.

Your project relies on packages from the npm Registry. These packages are your project’s dependencies.

You want to create your own npm package from your project. So you use npm pack to generate an npm package from your project. You might even decide to publish it to the npm Registry.

Other teams will add your package as a dependency in their own projects. We use Dependencies and Peer Dependencies in package.json to tell these other projects what packages also need to be added for our package to work.

So, at their most basic level here is how Dependencies and Peer Dependencies work:

Dependencies

Dependencies are listed in the package.json file in a dependencies object.

When you add a package in dependencies , you are saying:

  • My code needs this package to run.
  • If this package doesn’t already exist in my node_modules directory, then add it automatically.
  • Furthermore, add the packages that are listed in the package’s dependencies. These packages are called transitive dependencies.

Peer Dependencies

Peer Dependencies are listed in the package.json file in a peerDependencies object.

By adding a package in peerDependencies you are saying:

  • My code is compatible with this version of the package.
  • If this package already exists in node_modules, do nothing.
  • If this package doesn’t already exist in the node_modules directory or it is the wrong version, don’t add it. But, show a warning to the user that it wasn’t found.

Adding Dependencies

So, we add dependencies in the package.json file of our npm package folder. Let’s look at exactly how we add packages as dependencies and some examples of package dependencies.

Adding a Dependency

A Dependency is an npm package that our package depends on in order to be able to run. Some popular packages that are typically added as dependencies are lodash, request, and moment.

We add a regular dependency like this:

npm adds the package name and version to the dependencies object in our project’s package.json file.

Some of you might remember the old days when we had to use the —save flag to get npm to update the dependencies in package.json. Thankfully, we don’t need to do that anymore.

Adding a Peer Dependency

Peer Dependencies are used to specify that our package is compatible with a specific version of an npm package. Good examples are Angular and React.

To add a Peer Dependency you actually need to manually modify your package.json file. For example, for Angular component library projects, I recommend adding angular/core as a peer dependency. So if you wanted to specify that your package is built for Angular 7, you could include something like this:

About Conflicts

I get a lot of questions about whether a certain npm package should go into dependencies or into peerDependencies . The key to making this decision involves understanding how npm deals with version conflicts.

If you have read my previous articles, you know I like you to be able to do this stuff along with me! So feel free to work along with me for this little npm experiment.

conflict-test Project

To get started let’s create a trivial test project. I am going to name mine:
conflict-test

I created it like this:

I then manually edited the package.json file and added two dependencies:

These todd-a and todd-b packages also have their own dependencies:

todd-a

todd-b

The thing I want you to notice here is that todd-a and todd-b use the same version of lodash . But, they have a version conflict for todd-child :
todd-a uses todd-child version 1.0.0
todd-b uses todd-child version 2.0.0

Now I know that, like me, you are keenly interested to see how npm handles this version conflict. In my main project conflict-test I run npm install . As we would expect, npm magically installs the todd-a and todd-b packages in our node_modules folder. It also adds the packages that they depend on (the transitive dependencies). So after running npm install we take a look at the node_modules folder. It looks like this:

The interesting thing about this is that our project has one copy of lodash . But, it has two copies of todd-child . Notice that todd-b gets its own private copy of todd-child 2.0.0 .

So here is the rule:

npm deals with version conflicts by adding duplicate private versions of the conflicted package.

An Approach to Peer Dependencies

As we saw from our experiment with npm version conflicts, if you add a package to your dependencies , there is a chance it may end up being duplicated in node_modules.

Sometimes, having two versions of the same package is fine. However, some packages will cause conflicts when there are two different versions of them in the same code base.

For example, assume our component library was created using Angular 5. We wouldn’t want our package adding another completely different version of angular/core when someone adds it as a dependency to their Angular 6 application.

The key is:
We don’t want our library adding another version of a package to node-modules when that package could conflict with an existing version and cause problems.

peerDependencies or dependencies?

So this brings us to the main question for our dependencies:

When my package depends on another package, should I put it in dependencies or peerDependencies?

Well, as with most technical questions: it depends.

Peer Dependencies express compatibility. For example, you will want to be specific about which version of Angular your library is compatible with.

The Guidelines

Favor using Peer Dependencies when one of the following is true:

  • Having multiple copies of a package would cause conflicts
  • The dependency is visible in your interface
  • You want the developer to decide which version to install

Let’s take the example of angular/core . Obviously, if you are creating an Angular Library, angular/core is going to be a very visible part of your library’s interface. Hence, it belongs in your peerDependencies .

However, maybe your library uses Moment.js internally to process some time related inputs. Moment.js most likely won’t be exposed in the interface of your Angular Services or Components. Hence, it belongs in your dependencies .

Angular as a Dependency

Given that you are going to specify in your documentation that your library is a set of Angular Components and Services, you may be asking the question:

“Do I even need to specify angular/core as a dependency? If someone is using my library, they will already have an existing Angular project.”

Yes, we can usually assume that for our Angular specific library the Workspace will already have the Angular packages available. Hence, technically we wouldn’t need to bother adding them to our list of dependencies.

However, we really do want to tell the developer which Angular versions our library is compatible with. So I recommend the following approach:

Add at least angular/core for the compatible Angular version to your peerDependencies .

This way developers will see a warning if they try to use your Angular 7 library in their Angular 6 project. Don’t bother adding the other Angular packages. You can assume if they have angular/core, they have the other Angular libraries.

In Conclusion

When in doubt you should probably lean toward using peerDependencies . This lets the users of your package make their own choice about which packages to add.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *