What I Wanted to Do
I wanted to update one of our private packages (lets call it pkg-a) to a new patch version to include a bugfix we just released. The new patch version had no new dependency version requirements. I expected it to bump the version of only that package in the lockfile.
Tree before:
└┬ pkg-a@^1.0.0 locked at 1.0.0 (latest: 1.0.1)
└─ pkg-b@^1.0.0 locked at 1.0.0 (latest: 1.1.0)
Expected tree afterwards:
└┬ pkg-a@^1.0.0 locked at 1.0.1 (latest: 1.0.1)
└─ pkg-b@^1.0.0 locked at 1.0.0 (latest: 1.0.1)
What Happened Instead
It did not just update that package, but also all its dependencies, including sanitize-html. The update of pkg was safe, the change was very small. However, the update to sanitize-html actually had a critical bug in it. The update was not even noticed in code review since package-lock files are hidden in GitHub diffs by default.
Actual tree afterwards:
└┬ pkg-a@^1.0.0 locked at 1.0.1 (latest: 1.0.1)
└─ pkg-b@^1.0.0 locked at 1.0.1 (latest: 1.0.1)
Reproduction Steps
I have setup a setup as described above in a GitHub repo https://github.com/felixfbecker/npm-shallow-update-repro
You can also generate one yourself like this:
npm init
npm install sanitize-html@1.18.2 # just to lock the old version
npm install metascraper@3.11.7 # not the latest, so we can update it
npm rm sanitize-html # make sanitize-html an *indirect* dependency
Now your tree looks like this:
> npm ls sanitize-html
npm-shallow-update-repro@1.0.0 /Users/felix/src/github.com/felixfbecker/npm-shallow-update-repro
└─┬ metascraper@3.11.7
└── sanitize-html@1.18.2
Now run npm update metascraper. The tree changed to:
> npm ls sanitize-html
npm-shallow-update-repro@1.0.0 /Users/felix/src/github.com/felixfbecker/npm-shallow-update-repro
└─┬ metascraper@3.11.8
└── sanitize-html@1.18.4
Even though this would have been expected:
> npm ls sanitize-html
npm-shallow-update-repro@1.0.0 /Users/felix/src/github.com/felixfbecker/npm-shallow-update-repro
└─┬ metascraper@3.11.8
└── sanitize-html@1.18.2
because 1.18.2 is still in-range of ~1.18.2, which is the unchanged requirement: https://github.com/microlinkhq/metascraper/blob/v3.11.8/packages/metascraper/package.json#L75
If you git reset --hard HEAD && npm install, then try with npm update sanitize-html --depth 1 or --depth 0 the same happens.
Details
I tried a bunch of variations of npm update to achieve a “safe” update without indirect dependencies, passing in --depth 0 or --depth 1. But even with that flag, it would still update the indirect dependency.
I personally think that a “shallow” update should be the default for npm update (except when the new version has updated requirements of course, then update only the packages with new requirements), while a “reinstall” ala npm install pkg-a@latest would update everything. Otherwise, what’s the point of having two commands?
But I would also be okay if this behaviour could simply be controlled with --depth. In some contexts a “safe” update is more important than in others. In this one, we certainly always want to do shallow updates of pkg-a, because we release new versions every day, but don’t want to update the indirect dependencies every day. Other dependencies we update once per month, where it’s fine if it updates the indirect dependencies too (and I could just use a “reinstall” for that).
Especially automatic dependency updates through Renovate I would always expect to be shallow, because every package update is supposed to be tested in isolation on a branch.
I would expect npm to provide some way to do a shallow update.
Platform Info
$ npm --versions
{ npm: '6.3.0',
ares: '1.14.0',
cldr: '33.0',
http_parser: '2.8.0',
icu: '61.1',
modules: '64',
napi: '3',
nghttp2: '1.32.0',
node: '10.6.0',
openssl: '1.1.0h',
tz: '2018c',
unicode: '10.0',
uv: '1.21.0',
v8: '6.7.288.46-node.13',
zlib: '1.2.11' }
$ node -p process.platform
darwin