Skip to content

Commit

Permalink
docs: tidy up dependency pinning essay (#7911)
Browse files Browse the repository at this point in the history
  • Loading branch information
HonkingGoose committed Dec 10, 2020
1 parent 92c8f4d commit b391a55
Showing 1 changed file with 15 additions and 14 deletions.
29 changes: 15 additions & 14 deletions docs/usage/dependency-pinning.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@ description: The pros and cons of dependency pinning for JavaScript/npm

# Should you Pin your JavaScript Dependencies?

Once you start using a tool/service like Renovate, probably the biggest decision you need to make is whether to "pin" your dependencies instead of using semver ranges.
Once you start using a tool/service like Renovate, probably the biggest decision you need to make is whether to "pin" your dependencies instead of using SemVer ranges.
The answer is "It's your choice", however we can certainly make some generalisations/recommendations to help you.
Jump to the bottom conclusions if you get impatient.

If you do not want to read the in-depth discussion, and just want our recommendations, you can skip to the bottom.

## What is Dependency Pinning?

To ensure we're all talking about the same thing, it's important to define exactly what we mean by dependency "pinning".

Historically, projects use semver ranges in their `package.json`.
Historically, projects use SemVer ranges in their `package.json`.
For instance, if you run `npm install foobar` you will see an entry like `"foobar": "^1.1.0"` added to your `package.json`.
Verbosely, this means "any foobar version greater than or equal to 1.1.0 but less than 2".
Therefore the project will automatically use 1.1.1 if it's released, or 1.2.0, or 1.2.1, etc - meaning you will get not only patch updates but also feature (minor) releases too.
Expand All @@ -26,11 +27,11 @@ If instead you "pin" your dependencies rather than use ranges, it means you use
## Why use ranges?

For projects of any type, the main reason to use ranges is so that you can "automatically" get updated releases - which may even include security fixes.
By "automatically", we mean that any time you run `npm install` you will get the very latest version matching your semver - assuming you're not using a lock file, that is.
By "automatically", we mean that any time you run `npm install` you will get the very latest version matching your SemVer - assuming you're not using a lock file, that is.

#### Tilde vs Caret

If you're familiar with the theory of semver, you might think that you only need to use tilde ranges (e.g. `"~1.1.0"`) to get bug fixes, rather than caret ranges (e.g. `"^1.1.0"`).
If you're familiar with the theory of SemVer, you might think that you only need to use tilde ranges (e.g. `"~1.1.0"`) to get bug fixes, rather than caret ranges (e.g. `"^1.1.0"`).
This is true in theory but not in practice.
The reality is that for most projects, fixes are not "backported" to previous minor releases, and minor releases themselves may include fixes.
So for example release `1.2.0` may include one new feature and one fix, so if you stick with `1.1.0` then you will miss out on the fix as there will never be a `1.1.1` once `1.2.0` is already released.
Expand All @@ -41,7 +42,7 @@ This is the _reality_ of most open source packages.
A second reason for using ranges applies to "libraries" that are published as npm packages with the intention that they are used/`require()`'d by other packages.
In this case, it is usually a bad idea to pin all your dependencies because it will introduce an unnecessarily narrow range (one release!) and cause most users of your package to bloat their `node_modules` with duplicates.

For example, you might have pinned `foobar` to version `1.1.0` and another author pinned his/her `foobar` to dependency to `1.2.2`.
For example, you might have pinned `foobar` to version `1.1.0` and another author pinned his/her `foobar` dependency to `1.2.2`.
Any user of both your packages will end up with npm attempting to install two separate versions of `foobar`, which might not even work.
Even if both projects use a service like Renovate to keep their pinned dependencies up to date with the very latest versions, it's still not a good idea - there will always be times when one package has updated/released before the other one and they will be out of sync.
e.g. there might be a space of 30 minutes where your package specifies foobar `1.1.0` and the other one specifies `1.1.1` and your joint downstream users end up with a duplicate.
Expand All @@ -57,7 +58,7 @@ Note: we'll cover lock files later, don't worry.

Let's say that a "faulty" version `1.2.0` of `foobar` is released and it breaks one of your tests.

If you were using default caret semver ranges, then your `master` branch is now "broken" because its `package.json` says that any version 1.x above 1.1.0 is acceptable, and npm will choose the latest (`1.2.0`).
If you were using default caret SemVer ranges, then your `master` branch is now "broken" because its `package.json` says that any version 1.x above 1.1.0 is acceptable, and npm will choose the latest (`1.2.0`).
You would need to manually check and work out which dependency caused the failure (`foobar` may not have been the only dependency to have "automatically" upgraded since the last time your tests passed) and then you would need to pin the dependency yourself to stop `npm` installing `1.2.0`.

Consider the same situation if instead you were _pinning_ dependency versions.
Expand All @@ -68,7 +69,7 @@ Therefore you know exactly what you're running and you know exactly what failed
Now consider a similar theoretical scenario where `foobar@1.2.0` is faulty but it is _not_ caught by any of your automated tests.
This is more common and more dangerous.

If you were using semver ranges then this new version of `foobar` will likely be deployed to production automatically one day, sometime after which you notice errors and realise you need to fix it.
If you were using SemVer ranges then this new version of `foobar` will likely be deployed to production automatically one day, sometime after which you notice errors and realise you need to fix it.
Like before, you need to manually work out which dependency caused it - assuming you guess correctly that it was a new dependency version at fault - and pin it manually by editing `package.json` one dependency at a time.

Alternatively, if you were instead pinning `foobar` then you would receive a PR for `foobar@1.2.0` which awaits your approval.
Expand Down Expand Up @@ -137,7 +138,7 @@ It's a good question!

![broken-lockfile](assets/images/broken-lockfile.jpg)

Lock files are a great companion to semver ranges _or_ pinning dependencies, because these files lock (pin) deeper into your dependency tree than you see in `package.json`.
Lock files are a great companion to SemVer ranges _or_ pinning dependencies, because these files lock (pin) deeper into your dependency tree than you see in `package.json`.

#### What a lock file will do for you

Expand Down Expand Up @@ -176,7 +177,7 @@ The (broken) upgrade to `1.2.0` would have been explicitly proposed to you via a
Meanwhile you could be upgrading all the other essential fixes of other dependencies without worrying about `foobar`.
You could even be running `yarn upgrade` regularly to be getting _indirect_ package updates in the lockfile and seeing if everything still passes.

Therefore, the lock file does not solve the same semver problems that pinning solves - but it compliments it.
Therefore, the lock file does not solve the same SemVer problems that pinning solves - but it compliments it.
For this reason our usual recommendation using a lock file regardless of whether you pin dependencies or not, and pinning even if you have a lock file.

Don't forget though that our motto is "Flexible, so you don't need to be", so go ahead and configure however you want.
Expand Down Expand Up @@ -211,10 +212,10 @@ But certainly "does it give a false sense of security" is not a question we can

We recommend:

1. Any apps (web or Node.js) that aren't `require()`'d by other packages should pin all types of dependencies for greatest reliability/predictability
2. Browser or dual browser/node.js libraries that are consumed/`required()`'d by others should keep using semver ranges for `dependencies` but can use pinned dependencies for `devDependencies`
3. Node.js-only libraries can consider pinning all dependencies, because application size/duplicate dependencies are not as much a concern in Node.js compared to the browser. Of course, don't do that if your library is a micro one likely to be consumed in disk-sensitive environments
4. Use a lock file
1. Any apps (web or Node.js) that aren't `require()`'d by other packages should pin all types of dependencies for greatest reliability/predictability
2. Browser or dual browser/node.js libraries that are consumed/`required()`'d by others should keep using SemVer ranges for `dependencies` but can use pinned dependencies for `devDependencies`
3. Node.js-only libraries can consider pinning all dependencies, because application size/duplicate dependencies are not as much a concern in Node.js compared to the browser. Of course, don't do that if your library is a micro one likely to be consumed in disk-sensitive environments
4. Use a lock file

As noted earlier, when you pin dependencies then you will see an increase in the raw volume of dependency updates, compared to if you use ranges.
If/when this starts bothering you, add Renovate rules to reduce the volume, such as scheduling updates, grouping them, or automerging "safe" ones.
Expand Down

0 comments on commit b391a55

Please sign in to comment.