Provide a mechanism for users to specify a different dependency resolution at a specific point in the tree.
This supercedes and replaces Implement a package override option.
This feature was discussed in the Open RFC Call on 2020-04-08.
This addresses the following use cases:
- There is a bug that is yet to be fixed in a transitive dependency in our project's tree. While awaiting the bugfix to be published, we wish to override any instances of the dependency with a known good version.
- A security vulnerability is identified in a transitive dependency, but we are not able to upgrade the direct dependency pulling it in. An override would allow us to address the security issue in an easier way.
- There should only be a single copy of a given dependency in our tree, but npm's dependency resolution will result in a duplicate due to version conflicts.
- There is a transitive dependency X that exhibits a bug only when used in a particular way by another dependency Y. We wish to replace that instance of X, but only when included in the dependency graph of Y.
- A developer working on breaking change to
library
may wish to test it in several dependent projects at once within a workspace. The only way to do this today is to point them all at a tarball or git repository, which can be tedious.
Add a field to package.json
called overrides
, with the following shape:
{
"overrides": {
"<selection specifier>": "<result specifier or overrides set>"
}
}
The key is a package specifier with a name. The value is either a package
specifier without a name, or a nested overrides object. Nested override
objects apply to dependency resolutions within the portion of the
dependency graph indicated by the key, and may include a "."
member
with a string value to be applied to the node matching the override rule
set key.
The overrides
key will only be considered when it is in the root
package.json
file for a project. overrides
in installed dependencies
(including workspaces) will not be considered in dependency tree
resolution. Thus, there is no cascading overrides between multiple
different package.json
files at any given time.
Published packages may dictate their resolutions by pinning dependencies or
using an npm-shrinkwrap.json
file.
Applying overrides
for workspaces and installed dependencies may be
considered in a future RFC. However, there are considerable challenges in
the implementation, user expectations, and security of such an approach.
An override key will be considered a match if the named package specifier would be satisfied by the dependency being considered for resolution.
For example, consider a tagged git dependency
project@github:user/project#v1.0.0
, which contains a package.json
file
with the version 1.0.0
. In this case, the following override rule
would apply:
{
"[email protected]": "2.0.0"
}
If a dependency is subject to an override that puts it outside of the stated dependency range for a dependent, it will still be considered valid if the overridden version would have satisfied the dependency.
For example, consider a package foo
that depends on [email protected]
, installed
in the context of this override rule:
{
"bar@*": "2.0.0"
}
The resulting package tree on disk will have foo
loading [email protected]
when
it does require('bar')
. Despite this, it will not be treated as an
invalid dependency, because [email protected]
was valid prior to the override being
applied.
When the value of an override rule is a string, it is a replacement
resolution target for resolutions matching the key. String values must
be a dependency specifier without a name. (Aliases are supported using the
npm:
alias specifier.)
String overrides within a given overrides object are applied in definition order, stopping at the first rule to match. For example:
{
"overrides": {
"y@1": "1.2.3",
"[email protected]": "2.3.4" // <-- this will never match anything
}
}
In this case, the [email protected] -> 2.3.4
rule will never be applied, because
any y@1
dependency will be written to 1.2.3
, and stop evaluating string
override rules.
This prevents infinite regresses and loops, and greatly simplifies the feature both from an implementation and user experience point of view.
For example:
{
"overrides": {
"[email protected]": "2.0.0",
"[email protected]": "3.0.0",
"[email protected]": "1.0.0"
}
}
In this case, a package that depends on [email protected]
will instead be given
[email protected]
. A package that depends on [email protected]
will instead be given
[email protected]
. What it will not do is apply the [email protected]
override to
[email protected]
, and then consider whether any other overrides apply to it, and
so on. (In this case, it would create an infinite regress.)
A more realistic and less contrived case where a cascade could be desirable would be something like this:
{
"overrides": {
"[email protected]": "2.x", // <-- maybe 2.7.0, maybe some other 2.x
"[email protected]": "2.7.0"
}
}
In this case, we are saying that any webpack@1
dependencies should be
bumped up to webpack@2
, and furthermore, that any webpack@2
dependencies should be pinned to [email protected]
.
In practice, since rules are applied once and not stacked or cascaded, any
webpack dependency that would resolve to a version matched by 2.x
will be
overridden to 2.7.0
. But, any dependency that resolves to a version
matched by 1.x
will be set to whichever version happens to be the latest
2.x
at the time of installation.
In order to produce the intended behavior described, the user would have to either specify it twice:
{
"overrides": {
"[email protected]": "2.7.0",
"[email protected]": "2.7.0"
}
}
Or make the dependency matching range wider:
{
"overrides": {
"[email protected] || 2.x": "2.7.0"
}
}
An object value in an overrides object defines a child rule set.
If the first match for a given resolution is an object, then the object is a new rule set applied to all resolutions down that path in the dependency graph.
For example, this override rule will set all versions of bar
, but only
those depended upon by foo
.
{
"foo": {
"bar": "1.2.3"
}
}
Like string overrides, object overrides are only applied if they are the first rule in the set to match a given package.
{
"foo": "1.2.3",
"[email protected]": {
"bar": "2.3.4" // <-- this is never applied anywhere!
}
}
In this case, because the foo
rule will always match before the
[email protected]
rule, it takes precedence.
In order to both the package being targeted and its dependents, the special
key "."
can be used within an object override rule set. For example, to
set all versions of foo
to 1.2.3
and also set bar
to 2.3.4
when
depended upon by foo
, this ruleset could be used:
{
"foo": {
".": "1.2.3",
"bar": "2.3.4"
}
}
The "."
key is not relevant in the root overrides rule set, as the root
package is not ever subject to dependency resolution.
Thus, these two override rulesets are equivalent:
// string-valued rule
{
"foo": "1.0.0"
}
// object with . member
{
"foo": {
".": "1.0.0"
}
}
The "."
member must have a string value, to prevent ambiguous
resolutions.
// ambiguous and invalid!
{
"foo": {
".": { // <-- raises error, "." must be a string value
"bar": "1.0.0"
},
"bar": "2.0.0"
}
}
Parent rules are inherited by nested rule sets, applied after the child rules.
For example:
{
"foo": {
".": "1.0.0",
"bar": {
".": "2.3.4",
"baz": "3.0.0"
},
"baz": "2.0.0",
"boo": "3.0.0"
},
"boo": "1.0.0"
}
In this case,
- All versions of
foo
are set to1.0.0
by thefoo > .
rule. - All versions of
bar
depended upon byfoo
are set to2.3.4
by thefoo > bar > .
rule. - All versions of
baz
depended upon bybar
dependend upon byfoo
are set to3.0.0
by thefoo > bar > baz
rule. - All versions of
baz
depended upon byfoo
are set to2.0.0
, except those depended upon bybar
. - All versions of
boo
depended upon byfoo
(including those also depended upon bybar
) are set to3.0.0
. - All versions of
boo
in the tree are set to1.0.0
, except those depended upon byfoo
.
The assumption throughout this RFC is that nested dependency resolutions will be applied to all direct and transitive dependencies throughout the dependency graph from a given point.
In other words, the override {"x":{"c":"1.2.3"}}
will apply to any c
that exists anywhere in the dependency graph starting from any x
, without
differentiating between x -> a -> c
vs a direct dependendency x -> c
.
While this makes the override more powerful, and simplifies the implementation, it also increases the risk that an override may apply to packages that the user did not intend it to.
Supporting both overrides for an entire branch of the package tree and overrides limited to a direct dependency, would significantly increase the complexity of this feature.
Using a nested object expression that does not support **
or some
equivalent, it would be extremely tedious and error-prone to expect the
user to specify every path on the dependency graph where a module might
be found. Furthermore, it is arguably better in most cases to apply
the override too broadly rather than too narrowly.
Because the set of overrides that apply to a given node in the dependency graph will affect how its dependencies are resolved, a dependency must not be deduplicated against another instance that is subject to different override rules.
For example, consider the following dependency graph:
root -> (a@1, b@1)
a@1 -> c@1
b@1 -> c@1
c@1 -> d@1
Without any overrides in place, the tree on disk might look like this, with
the c
and d
dependencies deduplicated:
root
+-- a@1
+-- b@1
+-- c@1
+-- d@1
However, consider the following override rule applied at the root project level:
{
"overrides": {
"b": {
"d": "2"
}
}
}
The b
package will still depend on c@1
, just like a
, but the c@1
that it depends on will in turn depend on d@2
rather than d@1
.
Thus, the c
dependency cannot be deduplicated. The tree on disk would
look something like this:
root
+-- a@1
+-- b@1
| +-- c@1
| +-- d@2
+-- c@1
Because the dependency at root > b > c
has a different set of overrides
applied, it cannot be deduplicated against the dependency at root > a > c
.
There is no way to remove an override for a portion of the tree. If an override rule causes problems for a portion of the dependency graph, then the user must either:
- Apply the override more narrowly, limiting the dependency paths where it is applied.
- Apply additional override rules for the dependency paths where the higher-level override causes problems.
The overrides rules are only used to modify the effective resolution target for dependencies, and only dependency resolution is used to match rules.
There is no facility for overriding based on other factors, such as platform, operating system, or other package metadata.
There is no facility for mutating package metadata.
While overrides can be used to reduce duplication, it is not guaranteed.
Consider the following dependency graph:
root -> (x@*, y@*)
x@1 -> (y@1)
x@2 -> (y@2)
y@1 -> (x@1)
y@2 -> (x@2)
By default, this would result in the following package tree on disk:
root
+-- x@2
+-- y@2
When installed with the following override rules:
{
"x@1": {
"y": "2"
},
"y@1": {
"x": "2"
},
"x@2": {
"y": "1"
},
"y@2": {
"x": "1"
}
}
The resulting package tree looks like this:
root
+-- y@2 (inherits {"x":"1"} rule set)
| +-- x@1 (overridden by "y@2 > x" rule)
| (x@1 -> y@1 dep overridden by `"y@1": "2"` rule, and deduped above)
+-- x@2 (inherits {"y": "1"} rule set)
+-- y@1 (overridden by "x@2 > y" rule)
(y@1 -> x@1 dep overridden by `"x@1": "2"` rule, and deduped above)
Where previously there were 2 packages installed, now there are 4.
To replace all versions of x
with a version 1.2.3
throughout the tree:
{
"overrides": {
"x": "1.2.3"
}
}
If a bug is found in [email protected]
, which is known to be fixed in 1.2.4
, then
bump only that dependency (but anything that resolves to 1.2.2
or
1.2.4
should be left alone.)
{
"overrides": {
"[email protected]": "1.2.4"
}
}
To replace all versions of [email protected]
with version 1.2.3
, but only when used
as a dependency of [email protected]
:
{
"overrides": {
"[email protected]": {
"[email protected]": "1.2.3"
}
}
}
To replace all instances of underscore
with lodash
:
{
"overrides": {
"underscore": "npm:lodash"
}
}
To force all versions of react
to be 15.6.2
, except when used by the
dependencies of tap
(which depends on ink
and requires react@16
):
{
"overrides": {
"react": "15.6.2",
"tap": {
"react": "16"
}
}
}
To use a known good git fork of metameta
, but only when used as a
dependency of foo
when foo
is a dependency of bar
:
{
"overrides": {
"bar": {
"foo": {
"metameta": "git+ssh://[email protected]/metameta#known-good"
}
}
}
}
This algorithm gives rise to the following behaviors and edge cases when applying rules throughout the dependency resolution process.
A deliberately extreme example:
{
"overrides": {
"y@1": "1.2.3",
"[email protected]": "1.2.4", // not relevant
"[email protected]": "1.2.5", // not relevant
"y@>1.2 <1.3": "1.2.6", // not relevant
"[email protected]": "1.2.3" // not relevant
}
}
In this case, all the rules after the first are irrelevant, because only the first rule to match will have any effect.
It is possible to "swap" versions. This will not cause an infinite regress, because only the first rule will be applied.
{
"overrides": {
"swap@1": "2",
"swap@2": "1"
}
}
In this case, any version matching swap@1
will be overridden to swap@2
.
Any version initially matching swap@2
will be overridden to swap@1
.
Because rules do not stack, there is no infinite regress.
There are cases where it may be desirable to lock a version of a given package down to a specific version within the tree, and override the version of one of its dependencies.
Use the "."
key for this.
For example:
{
"y": {
".": "1.0.2",
"x": "5.0.3"
}
}
When a string value override rule is defined at the top level, it is inherited by child override rule sets.
For example, consider that y
depends on both x
and z
.
{
"y": {
"z": "1.2.3"
},
"x": "1.2.3"
}
Within the y
branch of the dependency graph, x
will be overridden to
[email protected]
and z
will be overridden to [email protected]
.
Elsewhere within the dependency graph, x
will be overridden to 1.2.3
,
but z
will not be overridden.
The name "overrides" was chosen for the following reasons:
This feature is fundamentally different from Yarn resolutions
, and closer
in both effect and intent to overrides
in Bower and Dep. As there are
fundamental semantic differences, it would not be possible to reliably
translate a Yarn resolutions
field to the format described here.
Therefor, it ought to be a different key.
Using this feature should be considered a hack in most cases, something that is done temporarily while waiting for a bug to be fixed, or to avoid excessive duplication caused by an overly strict meta-dependency specifier.
"Resolutions" sounds resolved and ok; "overrides" sounds like we're going against the package manager's recommendations. (Not all npm users are native English speakers, but enough are that this connotation is worth considering.)
-
Yarn Selective Dependency Resolutions
This feature is very similar to yarn resolutions. However, it avoids the following problems:
- Using path/glob syntax for a graph traversal is somewhat challenging.
In particular the
**
and*
behavior is not well defined, and given the other areas where we use paths and globs (files
andworkspaces
in particular) it sets an expectation that negating and advanced glob patterns would be supported. - Because yarn resolutions must indicate a specific version, it limits the use cases that we can support.
- Using path/glob syntax for a graph traversal is somewhat challenging.
In particular the
-
Limiting a package name to only a specific version, and preventing it from being used in published projects, is unnecessarily limiting.
Furthermore, the prior RFC was unclear whether dependencies with a
replace
field would have their replacements respected. Allowing replacements in nested dependencies is hazardous, and a warning without an action is an antipattern, as it tends to train users out of expecting that warnings should be acted upon. -
Pub uses a dependency override option that allows defining a specific source or version. As Dart does not use nested dependencies for conflict resolution, this is effectively the same as the feature this RFC describes, but without the nested override support.
-
Bower, like Pub, uses a flat dependency graph, and so conflicts must be resolved by the user. When a user chooses an option in a dependency conflict, the resolution is saved to the
bower.json
file in theresolutions
field, and used for future conflicts. As Bower does not use nested dependencies for conflict resolution, this is effectively the same as the feature this RFC describes, but without the nested override support.
-
Should
bundleDependencies
andshrinkwrap
dependencies be subject to overrides? It would be a change to those contracts, and impose additional implementation challenges, but my suspicion is that the user expectation is that they would be overridden.If they are not, then it needs to be called out in the
overrides
documentation.If they are, then the implementation needs to be updated to consider the challenges involved.
This question will be revisited when we have running code to establish whether the implementation challenge will be as significant as expected.