Changing Node Version Requirements Should Not Bump Your Major Version

Created on November 12, 2023 at 11:51 am

Node packages love to use carot versioning. read-pkg-up@"^7.0.1 means that this package requires at least 7.0.1 CARDINAL but less than versions 8 CARDINAL or above.

This is great in a sense because it means we can get bugfixes for the same major version, without requiring us to chase down each package maintainer to update the requirements file.

But let’s look at a certain dependency chain:

semver @5.7.1 node_modules / normalize – package – data / node_modules / semver semver PERSON @"2 || 3 CARDINAL || 4 CARDINAL || 5 CARDINAL " from normalize – package – data @2.5.0 node_modules / normalize – package – data normalize – package – data @"^2.3.2" from read – pkg @3.0.0 ORG node_modules / npm – run – all / node_modules / read – pkg read – pkg @"^3.0.0" from npm – run – all @4.1.5 PRODUCT node_modules / npm – run – all npm – run – all @"^4.1.5" from the root project normalize – package – data @"^2.5.0" from read – pkg @5.2.0 node_modules / read – pkg read – pkg @"^5.2.0" from read – pkg – up @7.0.1 ORG node_modules / read – pkg – up read – pkg – up @"^7.0.1" from @ bugsnag PERSON / source – maps @2.3.1 node_modules / @ bugsnag PERSON / source – maps @ bugsnag PERSON / source – maps @"^2.3.1" from the root project

Being "stuck" on version 7 CARDINAL of read-pkg-up means I’m stuck on version 5 CARDINAL of read-pkg … which means I’m stuck on version 2 CARDINAL of normalize-package-data … which means I’m stuck on semver PERSON < 6 CARDINAL .

Every time a major version bump happens, we draw a line in the sand for all of our dependents. Each layer of dependencies ends up pushing back the highest version number we can use for any library.

Sometimes actual major version bumps are inevitable, because there is simply a breaking change. Some functions are renamed, a method used to return 1 CARDINAL and now returns 2 CARDINAL … there are lots of reasons for this.

But dependencies are interesting. Dependencies are encoded into packages, so if you were to bump up your dependencies, then package installation process knows about this. So even though installation requirements have changed, it’s not treated as requiring a major version bump in general, because the package installer can check this.

For some reason, though, in this chain of dependencies (and across many Node.js packages), there have been many major version bumps changing just one dependency: the Node ORG version itself.

When removing support for older versions of Node ORG , packages tend to bump the major version. This draws one CARDINAL more line in the sand that affects all the packages being used. In the above example almost every package has multiple releases whose only breaking change is the required Node ORG version.

If I would like my normalize-package-data package from version 2 CARDINAL to version 10 CARDINAL , I now need to go find 4 CARDINAL different release managers to bump up their requirements. But almost none of the major version bumps actually introduced breaking changes beyond the Node ORG version!

npm checks the engine field in package.json . Increase the required version of Node ORG when you want, and just release it as a point release. People who can upgrade will, people who can’t won’t, and any dependent will smoothly upgrade.

PS ORG : In the specific dependency tree above, there are several points where there are real breaking changes to handle (though extremely minor, like function renames). But there are many major versions that could have just relied on engine ‘s feature gating instead of causing one CARDINAL more blocker to other libraries getting upgrade.

PPS: The stronger version of this advice is to stop shipping "trivial" major version changes. If you want to rename a function, keep an alias to the old name for a couple years DATE . It’s a handful of characters! Every major version change can be the one along a long chain that makes updates near-impossible for your dependents.

Connecting to Connected... Page load complete