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:
- We will compare exactly how Dependencies work versus Peer Dependencies.
- We will look at some examples of both Dependencies and Peer Dependencies.
- Then, we will examine how npm handles version conflicts.
- 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.
npm Peer Dependencies
We’ll assume that you have a basic working knowledge of npm . However, quite often people struggle with the different types of dependencies and, in particular Peer Dependencies. If so, this post will improve your understanding. There are actually five different dependency types defined by npm:
- normal dependencies
- dev dependencies
- peer dependencies
- optional dependencies
- bundled dependencies
Of these, normal and dev dependencies are generally well understood and the use cases for optional and bundled dependencies are few and far between, which brings us nicely to the subject of this post, peer dependencies.
We’ll describe what they are and discuss when it might be appropriate to use them.
Dependencies vs. Peer Dependencies
If you’ve used nodejs at all, you will have come across normal dependencies in package.json. They are used to describe the modules that your application depends on. They will always be included in your built module. They look a bit like this:
And, you’ve probably also seen dev dependencies, which are used to describe dependencies that are used as part of your development process but not needed in you product build. They are installed in your dev environment by npm install but they are not included in your built module.Test and build tools are commonly in the dev dependencies, for example:
Peer dependencies effectively declare a dependency without including the dependency in your built module. When an application includes your module, that application will in turn need to include the declared dependency. With npm version 4 through to 6, a warning is issued when you run npm install to remind you to install the peer dependencies. Prior to version 4, npm automatically included peer dependencies if they weren’t explicitly included. That behaviour led to too much complexity in dependency tree calculation and it was dropped in version 4. With npm version 7, a brand new dependency tree manager is being introduced. With this, automatic inclusion of peer dependencies is returning. You can read about the Arborist dependency tree manager here.
When to Use Peer Dependencies
Sometimes, and particularly when you are building a library that will be used by other applications, you will have a dependency that will almost certainly also be a core dependency of those other applications. For example, if you are building a library of React components, React will be a dependency you need, but almost certainly the application that uses your library will need React. This is where we use Peer Dependencies.
Peer dependencies provide the details of what the host application is expected to provide. Taking our React example, our peer dependencies might look like this:
Semantic Versioning
When specifying the allowed versions of a package in a peer dependency in our package.json , we often want to specify more liberal version ranges than we typically use for normal or dev dependencies. As a brief recap, semantic versioning or semver is used in package.json to specify the versions of a dependency that are compatible with the package described by the package.json. Semantic versioning generally defines the version of a package using three digits, major.minor.patch .
Most normal or dev dependencies use one of two specifiers: — tilde (
) to allow newer patch level versions of a package — caret (^) to allow newer minor level versions of a package
Caret is the default when we use npm install or npm install —save-dev . The npm option —save-prefix can be used to change the specifier used.
To refresh your knowledge of npm’s semver usage, we’d recommend reading the official docs.
When it comes to peer dependencies, we are generally specifying the versions of a dependency that our package can work with, rather than the version we’d prefer to use. This sometimes leads to broader specifications than those provided by the tilde and caret specifiers. For example, we might know that our package can work with more than one major version of a dependency. Consider that we might be building a React Library that works for both React 16 versions greater than minor version 8 an also works with React 17 and is optimistically expected to work with future minor updates to React 17. Our specifier might look like this:
A useful tool for checking your specifiers is the semver calculator, available on the npm web site.
Unmet Dependencies When Testing
When you use peer dependencies, npm will not automatically install those dependencies (see comments above in respect to npm version 7). This can lead to errors when you are running tests on your package, although you will get warnings when executing npm to prompt you to install the peer dependencies. One way to avoid these warnings and errors is to also include the peer dependencies as dev dependencies. That way, they will be available for local testing but will still be peer dependencies in the published npm module.
If you’d like to keep up with new posts, please consider following us.
npm Peer Dependencies
В этой статье я надеюсь прояснить, что такое одноранговые зависимости npm и особенно, когда их следует использовать. Одноранговые зависимости перечислены в файле package.json в объекте peerDependencies .
Чтобы получить максимальную отдачу от этой статьи, вы должны иметь хотя бы вводное представление о npm.
СОДЕРЖАНИЕ
- Мы сравним, как именно работают зависимости и одноранговые зависимости.
- Мы рассмотрим несколько примеров как зависимостей, так и одноранговых зависимостей.
- Затем мы рассмотрим, как npm обрабатывает конфликты версий.
- Наконец, прочно усвоив основы, мы предложим подход к принятию решения о целесообразности одноранговых зависимостей.
Сценарий
Чтобы все было по-настоящему, предположим, что вы создаете библиотеку Angular или даже простой файл JavaScript, который экспортирует некоторые функции.
Ваш проект полагается на пакеты из npm Registry. Эти пакеты являются зависимостями вашего проекта.
Вы хотите создать собственный пакет npm из своего проекта. Итак, вы используете npm pack для создания пакета npm из вашего проекта. Вы даже можете решить опубликовать его в реестре npm.
Другие команды добавят ваш пакет как зависимость в свои собственные проекты. Мы используем зависимости и одноранговые зависимости в package.json, чтобы сообщить этим другим проектам, какие пакеты также необходимо добавить для работы нашего пакета.
Итак, на самом базовом уровне вот как работают зависимости и одноранговые зависимости:
Зависимости
Зависимости перечислены в файле package.json в объекте dependencies.
Когда вы добавляете пакет в dependencies , вы говорите:
- Моему коду нужен этот пакет для запуска.
- Если этого пакета еще нет в моем каталоге node_modules, добавьте его автоматически.
- Кроме того, добавьте пакеты, перечисленные в зависимостях пакета. Эти пакеты называются транзитивными зависимостями.
Одноранговые зависимости
Peer Dependencies перечислены в файле package.json в объекте peerDependencies.
Добавляя пакет в peerDependencies , вы говорите:
- Мой код совместим с этой версией пакета.
- Если этот пакет уже существует в node_modules, ничего не делайте.
- Если этого пакета еще нет в каталоге node_modules или это неправильная версия, не добавляйте его. Но покажите пользователю предупреждение о том, что он не найден.
Добавление зависимостей
Итак, мы добавляем зависимости в файл package.json нашей папки пакетов npm. Давайте посмотрим, как именно мы добавляем пакеты в качестве зависимостей, и некоторые примеры зависимостей пакетов.
Добавление зависимости
Зависимость — это пакет npm, от которого зависит возможность запуска нашего пакета. Некоторые популярные пакеты, которые обычно добавляются как зависимости, — это lodash, request и moment.
Мы добавляем такую обычную зависимость:
npm добавляет имя и версию пакета к объекту dependencies в файле package.json нашего проекта.
Некоторые из вас, возможно, помнят старые времена, когда нам приходилось использовать флаг —save , чтобы заставить npm обновлять dependencies в package.json. К счастью, нам больше не нужно этого делать.
Добавление одноранговой зависимости
Peer Dependencies используются, чтобы указать, что наш пакет совместим с определенной версией пакета npm. Хорошие примеры — Angular и React.
Чтобы добавить одноранговую зависимость, вам действительно нужно вручную изменить файл package.json. Например, для проектов библиотеки компонентов Angular я рекомендую добавить angular/core в качестве одноранговой зависимости. Итак, если вы хотите указать, что ваш пакет создан для Angular 7, вы можете включить что-то вроде этого:
О конфликтах
Я получаю много вопросов о том, должен ли определенный пакет npm входить в dependencies или в peerDependencies . Ключом к принятию этого решения является понимание того, как npm справляется с конфликтами версий.
Если вы читали мои предыдущие статьи, вы знаете, что мне нравится, что вы можете делать это вместе со мной! Так что не стесняйтесь работать вместе со мной в этом небольшом эксперименте с npm.
конфликт-тестовый проект
Для начала создадим простой тестовый проект. Назову свое:
conflict-test
Я создал это так:
Затем я вручную отредактировал файл package.json и добавил две зависимости:
Эти todd-a и todd-b пакеты также имеют свои собственные зависимости:
тодд-а
todd-b
Я хочу, чтобы вы заметили, что todd-a и todd-b используют одну и ту же версию lodash . Но у них есть конфликт версий для todd-child :
todd-a использует todd-child версию 1.0.0
todd-b использует todd-child версию 2.0.0
Теперь я знаю, что вам, как и мне, очень интересно посмотреть, как npm справляется с этим конфликтом версий. В моем основном проекте conflict-test я запускаю npm install . Как и следовало ожидать, npm волшебным образом устанавливает пакеты todd-a и todd-b в нашу папку node_modules. Он также добавляет пакеты, от которых они зависят (транзитивные зависимости). Итак, после запуска npm install мы смотрим на папку node_modules. Это выглядит так:
Интересно то, что в нашем проекте есть одна копия lodash . Но у него есть две копии todd-child . Обратите внимание, что todd-b получает свою собственную частную копию todd-child 2.0.0 .
Итак, вот правило:
npm разрешает конфликты версий путем добавления дубликатов частных версий конфликтующего пакета.
Подход к взаимозависимости
Как мы видели из нашего эксперимента с конфликтами версий npm, если вы добавите пакет в свой dependencies , есть вероятность, что он может в конечном итоге дублироваться в node_modules.
Иногда достаточно иметь две версии одного и того же пакета. Однако некоторые пакеты будут вызывать конфликты, если в одной и той же кодовой базе есть две разные версии.
Например, предположим, что наша библиотека компонентов была создана с использованием Angular 5. Мы бы не хотели, чтобы в наш пакет добавлялась еще одна совершенно другая версия angular/core , когда кто-то добавляет ее как зависимость к своему приложению Angular 6.
Ключ:
Мы не хотим, чтобы наша библиотека добавляла другую версию пакета в node-modules, если этот пакет может конфликтовать с существующей версией и вызывать проблемы.
peerDependencies или зависимости?
Итак, это подводит нас к основному вопросу о наших зависимостях:
Когда мой пакет зависит от другого пакета, следует ли мне помещать его в зависимости или peerDependencies?
Ну, как и с большинством технических вопросов: это зависит от обстоятельств.
Peer Dependencies выражают совместимость. Например, вы захотите уточнить, с какой версией Angular совместима ваша библиотека.
Руководящие принципы
Используйте Peer Dependencies, если верно одно из следующих условий:
- Наличие нескольких копий пакета вызовет конфликты
- Зависимость видна в вашем интерфейсе
- Вы хотите, чтобы разработчик решил, какую версию установить
Возьмем, к примеру, angular/core . Очевидно, что если вы создаете библиотеку Angular, angular/core будет очень заметной частью интерфейса вашей библиотеки. Следовательно, он принадлежит вашему peerDependencies .
Однако, возможно, ваша библиотека использует Moment.js внутри для обработки некоторых входных данных, связанных со временем. Moment.js, скорее всего, не будет отображаться в интерфейсе ваших Angular Services или компонентов. Следовательно, он принадлежит вашему dependencies .
Угловой как зависимость
Учитывая, что вы собираетесь указать в своей документации, что ваша библиотека представляет собой набор компонентов и служб Angular, вы можете задать вопрос:
«Нужно ли мне вообще указывать angular / core как зависимость? Если кто-то использует мою библиотеку, у него уже будет существующий проект Angular ».
Да, обычно мы можем предположить, что для нашей конкретной библиотеки Angular в Workspace уже будут доступны пакеты Angular. Следовательно, технически нам не нужно было бы беспокоиться о добавлении их в наш список зависимостей.
Однако мы действительно хотим сообщить разработчику, с какими версиями Angular совместима наша библиотека. Поэтому я рекомендую следующий подход:
Добавьте как минимум angular / core для совместимой версии Angular в свой peerDependencies .
Таким образом, разработчики увидят предупреждение, если попытаются использовать вашу библиотеку Angular 7 в своем проекте Angular 6. Не беспокойтесь о добавлении других пакетов Angular. Вы можете предположить, что если у них есть angular / core, у них есть другие библиотеки Angular.
В заключение
В случае сомнений вам, вероятно, следует склоняться к использованию peerDependencies . Это позволяет пользователям вашего пакета самостоятельно выбирать, какие пакеты добавить.
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!
UPDATE: npm versions 1, 2, and 7 will automatically install peerDependencies if they are not explicitly depended upon higher in the dependency tree. For npm versions 3 through 6, you will receive a warning that the peerDependency is not installed instead.
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" .