The Problem
I’ve been working with my team on a software development project, and we detected that our dependencies were really outdated. Not only we were using an old version of our front-end library (React), but we were also locked into a version of our functional programming utility package (Lodash) released more than three years ago.
This project is a great example of technical debt. For React we were missing performance improvements and new features that the Facebook team has added. In the case of Lodash we were missing several functions that have been incorporated recently that would allow us to simplify part of the codebase.
As further clarification about how this tech debt was created, since our dependencies were old, several hacks and tweaks were added on code snippets to mimic the latest functionality that was not available for us. We’ve come to the realization that this costs more to maintain than addressing the underlying issue to bring our dependencies up to date to be able to use the new features.
We, therefore, decided to address this problem and make it part of our development process. This article describes a very simple approach outlining how we reduced technical debt by managing our JavaScript dependencies. In a nutshell, the idea is to periodically execute small tasks so you can be sure you are using the latest stable versions of the modules you use.
Checking which dependencies are out of date
We are living in a world where we mostly write glue code to combine different libraries and micro-frameworks as building blocks. These building blocks (aka npm packages, or Ruby gems, or Python packages) are usually open-source projects that we decide to use for the sake of saving some time while developing.
We rely on these packages’ authors, to keep them safe (secure and with the least amount of bugs). And we are responsible, as maintainers of our own project, to update the versions that are linked to our project, which would also allow us to leverage new features.
For instance, I’ll assume you are the author of a JavaScript (any JS flavor) project created with npm on which the dependencies are listed on a package.json
file at the root of your project.
The basic command to check for outdated packages is npm outdated
. After you run this, you’ll get an output as a table with a row for each outdated dependency, and three columns:
- Current: this is the version that you have currently installed.
- Wanted: this is the version you should update the package to, based on SemVer, according to the range defined on the
package.json
file. This generally means, if you use the caret (^) on the range, a patch or minor update that is backwards compatible and thus relatively safe to update without migrating anything else in your codebase. - Latest: this is the latest stable version available. You’ll probably want to update to this major version, but you need to be careful about it because it might contain backward-incompatible changes in relation to your current codebase. This means you need to do some extra work to adapt your project to be able to use this new version that the package owner released.
A real world situation
For example, here’s a partial result of some lines after running npm outdated
on a project I am working on:
Package Current Wanted Latestd3 3.5.6 3.5.17 4.12.2babel-eslint 7.2.3 7.2.3 8.2.1react-dom 16.0.0 16.2.0 16.2.0
I am using the following dependencies defined in package.json:
{ "d3": "^3.5.6", "babel-eslint": "^7.2.3", "react-dom": "^16.0.0"}
In the first case, for d3, it is just out of date, running npm update
will bring it up to spec (v3.5.17). However, you could also attempt to upgrade to the latest major version (v4.12.2). This task will require more effort, since you’ll need to adapt your modules and charts that use d3
for the latest version on your codebase.
For the second line, babel-eslint
, is already up to date. In spite of being updated, you can optionally update to the latest version available (v8.2.1), but, again, keep in mind that it will probably need some additional changes in your codebase.
Finally, for react-dom
, running npm update
will update the package because 16.2.0 satisfies the SemVer ^16.0.0.
Keeping up to the latest stable released versions
After running npm update
, if you want to update to the latest version of a package, regardless of your version defined in the package.json
file (non-strict versioned updates, that is, to the rightmost column of the table above), the commands to do so are:
First, install the ncu
tool: npm install -g npm-check-updates
Then, you can use ncu
this way: ncu --upgrade <package-name>
Another option is to update the version number on your package.json
to the latest major version and just run npm update
again.
Notice that if you are using yarn
instead of npm
, there are equivalent commands to achieve the same, including yarn outdated
that will print a table with nice and fancy colors. By the way, if you are not familiar with yarn
yet, you should give it a try, it is a reliable and fast npm
replacement.
With great power comes great responsibility
You are accountable for checking bugs in the libraries that you are using on your projects. Also, you should be aware of the latest versions, to understand what potential features you might be missing.
Github has introduced security alerts (for JavaScript and Ruby projects for now, but they are planning to increase support for another language in the future). This means, when a vulnerability is detected in any of you packages of you dependency graph, you’ll receive a notification and a suggested fix mentioning what version you should update to.
I’d also suggest keeping an eye on the news, the npm ecosystem is still quite fragile (did anyone say left-pad?) and sometimes your updates can trigger cryptic errors for no apparent reason. If you are having trouble installing dependencies check for the npm github repository. Usually, issues are reported quite fast over there. This one is also an interesting (and sarcastic) read that portrays the chaotic status of npm packages regarding their security: I’m harvesting credit card numbers and passwords from your site. Here’s how.
Make it a process
Lastly, I would suggest including this as part of your regular processes that you execute to keep your software running. If you haven’t updated your dependencies for a long time, you should do an assessment first, using npm outdated
as described and estimate what dependencies will require the biggest effort to update. After that, you can create tasks to upgrade these packages and add them to your technical debt backlog, you do have a tech debt backlog, right?
Another alternative is to create a task in your issue tracker to review your dependencies and update them all together, in a recurrent way. Maybe once per month or once per sprint if you are using some kind of agile methodology.
When a major version changes, the API usually changes as well. Therefore your tests will probably break. I’d also suggest running all your test cases after updating dependencies, especially if you are updating to a new major version because if you don’t do so, you might be using a slate API on your test suites. This is a recommended place to start applying changes to your codebase right after you update to a new version, to gradually start fixing any broken tests.
If you need to update to a new major version of a package, always look for the CHANGELOG.md
or HISTORY.md
files. That would allow you to understand what was released and what are the breaking changes that were introduced by the maintainers of the project. Another piece of advice: if you are coding a front-end app, it might be useful to explore the output of your JavaScript console on the browser to check if you have new warnings. In some cases, warnings are added by certain libraries to communicate that in the future some of the APIs you are using are going to be deprecated (for example, React does this on PropTypes).
TL;DR
- Use
npm outdated
oryarn outdated
or a similar tool based on the technology you are using to be able to grasp your situation about out-of-date dependencies linked to your software. - Make the process to update dependencies a task within your project management system and assign it to a member of the team each time it needs to be done.
- After updating your dependencies, run your entire test suite to check that all tests pass and everything is still working.
- When updating to a new major version, look at the project’s documentation to understand the breaking changes that were introduced.
- Watch out for potential security vulnerabilities reported in the packages that you are using.