|
| 1 | +# Install Peer Dependencies |
| 2 | + |
| 3 | +## Summary |
| 4 | + |
| 5 | +Install `peerDependencies` along with packages that peer-depend on them. |
| 6 | + |
| 7 | +Ensure that a validly matching peer dependency is found at or above the |
| 8 | +peer-dependant's location in the `node_modules` tree. |
| 9 | + |
| 10 | +If `peerDependencies` are omitted from the install, then create a tree |
| 11 | +which _could_ have `peerDependencies` added correctly. |
| 12 | + |
| 13 | +## Motivation |
| 14 | + |
| 15 | +Due to some of the difficulties that `peerDependencies` present with the |
| 16 | +installer as of npm v6, `peerDependencies` are not installed by default |
| 17 | +with npm. Instead, it's on individual consumers to install and manage |
| 18 | +`peerDependencies` by themselves, prompted by a warning. |
| 19 | + |
| 20 | +That warning is often misinterpreted as a problem, and reported to package |
| 21 | +maintainers, who in response, sometimes omit the peer dependency, treating |
| 22 | +it as effectively an optional dependency instead, but with no checks on its |
| 23 | +version range or validity. |
| 24 | + |
| 25 | +Furthermore, since the npm installer is not peer dependency aware, it |
| 26 | +can design a tree which causes problems when peer dependencies are present. |
| 27 | + |
| 28 | +This proposed algorithm addresses these problems, making `peerDependencies` |
| 29 | +a first-class concept and a requirement for package tree validity. |
| 30 | + |
| 31 | +For example, `tap` had a dependency on `ink`, which had a peer dependency on |
| 32 | +`react@16`. In order to meet this peer dependency `tap` also added a |
| 33 | +dependency on `react@16`. However, if a package depends on both `tap` and |
| 34 | +`react@15`, then the installer will see the conflicts _only as it relates |
| 35 | +to tap's dependency_, resulting in a package tree like: |
| 36 | + |
| 37 | +``` |
| 38 | ++-- react (15) |
| 39 | ++-- ink |
| 40 | ++-- tap |
| 41 | + +-- react (16) |
| 42 | +``` |
| 43 | + |
| 44 | +Because no version of `ink` existed higher in the tree, the installer |
| 45 | +moves it up a level, even though this breaks the peer dependency. |
| 46 | + |
| 47 | +To work around this, `tap` currently bundles both `ink` and `react`, but |
| 48 | +this is not optimal. In cases where `ink` and/or `react` _can_ be |
| 49 | +deduplicated, they no longer are. |
| 50 | + |
| 51 | +## Detailed Explanation |
| 52 | + |
| 53 | +This extends the "maximally naive deduplication" algorithm that npm |
| 54 | +currently uses. |
| 55 | + |
| 56 | +### Validity Test |
| 57 | + |
| 58 | +A peer dependency is valid iff: |
| 59 | + |
| 60 | +- The name resolves from the dependant package to a package which satisfies |
| 61 | + the listed dependency according to standard dependency resolution |
| 62 | + semanatics, and |
| 63 | +- The resolved dependency is not found in the dependant's `node_modules` |
| 64 | + tree (ie, it must be at or above it's own parent), _unless_ the dependent |
| 65 | + is the root in its package tree. |
| 66 | + |
| 67 | +### Adding a New Dep |
| 68 | + |
| 69 | +When adding a dependency `D` in a range `R` with a set of peer dependencies |
| 70 | +`P` at location `L` in the tree: |
| 71 | + |
| 72 | +- For each `p` in `P`, starting from `L`, find the location in the |
| 73 | + tree closest to the root where `p` can be placed without conflicts. |
| 74 | +- If all `p` in `P` can be placed: |
| 75 | + - then: note the location furthest from the root where some `p` was |
| 76 | + placed, as location `L'` |
| 77 | + - else: error, `D` cannot be placed in this tree at location `L`. |
| 78 | +- Starting from `L`, find the location in the tree closest to `L'` where |
| 79 | + `D` can be placed without conflicts. |
| 80 | +- If `D` can be placed between `L` and `L'`: |
| 81 | + - then: hooray! it is installed successfully. |
| 82 | + - else: error, `D` cannot be placed in this tree at location `L`. |
| 83 | + |
| 84 | +(Optional failure handling: attempt with other versions of `D` in the range |
| 85 | +`R`.) |
| 86 | + |
| 87 | +### Handling Future Tree Munging |
| 88 | + |
| 89 | +If a user installs a new dependency, which will cause a conflict with |
| 90 | +`D` or any of `P`, then re-start the placement of `D` and `P` at `L`. |
| 91 | + |
| 92 | +If `D` and `P` cannot be placed in the tree in the presence of the newly |
| 93 | +requested dependency, then refuse to install it until the user resolves the |
| 94 | +conflict. Otherwise, move `D` and `P` to their new homes as part of the |
| 95 | +installation. |
| 96 | + |
| 97 | +### Tracking and Verifying |
| 98 | + |
| 99 | +When reading from the actual `node_modules` tree (or an inflated |
| 100 | +shrinkwrap, ie, any time we have a full manifest), Arborist will flag |
| 101 | +`Edge` nodes of the `peer` type with an `INVALID` error if they resolve to |
| 102 | +their peer dependant's `node_modules` folder. |
| 103 | + |
| 104 | +## Rationale and Alternatives |
| 105 | + |
| 106 | +### A: Leave it |
| 107 | + |
| 108 | +We could keep not installing peer dependencies, and printing a warning |
| 109 | +about it. It causes problems, but there are workarounds. |
| 110 | + |
| 111 | +The main issue is that, because the use of `peerDependencies` has gotten so |
| 112 | +popular in the React community, and because React is extremely popular |
| 113 | +among front-end developers who are somewhat new to npm, the hazards of the |
| 114 | +current approach affect them the most profoundly, and they are the least |
| 115 | +able to know what to do when faced with the error. |
| 116 | + |
| 117 | +### B: Drop Support for Peer Dependencies Entirely |
| 118 | + |
| 119 | +Tempting. But that ship sailed long ago. Peer dependencies _do_ address a |
| 120 | +valid need for cases where a module adds functionality to a framework or |
| 121 | +plugin architecture. Dropping support would be too disruptive. |
| 122 | + |
| 123 | +### C: Treat Like Regular Dependencies |
| 124 | + |
| 125 | +Most of the time, this would result in the same package tree, and in fact, |
| 126 | +many react-using modules (like `ink`) do not need the peer-nature of a |
| 127 | +peer dependency. |
| 128 | + |
| 129 | +However, this would be a violation of the contract as it is widely |
| 130 | +understood and documented, and so would also be too disruptive. |
| 131 | + |
| 132 | +### D: Treat Like Optional Dependencies |
| 133 | + |
| 134 | +All the problems of B, combined with the problems of C. |
| 135 | + |
| 136 | +### E: Let Authors Declare Which peerDependencies Should Be Installed |
| 137 | + |
| 138 | +Add a dependency to both `dependencies` and `peerDependencies`. This would |
| 139 | +require that the package be installed at or above the dependent's level in |
| 140 | +the tree, and be satisfied by anything in the `peerDependencies` specifier. |
| 141 | +However, if _not_ found in the tree, then the package specifier in |
| 142 | +`dependencies` will be automatically installed. |
| 143 | + |
| 144 | +However, having a package in both peerDependencies and dependencies means |
| 145 | +that it would be installed as a normal dependency in npm v6 and before, |
| 146 | +which will generate an incorrect tree in many of the cases that the feature |
| 147 | +contemplated in this RFC seeks to address. |
| 148 | + |
| 149 | +See: [yarnpkg/berry#1001](https://github.com/yarnpkg/berry/issues/1001) |
| 150 | + |
| 151 | +### F: Use `peerDependenciesMeta` To Trigger Auto-Install |
| 152 | + |
| 153 | +We could do something like this: |
| 154 | + |
| 155 | +```json |
| 156 | +{ |
| 157 | + "peerDependencies": { |
| 158 | + "foo": "1.x", |
| 159 | + "bar": "2.x" |
| 160 | + }, |
| 161 | + "peerDependenciesMeta": { |
| 162 | + "foo": { |
| 163 | + "autoinstall": true |
| 164 | + }, |
| 165 | + "bar": { |
| 166 | + "autoinstall": false |
| 167 | + } |
| 168 | + } |
| 169 | +} |
| 170 | +``` |
| 171 | + |
| 172 | +That would enable package authors to be more fine-grained about which peer |
| 173 | +dependencies are installed, and which are not, and is not incompatible with |
| 174 | +this RFC. However, it is out of scope for this RFC, and may be |
| 175 | +contemplated as a way to address any concerns that arise during the v7 beta |
| 176 | +testing process. |
| 177 | + |
| 178 | +The default value of the `autoinstall` field in `peerDependenciesMeta`, and |
| 179 | +whether it overrides any `--omit=peer` or `--include=peer` options, is left |
| 180 | +as an open question for that future RFC. |
| 181 | + |
| 182 | +## Implementation |
| 183 | + |
| 184 | +This is implemented in `@npmcli/arborist` and included in npm v7. |
| 185 | + |
| 186 | +The `omit` option to `Arborist.reify()` can be used to exclude |
| 187 | +`peerDependencies` (or optional or dev dependencies) from the reification |
| 188 | +process. |
| 189 | + |
| 190 | +## Unresolved Questions and Bikeshedding |
| 191 | + |
| 192 | +### Issues Relying on peerDeps as "More Optional" `optionalDependencies` |
| 193 | + |
| 194 | +For several years _prior_ to npm v7, peerDependencies were not installed |
| 195 | +automatically. This has led to some cases where users rely on this fact, |
| 196 | +and use `peerDependencies` as a sort of more-optional |
| 197 | +`optionalDependencies`. That is, a dependency which is not installed by |
| 198 | +default, allowing the user greater control over its resolution. |
| 199 | + |
| 200 | +For example, a package.json file might do this: |
| 201 | + |
| 202 | +```json |
| 203 | +{ |
| 204 | + "peerDependencies": { |
| 205 | + "secret-thing": "1.x" |
| 206 | + } |
| 207 | +} |
| 208 | +``` |
| 209 | + |
| 210 | +and rely on users to provide `secret-thing` from a private git repository |
| 211 | +or other alternative specifier. Upon seeing this, npm v7 will attempt |
| 212 | +to fetch `secret-thing` from the registry if it has a version specifier, |
| 213 | +and is not satisfied by something higher up in the dependency tree already. |
| 214 | + |
| 215 | +However, as the default warning on seeing a missing peer dependency is |
| 216 | +to tell the user to install it, the status quo could be expected to lead to |
| 217 | +the same behavior, albeit without _automating_ that behavior. |
| 218 | + |
| 219 | +In the end, we have decided to release the npm v7 betas with |
| 220 | +`peerDependencies` autoinstallation enabled, and judge from early |
| 221 | +play-testing whether it's a net improvement in the user experience. If it |
| 222 | +turns out to cause problems, or not be worth the risk, we can default to |
| 223 | +omitting `peerDependencies`, and still build trees that _can_ have peer |
| 224 | +dependencies correctly installed by explicitly including them. |
0 commit comments