npm-shrinkwrap Sucks
Posted on Tue 03 May 2016 in JavaScript
Despite suggesting that you should always shrinkwrap your npm dependencies, I've come to the conclusion that npm-shrinkwrap not only sucks, but is fundamentally broken. It doesn't quite suck as much as having your dependencies update underneath your feet, but it still sucks...
Adding new dependencies is a pain
Adding new dependencies to your project without npm-shrinkwrap is pretty straight forward:
- Run
npm install --save-dev left-pad
- Raise a pull request with a 1 line change.
Performing the same task with a shrinkwrap.json
file present is a pain...
- Run
npm install --save-dev left-pad
- Run
npm prune
to de-dupe the dependency graph incase one of your existing modules made use ofleft-pad
somewhere otherwisenpm shrinkwrap
will fail - Run
npm shrinkwrap --dev
to update the shrinkwrap file - Raise a pull request, note that hte package.json file has a tonne of unreadable and unexpected changes - enjoy the fact that github refuses to render the diff on the shrinkwrap file...
Sure you could look to use uber's npm-shrinkwrap tool but that's yet another dependency to add.
It's not obvious when things are broken
So this one is not npm-shrinkwrap's fault, but it caused my team to lose some productivity today: our project makes use of 4 (count em!) package.json files, each of which has their own shrinkwrap file - we use these to lock the dependency graph and have a weekly CI task to update everything using npm-check-updates. Somehow one of the npm-shrinkwrap.json
files went AWOL; everything carried on working as expected for about two weeks but a chunk of the project's dependencies were being updated with each build. We only found out when a bug in ESLint caused our Pull Request builds to start failing which triggered the obvious question of "Why the hell did ESLint update automatically?!".
So what's the solution?
A co-worker of mine suggested the blindingly obvious approach of just using explicit versions in the package.json
file, so instead of declaring:
dependencies: {
"left-pad": "^1.0.2"
}
You would instead declare:
dependencies: {
"left-pad": "1.0.2"
}
That's it. npm provides the -E, --save-exact
flag to do this for you when installing with --save
or --save-dev
. npm-check-updates works as expected (suggesting newer versions) and your pull request diffs go back to a single line. To prevent developers accidentally committing a "ranged" dependency we've added the following pre-flight check as part of our CI process:
grep \"[~^] package.json
if [[ $? != 1 ]]; then
echo "Non-exact dependency version detected, install with --save-exact"
exit 1
fi
Having written this, I'm not sure I see any benefits of using npm shrinkwrap
instead of enforcing exact dependency versions in your package.json
(and using a tool to manually update them on a controlled cadence).
Update (4th May 2016)
After posting this I got some great feedback in the comments and on /r/javascript - using exact versions in the your project's package.json
does nothing for your project's transitive dependencies which probably don't make use of exact versions and therefore will be no predictably resolved from one npm install
to the next. To address this I've re-instated npm-shrinkwrap
back into my project (it still sucks), but I've also modified our pre-flight check script to check that the shrinkwrap files are present (and haven't gone wondering off) - combining this with exact versions has made things a little easier to reason about).