279

Why does, for example, a Grunt plugin define its dependency on grunt as "peer dependencies"?

Why can't the plugin just have Grunt as its own dependency in grunt-plug/node_modules?

Peer dependencies are described here: https://nodejs.org/en/blog/npm/peer-dependencies/

But I don't really get it.

Example

I'm working with AppGyver Steroids at the moment which uses Grunt tasks to build my source files into a /dist/ folder to be served on a local device. I'm quite new at npm and grunt so I want to fully comprehend what is going on.

So far I get this:

[rootfolder]/package.json tells npm it depends on the grunt-steroids npm package for development:

  "devDependencies": {
    "grunt-steroids": "0.x"
  },

Okay. Running npm install in [rootfolder] detects the dependency and installs grunt-steroids in [rootfolder]/node_modules/grunt-steroids.

Npm then reads [rootfolder]/node_modules/grunt-steroids/package.json so it can install grunt-steroids own dependencies.:

"devDependencies": {
    "grunt-contrib-nodeunit": "0.3.0",
    "grunt": "0.4.4"
  },
"dependencies": {
    "wrench": "1.5.4",
    "chalk": "0.3.0",
    "xml2js": "0.4.1",
    "lodash": "2.4.1"
  },
"peerDependencies": {
    "grunt": "0.4.4",
    "grunt-contrib-copy": "0.5.0",
    "grunt-contrib-clean": "0.5.0",
    "grunt-contrib-concat": "0.4.0",
    "grunt-contrib-coffee": "0.10.1",
    "grunt-contrib-sass": "0.7.3",
    "grunt-extend-config": "0.9.2"
  },

The "dependencies" packages are installed into [rootfolder]/node_modules/grunt-steroids/node_modules which is logical for me.

The "devDependencies" aren't installed, which I'm sure is controlled by npm detecting I'm just trying to use grunt-steroids, and not develop on it.

But then we have the "peerDependencies".

These are installed in [rootfolder]/node_modules, and I don't understand why there and not in [rootfolder]/node_modules/grunt-steroids/node_modules so that conflicts with other grunt plugins (or whatever) are avoided?

Sunil Garg
  • 14,608
  • 25
  • 132
  • 189
Thomas Stock
  • 10,927
  • 14
  • 62
  • 79

3 Answers3

528

TL;DR: peerDependencies are for dependencies that are exposed to (and expected to be used by) the consuming code, as opposed to "private" dependencies that are not exposed, and are only an implementation detail.

The problem peer dependencies solve

NPM's module system is hierarchical. One big advantage for simpler scenarios is that when you install an npm package, that package brings its own dependencies with it so it will work out of the box.

But problems arise when:

  • Both your project and some module you are using depend on another module.
  • The three modules have to talk to each other.

In Example

Let's say you are building YourCoolProject and you're using both JacksModule 1.0 and JillsModule 2.0. And let's suppose that JacksModule also depends on JillsModule, but on a different version, say 1.0. As long as those 2 versions don't meet, there is no problem. The fact that JacksModule is using JillsModule below the surface is just an implementation detail. We are bundling JillsModule twice, but that's a small price to pay when we get stable software out of the box.

But now what if JacksModule exposes its dependency on JillsModule in some way. It accepts an instance of JillsClass for example... What happens when we create a new JillsClass using version 2.0 of the library and pass it along to jacksFunction? All hell will break loose! Simple things like jillsObject instanceof JillsClass will suddenly return false because jillsObject is actually an instance of another JillsClass, the 2.0 version.

How peer dependencies solve this

They tell npm

I need this package, but I need the version that is part of the project, not some version private to my module.

When npm sees that your package is being installed into a project that does not have that dependency, or that has an incompatible version of it, it will warn the user during the installation process.

When should you use peer dependencies?

  • When you are building a library to be used by other projects, and
  • This library is using some other library, and
  • You expect/need the user to work with that other library as well

Common scenarios are plugins for larger frameworks. Think of things like Gulp, Grunt, Babel, Mocha, etc. If you write a Gulp plugin, you want that plugin to work with the same Gulp that the user's project is using, not with your own private version of Gulp.

temporary_user_name
  • 35,956
  • 47
  • 141
  • 220
Stijn de Witt
  • 40,192
  • 13
  • 79
  • 80
  • 4
    One important thing I noticed and isn't said anywhere, when we're building a plugin, should we have a duplicate of package dependencies, for the peer dependencies? In the OP example, we can see that `"grunt": "0.4.4"` is both in devDependencies and peerDependencies, and it does make sense to me to have a duplicate there, because it means both that I need that `grunt` package for my own use, but also that the users of my library can use their own version, as long as it respects the peerDependencies version lock. Is that correct? Or is the OP example a very bad one? – Vadorequest Nov 24 '18 at 17:11
  • 5
    I can imagine people creating a Grunt plugin being fans of Grunt :) As such it seems natural for them to use Grunt themselves for the build process of their plugin.... But why would they want to lock the Grunt version range their plugin works with to the build process they use to create it? Adding it as a dev dependency allows them to decouple this. Basically there are 2 phases: build time and run time. Dev dependencies are needed during build time. Regular and peer dependencies are needed at runtime. Of course with dependencies of dependencies everything becomes confusing fast :) – Stijn de Witt Nov 27 '18 at 23:26
  • Thanks., very clear answer. If all the dependencies follow `on-way-talk` rule(not talk to each other), then there is no need for peerDependencies.And peerDependencies solution is just warning. The final decision depends on developer. – ice6 Aug 17 '19 at 08:25
  • 1
    Thank you for this answer! Just to clarify, in your example, if `JacksModule` depends on `JillsModule ^1.0.0` with`JillsModule` being a peer dependency of `JacksModule` and `YourCoolProject` were using `JacksModule` and `JillsModule ^2.0.0`, we will get the peer dependency warning by NPM, which will advise us to install `JillsModule ^1.0.0` as well. But what happens then? `YourCoolProject` will now have two versions of `JillsModule` importable through `import jillsModule from "..."`? And how do I remember that when I use `JacksModule` I need to pass it an instance of `JillsModule v1.0.0`? – tonix Feb 25 '20 at 12:09
  • 1
    @tonix Well, it will indeed be a problem that you have a version incompatibility. peerDependencies does not solve that. But it does help to make the problem explicit. Because it will clearly show the version mismatch instead of using two versions silently. The app developer that is selecting the libraries will have to find a solution. – Stijn de Witt Mar 06 '20 at 12:51
  • Thank you for your reply. In this case I can see two options: either remove my dependency from `JillsModule ^2.0.0` and refactor the codebase to use `JillsModule ^1.0.0` with the `JacksModule` library or not depend on `JacksModule` library at all... – tonix Mar 06 '20 at 18:29
  • 2
    @tonix Or the third option: clone the `JacksModule` repo, upgrade it to depend on `JillsModule ^2.0.0` and offer a PR to the project maintainer. It may help to submit a bug first saying this dependency is outdated and you would like to help update it. If you make a good PR, most library maintainers will merge it and thank you for it. If maintainers are unresponsive, you can publish your fork to NPM namespaced under your name and use your fork instead. In any way, there are solutions but `peerDependencies` does not solve it on it's own. – Stijn de Witt Apr 14 '20 at 19:25
  • That's cool. Could you advise some resources where I can learn how to make PRs? Thank you! – tonix Apr 14 '20 at 19:48
  • @tonix Basically: Fork the repo in question, clone your fork, create a new branch, commit changes to new branch, push new branch to your fork. Go to github.com. Press button Create Pull Request that appears. It can be done directly from Git as well, but I do it this way. – Stijn de Witt Apr 15 '20 at 17:11
  • `Go to github.com. Press Create Pull Request` You create the pull request on the original repo or on the fork? – tonix Apr 15 '20 at 18:38
  • @tonix I visit my own fork and there the button appears – Stijn de Witt Apr 15 '20 at 20:21
  • 1
    @tonix Great to hear! And welcome to the Open Source community! It all starts with a first PR doesn't it? Here's hoping the project maintainer is a bit responsive and you get your PR merged quickly. If not, publishing your fork under your own namespace in NPM is always an option. Good luck! – Stijn de Witt Apr 26 '20 at 12:26
  • Thank you! If the project maintainer is unresponsive, and I publish my fork on NPM, then how other people may collaborate and send eventual issues and PRs to my fork instead of the original/unresponsive repo? If I open my fork I do not see the `Issues` tab... Thank you! – tonix Apr 26 '20 at 19:37
  • I have found it out: https://softwareengineering.stackexchange.com/questions/179468/forking-a-repo-on-github-but-allowing-new-issues-on-the-fork – tonix Apr 26 '20 at 21:47
32

I would recommend you to read the article again first. It's a bit confusing but the example with winston-mail shows you the answer why:

For example, let's pretend that winston-mail@0.2.3 specified "winston": "0.5.x" in its "dependencies" object because 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 and put them in your package.json as

{
  "dependencies": {  
    "winston": "0.6.2",  
    "winston-mail": "0.2.3"  
  }  
}

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

├── winston@0.6.2  
└─┬ winston-mail@0.2.3                
  └── winston@0.5.11

In this case, it is possible to have multiple versions of a package which would cause some issues. Peer dependencies allow npm developers to make sure that the user has the specific module (in the root folder). But you're correct with the point that describing one specific version of a package would lead to issues with other packages using other versions. This issue has to do with npm developers, as the articles states

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.

Therefore developers should follow semver for defining peerDependencies. You should open an issue for the grunt-steroids package on GitHub...

Andria
  • 4,712
  • 2
  • 22
  • 38
Fer To
  • 1,487
  • 13
  • 24
  • 1
    You say that `multiple versions of a package which would cause some issues` but isn't that the whole point of a package manager? They even discuss this further up in the same article where there are 2 versions of the same package in the project: one provided by the developer and one supplied by a 3rd party library. – Adam Beck Apr 02 '15 at 03:10
  • 1
    I think I understand the point of peer dependency but in the `winston` example am I now just unable to use the `winston-mail` library because my version does not match the peer dependency? I would much rather have that temporary downgrade from latest and greatest for the 1 library than to not be able to use it at all. – Adam Beck Apr 02 '15 at 03:12
  • 1
    for your first comment, as far as I understand and use it, it has to do with testing, e.g. if you have a package which has been tested by you on for a specific 3rd party package, you can't be sure that if one of your dependencies change ( bug fix, major feature update) that your package will work. Therefore, you can specify a specific plugin version and are save with your tests. – Fer To Apr 02 '15 at 09:26
  • 1
    On your second comment: that's why they say in the docs that developers should be lenient with their package dependencies and should use semver, e.g. instead of "0.2.1", "~0.2.1"-> allows "0.2.x" but not "0.3.x" , or ">=0.2.1" -> everything from "0.2.x" to "1.x" or "x.2.". .. (but not really preferable for an npm package would go with ~ – Fer To Apr 02 '15 at 09:31
24

peerDependencies explained with the simplest example possible:

{
  "name": "myPackage",
  "dependencies": {
    "foo": "^4.0.0",
    "react": "^15.0.0"
  }
}


{
  "name": "foo"
  "peerDependencies": {
    "react": "^16.0.0"
  }
}

running npm install in myPackage will throw an error because it is trying to install React version ^15.0.0 AND foo which is only compatible with React ^16.0.0.

peerDependencies are NOT installed.

Alex Lomia
  • 6,705
  • 12
  • 53
  • 87
Christopher Tokar
  • 11,644
  • 9
  • 38
  • 56
  • why not just put react 16 as a dep inside foo? that way both 15 and 16 will be avaiable and foo can use 16 and mypackage can use 15? – java_doctor_101 Aug 17 '19 at 06:17
  • 1
    React is a framework that is bootstrapped at runtime, in order for both React 15 and React 16 to exist on the same page you would need both to be boostrapped simultaneously which would be extremely heavy and problematic for the end user. If `foo` works with both React 15 and React 16 then it *could* list its peerDependency as `>=15 < 17`. – Jens Bodal Sep 13 '19 at 21:47
  • nitinsh99 my answer was to explain the purpose of peerDependencies with the simplest example possible, not how to get rid of the error thrown by peerDependencies – Christopher Tokar Oct 01 '19 at 08:08
  • @nitinsh99 adding react inside package dependency will provide issue like Hooks - Multiple reacts in a package – Masood Mar 19 '20 at 08:01