Implicit transitive peer dependencies
Tags: npm, yarn, javascript
Published: 2019-02-21 19:11:39

---
title: Implicit transitive peer dependencies
published: true
description: Where we see why `peerDependencies` mustn't be forgotten
tags: npm, yarn, javascript
---

**TL;DR:** If you write a package that depends on `Foo`, and if `Foo` has a peer dependency, then you must provide it in either of the `dependencies` or `peerDependencies` fields. You won't "implicitly inherit" the peer dependencies declared in `Foo`.

---

Peer dependencies are a fickle beast. Sometimes powerful since they allow us to pick ourselves the version of a package we want to use, and sometimes annoying as they trigger a bunch of "unmet peer dependency" errors (btw, Yarn now supports [optional peer dependencies](https://github.com/yarnpkg/rfcs/blob/master/accepted/0000-optional-peer-dependencies.md)! ;). They also have some corner cases, and it's one of them we're going to talk about today.

Imagine you're writing a preset for Babel. Your preset depends on `babel-plugin-proposal-class-properties` which is super userful. Nice! This is what your `package.json` will look like:

```json
{
  "name": "babel-preset-arcanis",
  "dependencies": {
    "@babel/plugin-proposal-class-properties": "^7.3.3"
  }
}
```

And you publish this to the npm registry, and all is good. Right? Wrong! See, you've forgotten a small detail. Let's see the `package.json` for [`babel-plugin-proposal-class-properties@7.3.3`](https://cdn.jsdelivr.net/npm/@babel/plugin-proposal-class-properties@7.3.3/package.json) to figure out the problem.

```json
{
  "name": "@babel/plugin-proposal-class-properties",
  "version": "7.3.3",
  "...",
  "peerDependencies": {
    "@babel/core": "^7.0.0-0"
  }
}
```

Ah! Unknowingly to us, `babel-plugin-proposal-class-properties` has a peer dependency on `@babel/core`, and we're not providing it! Now, I already hear you: *"but my dear Maël, `@babel/core` is meant to be provided by the user of our preset and as such we don't need to list it - the package manager will figure it out"*. It sounds logical indeed, but there's a flaw in your plan.

---

Let's put our Babel example aside for a second, and let's consider a slightly different case. Imagine the following situation:

  - Your application has a dependency on `Foo` and `Bar@1`
  - The `Foo` package has a dependency on `Baz` and `Qux`
  - The `Baz` package has a peer dependency on `Bar`
  - For simplicity, let's say that `Baz` and `Qux` cannot be hoisted (in a real case scenario, this would typically be because their direct ancestors happen to depend on incompatible versions).

Now let's unravel what happens. Again for simplicity, let's imagine we're in an old style, non-PnP, environment (ie a big `node_modules`). In this situation we're going to end up with something similar to the following:

```
./node_modules/bar@1
./node_modules/foo
./node_modules/foo/node_modules/baz
./node_modules/foo/node_modules/qux
```

So: is `Baz` able to access the version of `Bar` provided by your application? *"Well yes, of course"*, I hear you say, *"Ergo, checkmate, I win, and you owe me five bucks."* Not so fast. let's talk a bit about this `Qux` fellow. In fact, let's add the following requirement:

  - The `Qux` package has a dependency on `Bar@2`

It doesn't sound much, but how will it change the layout of our packages on the disk? Well, quite a bit. See, because `Bar@1` (required by our application) and `Bar@2` (required by `Qux`) cannot be merged, the package manager will find itself in a situation where `Bar` can only be hoisted one level up (inside `Foo`):

```
./node_modules/bar@1
./node_modules/foo
./node_modules/foo/node_modules/baz
./node_modules/foo/node_modules/bar@2
./node_modules/foo/node_modules/qux
```

See? Our `Bar@2` packages appeared in `foo/node_modules/bar` - it couldn't be hoisted any further! And what it entails is simple: now, instead of `Baz` being able to require `Bar@1` as you maybe expect, it will instead use `Bar@2` that has been hoisted from the `Qux` dependencies.

I hear you, once again: *"ok, but the package manager should figure out that since there's a transitive peer dependency in `Foo`, then `Bar@2` should not be hoisted into it"*. You're starting to ask a lot from the package manager, aren't you? And the answer isn't that simple. See, some packages might rely on the broken behavior (as in, they would expect `Qux` to get `Bar@2`). Changing this would actually be a breaking change - on top of being a funny problem algorithmically speaking (funny story for another time).

--- 

So let's go back to our Babel example. What's the answer? What should we do to avoid issues such as the one described above? What sacrifice must be do to appease the Old Gods? Fortunately, it's much simpler:

```json
{
  "name": "babel-preset-arcanis",
  "dependencies": {
    "@babel/plugin-proposal-class-properties": "^7.3.3"
  },
  "peerDependencies": {
    "@babel/core": "^7.0.0"
  }
}
```

See what I've done? I've just listed `@babel/core` as one of our dependencies. Nothing more, nothing less. Thanks to this, the package manager is now fully aware of what behavior to adopt: since there is a peer dependency on `@babel/core`, it is now forbidden to hoist it from a transitive dependency back to the level of `babel-preset-arcanis` 👌