At some point you will face a valid reason to upgrade dependencies within JS project - a deprecation, end of support or version mismatch between different libraries.
Unfortunately, most cases of upgrading deps result in an unexpected cascade of issues.
Let’s take a moment and think how to make this more manageable and hopefully more pleasant experience.
Semantic version meaning and installation strategies
For starters, let’s quickly review what each version number is supposed to mean in semantic versioning:
- Major version bump means incompatible API changes a.k.a. BREAKING CHANGE
- Minor version bump means addition of functionality in a backwards compatible manner
- Patch version bump means backwards compatible bug fixes
This is a perfect world definition, but sometimes in reality even a minor version can introduce a breaking change.
You can specify which update types your package can accept from dependencies in your package.json file.
For example, to specify acceptable version ranges up to 1.0.4, use the following syntax:
- Patch releases: 1.0 or 1.0.x or ~1.0.4
- Minor releases: 1 or 1.x or ^1.0.4 (this is default, behaviour)
- Major releases: * or x
Find your inner minimalist
Prevention is the best cure
Do not install a dependency, unless it’s necessary. Sometimes it’s better to stick to language standards or even consider writing your own solution for small changes.
Before installing anything new to the package check number of stars, downloads and when the last update occurred.
Audit your project from time to time. We work in dynamic environments and sometimes the fastest route to accomplish a task, is to install something quick, and with time it becomes stale. As a maintainer it’s a good practise to go through your dependencies and do a major cleaning. Remove anything that is unused, but be careful and check twice before doing so.
Every time you add something strange e.g. a lib in a certain version as a workaround, leave some sort of explanation ( e.g. inside README file) to your team and future self.
The fewer deps installed, the less there is to maintain in the future and possibly fewer conflicts will occur.
If the dep you are using gets deprecated or not supported, simply create your own fork with fixes.
Use peer deps and dedupe
For legacy projects sometimes there is no other way, then to install previous versions of some peer-package, so you can:
npm install --legacy-peer-deps
Removes all duplication of the same peer-package:
npm dedupe --legacy-peer-deps
Uncontrolled upgrades
Using ^~* modifiers can sometimes result with your project “magically” stop working, as some breaking changes can be introduced silently.
To prevent such uncontrolled behaviour, I strongly advise installing exact versions of dependencies.
npm install --save-exact package-name
It disables automatic patch updates, but can also prevent lots of unwanted headaches.
Control your upgrade process
Come up with software dependency management policy within your organization
Keep your team organized, by establishing and spreading the rules for upgrading dependencies. You can formalize a time frame, a specific scenerio which triggers update process as well as one which forbids one.
Useful tools
To speed up audit process I suggest using a tool like npm-check-updates. It allows to quickly list and update deprecated packages.
For legacy or long-lived projects I like to use my custom script, which makes sure a clean install (without package-lock) is performed:
reset="\033[m"
tput setaf 3; read -p "This will remove node_modules and package-lock.json files. Are you sure? (y/N) " -n 1 -r
echo "${reset}"
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo "[1/4] Removing previous node_modules and package-lock.json"
rm -rf node_modules package-lock.json
echo "Done."
echo "[2/4] Installing with legacy peer deps"
npm i --legacy-peer-deps
echo "Done."
echo "[3/4] Deduping..."
npm dedupe --legacy-peer-deps
echo "Done."
echo "[4/4] Running tests"
npm run test:coverage
echo "Done."
fi
It allows me to speed up process of reinstalling everything and triggers testing at the end.
I strongly recommend to update your dependencies one by one and have total control of what has changed and how, as most breaking changes still require introducing codemods (best case) or lots of modifications by hand.
Debugging conflicts and difficult cases
Maybe, the error you got after an upgrade comes from a dep, you have not installed on your own, but it comes as a peer dep of some other package.
Most of the time, the stack trace will show a problematic package name. If that’s the case use npm ls package-name
to
make sure what exact version is installed. Then try to read through a changelog or readme or migration guide of that
package. If it’s not helpful in any way, you should reach out a creator and issue a bug. In the meantime add this
package with a specific version that it used work properly and install with our script.
Summary
- Be frugal with additional deps installations.
- Update/remove one dep at the time, for better control.
- Make conscious decision about automatic updates.