Skip to content

Commit 7650ec4

Browse files
committed
Let's install peer deps again!
Here's how.
1 parent a2c0fc5 commit 7650ec4

File tree

1 file changed

+224
-0
lines changed

1 file changed

+224
-0
lines changed

accepted/0000-install-peer-deps.md

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
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

Comments
 (0)