Node.js – Why use peer dependencies in npm for plugins

node.jsnpmpackage-managers

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?

Best Answer

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.