Why newer isn't always better
Twice in the past week I've seen projects I'm working on go off the rails when an automatic, seemingly minor, dependency update didn't do what it advertised.
First, it's helpful to understand something about how packages are versioned in the node.js/JavaScript ecosystem. By and large, such projects use Semantic Versioning (semver) to describes version numbers in the following terms:
Given a version number MAJOR.MINOR.PATCH, increment the:
1. MAJOR version when you make incompatible API changes,
2. MINOR version when you add functionality in a backwards compatible manner, and
3. PATCH version when you make backwards compatible bug fixes.
As a result, version 2.3.4
has a distinct meaning from 2.3.5
(patch update) or 2.4.0
(minor update) or 3.0.0
(major update).
Second, when we npm install
a dependency, we typically don't use absolute version numbers (although you certainly can!). Instead, we typically use one of the following ways of defining a dependency version in package.json:
version
Must matchversion
exactly>version
Must be greater thanversion
>=version
etc<version
<=version
~version
"Approximately equivalent to version" See semver^version
"Compatible with version" See semver1.2.x
1.2.0, 1.2.1, etc., but not 1.3.0http://...
See 'URLs as Dependencies' below*
Matches any version""
(just an empty string) Same as*
version1 - version2
Same as>=version1 <=version2
.range1 || range2
Passes if either range1 or range2 are satisfied.git...
See 'Git URLs as Dependencies' belowuser/repo
See 'GitHub URLs' belowtag
A specific version tagged and published astag
Seenpm dist-tag
path/path/path
See Local Paths below
Probably the most common ways that I see people use are ~1.2.3
and ^1.2.3
. The former is a tilde range, and the latter is a caret range. Understanding what they mean is important.
1.2.3
- use version1.2.3
exactly. Accept no subsititutes.~1.2.3
- use version1.2.3
or any other compatible patch level version:1.2.4
,1.2.5
, ...,1.2.9
^1.2.3
- use version1.2.3
or any other compatible minor or patch level version:1.2.4
,1.2.9
,1.3.0
,1.4.6
,1.9.9
There's a real difference between using 1.2.3
and ^1.2.3
. Part of that difference is how much you trust the ecosystem to follow the rules of Semantic Versioning, and how well you think they've done interpretting what is and isn't a breaking change.
I said earlier that I'd been burned by semantic versioning recently, and I wanted to talk about one of the instances. I was starting to work on fixing a bug in one of the repositories for diagrams.net. I cloned the repo, built the code, and immediately crashed. The error seemed to imply a version issue, so I tried adjusting my environment (surely it's me?) No amount of fidgeting with my local dev environment would get rid of the crash, and I decided to turn my attention to figuring out what it was instead of fixing my actual bug.
It turned out that one of the dependencies had shipped an update 2 days previous from 1.2.1
to 1.2.2
, and decided to introduce a startup exception if your version of Electron didn't meet some new criteria. Using 1.2.1
was fine; but 1.2.2
meant you crashed into the ocean on startup. To repeat, that's moving from 1.2.1
to 1.2.2
:
3. PATCH version when you make backwards compatible bug fixes.
Because the diagrams.net project was using version ^1.2.1
, the compatible version 1.2.2
is a reasonable substitute. Except that it wasn't.
I mention this because it's easy to get burned by inconsistent updates by upstream projects, who play fast and loose with major.minor.patch update numbers. Understanding what you're signing up for when you pick a dependency is important. So too is knowing what you mean when you ship an update to a project others are using.