Node packages love to use carot versioning. read-pkg-up@"^7.0.1 means that this package requires at least 7.0.1 but less than versions 8 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@"2 || 3 || 4 || 5" from normalize-package-data@2.5.0
  node_modules/normalize-package-data
    normalize-package-data@"^2.3.2" from read-pkg@3.0.0
    node_modules/npm-run-all/node_modules/read-pkg
      read-pkg@"^3.0.0" from npm-run-all@4.1.5
      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
      node_modules/read-pkg-up
        read-pkg-up@"^7.0.1" from @bugsnag/source-maps@2.3.1
        node_modules/@bugsnag/source-maps
          @bugsnag/source-maps@"^2.3.1" from the root project

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

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 and now returns 2... 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 version itself.

When removing support for older versions of Node, packages tend to bump the major version. This draws one 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 version.

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

npm checks the engine field in package.json. Increase the required version of Node 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: 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 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. 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.