Skip to content

Commit

Permalink
acceptDependencies package.json field
Browse files Browse the repository at this point in the history
PR-URL: #72
Credit: @coreyfarrell
Close: #72
Reviewed-by: @isaacs

EDIT(@isaacs): Updated with implementation details and results of
discussion.
  • Loading branch information
coreyfarrell authored and isaacs committed Mar 18, 2020
1 parent 2737ac7 commit 2869e4b
Showing 1 changed file with 195 additions and 0 deletions.
195 changes: 195 additions & 0 deletions accepted/0023-acceptDependencies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
# `acceptDependencies` to allow deduplication of conditionally compatible dependency versions

## Summary

Provide a way for a module author to declare alternative versions of
dependencies that can be used.

## Motivation

npm does not currently provide a method for a package which supports
`node.js >=4` to allow upgrade of a dependency which requires `node.js >=6`
unless they also bump their own minimum verion. This proposal would allow
such authors to declare that an alternative version of the dependency can
be used if that version is already being installed by another dependency.
Generally this would be used when the dependency is only a breaking change
due to dropping support for old node.js versions or when other breaking
changes do not apply to the local module.

Take `make-dir` for example. Maybe you maintain a package which still
needs to support node.js 4.x. This restricts you to `[email protected]`.
This proposal allows you to declare that `[email protected]` or `[email protected]`
will also work. When users install your module they will get
`[email protected]` by default, but if they also install another dependency
which requires `[email protected]` your project will use the upgraded version
as well. The idea is that if the updated version is being installed by the
application it is no longer necessary to function on node.js 4.x.

## Detailed Explanation

npm would support reading `acceptDependencies` object from package.json.
This object would declaring alternative versions of packages listed in
`dependencies`, `devDependencies`, `optionalDependencies` or
`peerDependencies`.

Example `package.json`:

```js
{
"name": "my-node4-package",
"engines": {
"node": ">=4"
},
"dependencies": {
"make-dir": "^1.3.0"
},
"acceptDependencies": {
"make-dir": "2.x - 3.x"
}
}
```

Creating a node.js 8 application and installing this package alone would
produce the following `npm ls`:

```
[email protected] /usr/src/npm/example-app
└─┬ [email protected]
└─┬ [email protected]
└── [email protected]
```

Now suppose you are also going to use `make-dir` directly, so you run `npm
i make-dir`, installing the latest, 3.0.0. With current versions of npm
this will result in `npm ls` showing:

```
[email protected] /usr/src/npm/example-app
├─┬ [email protected]
│ └── [email protected]
└─┬ [email protected]
└─┬ [email protected]
└── [email protected]
```

Future versions with support for `acceptDependencies` would show:

```
[email protected] /usr/src/npm/example-app
├─┬ [email protected]
│ └── [email protected]
└─┬ [email protected]
└── [email protected] deduped
```

### Acceptable `acceptDependencies` package specifiers

Values in the `acceptDependencies` object may be any package specifier that
is allowed in other dependency objects within package.json.

In every case, regardless of the type of dependency, the specifier in the
`acceptDependencies` object will be considered a valid resolution if
already present.

For example:

```js
// will fetch from the registry, unless the git version is present
// note that this will allow ANY version, as long as it comes from that
// git repo, but only 1.x otherwise.
{
"dependencies": {
"@user/project": "1.x"
},
"acceptDependencies": {
"@scope/dep": "git+ssh://[email protected]:user/project.git"
}
}
```

```js
// inverse of the above. fetch from the git repository, UNLESS a 1.x
// version is already present in the tree as a deduplicated option.
{
"dependencies": {
"@user/project": "git+ssh://[email protected]:user/project.git"
},
"acceptDependencies": {
"@scope/dep": "1.x"
}
}
```

## Rationale

Some maintainers want to provide support for the oldest possible version of
node.js. This often causes a large number of duplicate packages to be
installed. Older versions of the dependencies may be slower at runtime and
cause a large expansion of install size for applications that largely
require up to date dependencies. This proposal intends to reduce the
trade-off required to support old versions of node.js.

## Alternatives

1. `resolutions` field of yarn.
2. System-wide `.npmresolutions` to allow global upgrade / remapping to
specific versions.
3. Proposed `overrides` field of npm (ref #39)

All of these options can be more powerful than the idea of
`acceptDependencies`. The with `resolutions` and `.npmresolutions` is that
they would only work if users of a module determine that the dependency can
and should be upgraded. This can result in footguns, takes control out of
the maintainers hands. `overrides` shifts the authority from users to
module authors but would require introducing condition expansion into
`package-lock.json`.

## Implementation

The `Edge` class in `@npmcli/arborist` will track the `acceptDependencies`
value for any type of dependency relationship it tracks.

When testing whether an Edge is valid, check both the spec on the edge, as
well as the `accept` spec, and resolve as valid if either spec is
satisfied.

When resolving a specifier to a new Node in the tree, _only_ use the actual
specifier for the Edge, and ignore the `acceptDependencies` specifier.

Packages listed in `acceptDependencies` are only relevant if they are
_also_ listed as another type of dependency. Otherwise, they are ignored.

## Known Limitations

This does not address the following use cases, which are out of scope of
this RFC.

### Dependency Groups

A package may wish to indicate that it needs to use both `module` and
`module-plugin`. It can work with either the `1.x` or `2.x` versions of
these dependencies, but they must be compatible versions in order to work
properly. That is, it can install _either_ `[email protected]` and
`[email protected]`, _or_ it can install `[email protected]` and
`[email protected]`.

This is properly addressed by one of these modules indicating a peer
dependency on the other. (Typically the plugin indicating that it has a
peer dependency on the core module.)

The tree design algorithm in npm v7 is capable of resolving
`peerDependencies` properly, so it will behave correctly. Even if peer
dependencies are omitted from the actual reification, the tree will be
designed such that it would not cause a conflict if they _were_ installed.

### Targeted Optional Metadep Omissions

It would be nice to be able to indicate that an optional dependency of an
installed dependency should not be installed, or even attempted.

For example, a package `small-pkg` has an optional dependency on
`huge-pkg`. A user may wish to install `small-pkg`, but _not_ fetch
`huge-pkg`.

`optionalDependencies` does not provide a way to indicate that anything
should _not_ be installed.

0 comments on commit 2869e4b

Please sign in to comment.