Upgrading dependencies strategy for Node.js project

Considerations to upgrade dependent packages when creating an application in Node.js.
2020.04.16

This is an English version of this article

TL;DR

Three points to upgrade dependencies when building an application.

  1. Fix the versions of dependencies / devDependencies.
  2. Upgrade the dependent packages every month.
  3. Use npm-check-updates to upgrade dependent packages.

What is the dependencies for Node.js Project?

It is described in dependencies and devDependencies of package.json.

dependencies specifies dependencies required for the package to run, and devDependencies specifies dependencies required for developing the package. devDependencies will not be installed if the package is installed as a dependency of any packages.

Specifying the version of the dependent package

In dependencies and devDependencies, the caret ^ U+005E is given at the beginning when a new dependency package is added without specifying any options, respectively, like followings.

// snip

  "devDependencies": {
    "typescript": "^3.8.3",
  },

// snip

For versions, Semantic Versioning has been adopted.

The leading caret indicates "Install the current latest version with the same number on the left". The specific implications can be easily understood by looking at the following table

Notation Meanings
^1.3.4 latest version less than 2.0.0
^0.3.2 latest version less than 0.4.0
^0.0.4 latest version less than 0.0.5
^2.1.4-beta.3 latest version less than 3.0.0
^0.0.3-beta.3 latest version less than 0.0.4

You can also add a tilde ~ U+007E, which says "Install the current, latest version with the same minor version". If no minor version is specified, e.g. ~1, it means the latest version with the same major version.

There are other ways to specify the version, but if you know these two types, there is no problem.

Fixing versions

As mentioned above, if you don't care about it, you'll end up with a wide range of package versions to be installed. This may cause different versions to be installed between team members, and your product may behave differently for different members.

As a mechanism to prevent this, npm provides package-lock.json and yarn provides yarn.lock.

These are generated to fix the structure of node_modules when running npm install or yarn, respectively, and are shared by team members by management on Git. This prevents being installed different versions of dependent packages between members.

Fixing version explicitly

Although package-lock.json and yarn.lock fix the version of the dependent package, I personally believe that the version written in package.json should be fixed.

  • It makes us become aware of the choices
  • It isolates the confirmation when the version lock file is regenerated.

It makes us become aware of the choices

At the very least, you should be aware of what features and vulnerabilities there are for the packages that your project directly relies on. The project will be beyond your control if you don't know what features and bugs are being added and what to do with them.

Of course, any version of the dependency package can be used for verification and other projects that are discarded on the spot.

It isolates the confirmation when the version lock file is regenerated.

As described below, deleting version lock files allows you to upgrade dependent packages to the latest versions in a batch. In this case, the unintended disruptive changes caused by the upgrade of a directly dependent package will be unknown.

Methods to fix version explicitly

Remove ^ or ~ if it already exists in the version specification of dependencies / devDependencies.

If you want to fix the version on a new installation, you may want to use the following options

Package manager Option
npm --save-exact
yarn --exact

Use these as follows.

npm install --save-exact react
// or
yarn add --exact react

When to upgrade the version

When a dependent package is upgraded, it should be followed and used in its version, as its contents are generally more sophisticated. Here we will discuss when you should upgrade.

When a vulnerability is found in a dependent package

There's a feature on GitHub called Security alerts, which you can usually use. However, you have to agree to let GitHub inspect your code, so don't use it if you're worried about leaking information.

As a way to manually look for vulnerabilities, npm provides npm audit / yarn provides yarn audit. If you hit these commands at hand, you'll see dependent packages that have vulnerabilities, so upgrade accordingly.

Every 1 month

Regularly upgrade dependent packages to add features and increase efficiency of processing content. Considering the speed at which npm packages are developed these days, it's a good idea to upgrade about every month. Too often, it's too much of a burden on the developer and won't last long, and over a longer span of time, many disruptive changes have to be considered at the same time, making upgrades a major undertaking.

You may want to discuss this with your team members and decide on a week to make the upgrade.

Upgrading versions

Ways to upgrade verions provided by framework

Some frameworks may provide its own upgrade methods.

Case: create-react-app

You can find [how to upgrade] (https://github.com/facebook/create-react-app/blob/master/CHANGELOG.md#migrating-from-340-to-341) for each version.

Case: React Native

A [dedicated tool] (https://react-native-community.github.io/upgrade-helper/) is provided.

You can specify the current version and which version you want to upgrade to to show your work.

Case: Vue.js

You can also find the [how to upgrade page] (https://cli.vuejs.org/migrating-from-v3/).

Removing and regenerating version lock files

You should do this first during regular upgrades.

rm -f package-lock.json
npm i
// or
rm -f yarn.lock
yarn

Dependent packages are now up to date. Depending on the specification of package.json, even if your upgrade is supposed to be backward compatible, you should make sure that it passes the various tests and check the operation manually.

Backward compatibility is very difficult to maintain, because the dependent packages may contain unintentionally destructive changes.

npm-check-updates

npm-check-updates is a tool that compares the current version used by the project with the latest version published in npm and prints out the differences. Use as following

npx npm-check-updates

The results are as follows.

$ npx npm-check-updates
npx: installed 235 in 11.497s
Checking /Users/takagi.kensuke/work/dev/classmethod/oss/liff-app-template/package.json
[====================] 8/8 100%

 html-webpack-plugin   3.2.0  →   4.2.0
 typescript            3.7.5  →   3.8.3
 webpack              4.41.6  →  4.42.1

Run ncu -u to upgrade package.json

You can change all packages to the latest version by specifying the -u option.

However, if there are many changes, it is recommended to upgrade each related package group and check its operation. Let's upgrade with granularity, for example, test packages, development packages, and runtime dependencies.

Don't forget to run npm i or yarn after the version change.

peerDependencies

Basically, dependencies should be the latest version, but they should satisfy peerDependencies too. This is used to specify the version of the package to work with, and the version of the main package that the plugin or other tool runs on.

When upgrading to the latest version, such as npm-check-updates, make sure to upgrade both plugins and the main package at the same time. Also, if a violation of peerDependencies is found by npm i / yarn after the latest upgrade, drop the version.

Refraining from using dependent upgrade tools.

There are some things out there that will detect changes when a dependent package is upgraded and generate a pull request, but you should refrain from using them. It is inevitable that you will have to upgrade dependent packages in any phase, and the pull requests created by those tools will get in the way.

At the very least, you should be aware of the upgrade.

Summary

Think of this upgrade strategy as just one way of doing things. This is a fairly safe operation, such as fixing the direct dependency package version.

Also keep in mind that the circumstances are different when creating a library, not an application. This is because the intended user is completely different from the application.