Understanding npm package upgrades

Understanding npm package upgrades

I maintain a small task management app called Attainment that I used to teach myself more about front-end javascript development. It uses the Vue.js framework and I use npm to manage my upstream dependencies.

I don’t get much consistent time to hack on it, so it’s usually several months in between development cycles. When I come back to the project, I’ve invariably found myself asking “what’s changed and what should I update?” when it comes to my dependencies. I’ve frequently found myself wondering what the difference is between npm upgrade and npm update and how do I figure out what dependencies are out of date in my web projects. This post is my attempt to learn the best ways to manage this and hopefully it will help others as well.

What’s the status?

My first question is “how do I know where I stand”. I’d like to get a listing of things that are currently out of date. The command to do that is npm outdated. Here’s what it looks like against Attainment after a long period of neglect:

coldwd at thoreau in ~/src/github.com/dencold/attainment-web on master
$ npm outdated
Package                 Current  Wanted         Latest  Location
@vue/cli-plugin-babel     3.0.4   3.7.0          3.7.0  attainment-vue
@vue/cli-plugin-eslint    3.0.4   3.7.0          3.7.0  attainment-vue
@vue/cli-service          3.0.4   3.7.0          3.7.0  attainment-vue
bootstrap                 3.3.7   3.4.1          4.3.1  attainment-vue
chartist                 0.10.1  0.10.1         0.11.0  attainment-vue
firebase                  5.5.2  5.11.1         5.11.1  attainment-vue
fuse.js                   3.2.1   3.4.4          3.4.4  attainment-vue
moment                   2.22.2  2.24.0         2.24.0  attainment-vue
node-sass                 4.9.3  4.12.0         4.12.0  attainment-vue
vue                      2.5.17  2.6.10         2.6.10  attainment-vue
vue-datetime              0.7.1   0.7.1  1.0.0-beta.10  attainment-vue
vue-fuse                  1.5.2   1.5.2          2.0.2  attainment-vue
vue-js-modal             1.3.26  1.3.31         1.3.31  attainment-vue
vue-moment                3.2.0   3.2.0          4.0.0  attainment-vue
vue-router                3.0.1   3.0.6          3.0.6  attainment-vue
vue-template-compiler    2.5.17  2.6.10         2.6.10  attainment-vue
vuex                      3.0.1   3.1.0          3.1.0  attainment-vue

This gives us the dependency in the left most column, followed by the version currently installed, the one we want, and the latest available version upstream. Why is it that sometimes what we want is not the latest version listed? For example look at the details for bootstrap from that output:

Package                 Current  Wanted         Latest  Location
bootstrap                 3.3.7   3.4.1          4.3.1  attainment-vue

The latest version is 4.3.1, but we want 3.4.1. What’s up with that? Well, npm understands semver and we have the “dependencies” section of our package.json file defined as:

  "dependencies": {
    "bootstrap": "^3.4.1",
    "chartist": "^0.10.1",
    "firebase": "^5.11.1",
    "fuse.js": "^3.4.4",
    "moment": "^2.24.0",
    "vue": "^2.6.10",
    "vue-clickaway": "^2.2.2",
    "vue-datetime": "^0.7.1",
    "vue-fuse": "^1.5.2",
    "vue-js-modal": "^1.3.31",
    "vue-moment": "^3.2.0",
    "vue-router": "^3.0.6",
    "vuex": "^3.1.0"
  },

Notice that bootstrap is configured with version ^3.4.1 this tells npm that we can accept any patch or minor version updates (e.g. anything within the 3.0.0 release) safely. So in the output of npm outated, above, bootstrap’s latest version is 4.3.1, but we’ll only accept the latest in the 3.0.0 line, which is 3.4.1.

Let’s update!

Now that we know what is out of date, and we know that npm is (hopefully) protecting us from any breaking changes in a major version change, let’s update our dependencies. We do this using npm update:

coldwd at thoreau in ~/src/github.com/dencold/attainment-web on master
$ npm update
npm WARN deprecated joi@14.3.1: This module has moved and is now available at @hapi/joi. Please update your dependencies as this version is no longer maintained an may contain bugs and security issues.
npm WARN deprecated topo@3.0.3: This module has moved and is now available at @hapi/topo. Please update your dependencies as this version is no longer maintained an may contain bugs and security issues.
npm WARN deprecated hoek@6.1.3: This module has moved and is now available at @hapi/hoek. Please update your dependencies as this version is no longer maintained an may contain bugs and security issues.

> grpc@1.20.0 install /home/coldwd/src/github.com/dencold/attainment-web/node_modules/grpc
> node-pre-gyp install --fallback-to-build --library=static_library

node-pre-gyp WARN Using request for node-pre-gyp https download 
[grpc] Success: "/home/coldwd/src/github.com/dencold/attainment-web/node_modules/grpc/src/node/extension_binary/node-v67-linux-x64-glibc/grpc_node.node" is installed via remote

> node-sass@4.12.0 install /home/coldwd/src/github.com/dencold/attainment-web/node_modules/node-sass
> node scripts/install.js

Downloading binary from https://github.com/sass/node-sass/releases/download/v4.12.0/linux-x64-67_binding.node
Download complete..] - :
Binary saved to /home/coldwd/src/github.com/dencold/attainment-web/node_modules/node-sass/vendor/linux-x64-67/binding.node
Caching binary to /home/coldwd/.npm/node-sass/4.12.0/linux-x64-67_binding.node

> node-sass@4.12.0 postinstall /home/coldwd/src/github.com/dencold/attainment-web/node_modules/node-sass
> node scripts/build.js

Binary found at /home/coldwd/src/github.com/dencold/attainment-web/node_modules/node-sass/vendor/linux-x64-67/binding.node
Testing binary
Binary is fine
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})

+ fuse.js@3.4.4
+ bootstrap@3.4.1
+ moment@2.24.0
+ @vue/cli-plugin-babel@3.7.0
+ @vue/cli-plugin-eslint@3.7.0
+ node-sass@4.12.0
+ vue-js-modal@1.3.31
+ @vue/cli-service@3.7.0
+ vuex@3.1.0
+ vue-template-compiler@2.6.10
+ vue-router@3.0.6
+ vue@2.6.10
+ firebase@5.11.1
added 151 packages from 368 contributors, removed 280 packages, updated 380 packages, moved 33 packages and audited 24396 packages in 53.893s
found 1 vulnerability (1 high)
  run `npm audit fix` to fix them, or `npm audit` for details

That was…more output than I expected. Let’s break that down.

Deciphering update’s output

The first section of output:

npm WARN deprecated joi@14.3.1: This module has moved and is now available at @hapi/joi. Please update your dependencies as this version is no longer maintained an may contain bugs and security issues.
npm WARN deprecated topo@3.0.3: This module has moved and is now available at @hapi/topo. Please update your dependencies as this version is no longer maintained an may contain bugs and security issues.
npm WARN deprecated hoek@6.1.3: This module has moved and is now available at @hapi/hoek. Please update your dependencies as this version is no longer maintained an may contain bugs and security issues.

That’s telling me that I need to change the joi dependency to @hapi/joi, but the joi doesn’t appear anywhere in my package.json file. The answer is to use the npm ls command:

coldwd at thoreau in ~/src/github.com/dencold/attainment-web on master
$ npm ls joi
attainment-vue@0.1.0 /home/coldwd/src/github.com/dencold/attainment-web
└─┬ @vue/cli-plugin-babel@3.7.0
  └─┬ @vue/cli-shared-utils@3.7.0
    └── joi@14.3.1 

So, the @vue/cli-shared-utils package depends on joi, and the @vue/cli-shared-utils itself is a dependency of @vue/cli-plugin-babel. We can see this captured in the package-lock.json file:

    "@vue/cli-shared-utils": {
      "version": "3.7.0",
      "resolved": "https://registry.npmjs.org/@vue/cli-shared-utils/-/cli-shared-utils-3.7.0.tgz",
      "integrity": "sha512-+LPDAQ1CE3ci1ADOvNqJMPdqyxgJxOq5HUgGDSKCHwviXF6GtynfljZXiSzgWh5ueMFxJphCfeMsTZqFWwsHVg==",
      "dev": true,
      "requires": {
        "chalk": "^2.4.1",
        "execa": "^1.0.0",
        "joi": "^14.3.0",
        "launch-editor": "^2.2.1",
        "lru-cache": "^5.1.1",
        "node-ipc": "^9.1.1",
        "opn": "^5.3.0",
        "ora": "^3.4.0",
        "request": "^2.87.0",
        "request-promise-native": "^1.0.7",
        "semver": "^6.0.0",
        "string.prototype.padstart": "^3.0.0"
      },
      "dependencies": {
        "semver": {
          "version": "6.0.0",
          "resolved": "https://registry.npmjs.org/semver/-/semver-6.0.0.tgz",
          "integrity": "sha512-0UewU+9rFapKFnlbirLi3byoOuhrSsli/z/ihNnvM24vgF+8sNBiI1LZPBSH9wJKUwaUbw+s3hToDLCXkrghrQ==",
          "dev": true
        }
      }
    },

Buuuuuttttt, where the hell does the root package @vue/cli-plugin-babel come from, you may be asking yourself. It also wasn’t listed in the package.json. That’s because I left out the devDependencies section of package.json:

  "devDependencies": {
    "@vue/cli-plugin-babel": "^3.7.0",
    "@vue/cli-plugin-eslint": "^3.7.0",
    "@vue/cli-service": "^3.7.0",
    "node-sass": "^4.12.0",
    "sass-loader": "^7.0.1",
    "vue-template-compiler": "^2.6.10"
  }

So, I’m getting a warning for a package that is depended by another package that itself is a dependency for the original package that was actually listed as a project dependency. This is a light version of what craziness in package management commonly known as dependency hell in js development.

So, now we’ve figured out where these warnings are coming from, what do we do about them? The answer is…nothing. According to the project maintainers, these are safe to ignore and are the result of transient dependencies that they don’t have control over. ¯\_(ツ)_/¯

The next bit of output that is actually helpful is this one:

+ fuse.js@3.4.4
+ bootstrap@3.4.1
+ moment@2.24.0
+ @vue/cli-plugin-babel@3.7.0
+ @vue/cli-plugin-eslint@3.7.0
+ node-sass@4.12.0
+ vue-js-modal@1.3.31
+ @vue/cli-service@3.7.0
+ vuex@3.1.0
+ vue-template-compiler@2.6.10
+ vue-router@3.0.6
+ vue@2.6.10
+ firebase@5.11.1

These are acutal updates that were performed on our dependencies, you’ll see that it matches up with what we were expecting, above. For example, that bootstrap dependency we made a fuss about has indeed been updated to 3.4.1. Awesome.

Audit checks

If you were paying close attention, you probably noticed a pretty important note at the end of the output:

found 1 vulnerability (1 high)
  run `npm audit fix` to fix them, or `npm audit` for details

This is important and highlights security vulnerabilities that should be taken care of ASAP. Let’s do as the update recommends and run npm audit and see what’s going on here:

coldwd at thoreau in ~/src/github.com/dencold/attainment-web on master
$ npm audit
                                                                                
                       === npm audit security report ===                        
                                                                                
┌──────────────────────────────────────────────────────────────────────────────┐
│                                Manual Review                                 │
│            Some vulnerabilities require your attention to resolve            │
│                                                                              │
│         Visit https://go.npm.me/audit-guide for additional guidance          │
└──────────────────────────────────────────────────────────────────────────────┘
┌───────────────┬──────────────────────────────────────────────────────────────┐
│ High          │ Arbitrary File Overwrite                                     │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Package       │ tar                                                          │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Patched in    │ >=4.4.2                                                      │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Dependency of │ node-sass [dev]                                              │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Path          │ node-sass > node-gyp > tar                                   │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ More info     │ https://npmjs.com/advisories/803                             │
└───────────────┴──────────────────────────────────────────────────────────────┘
found 1 high severity vulnerability in 24396 scanned packages
  1 vulnerability requires manual review. See the full report for details.

So we have a problem with the tar package, specifically any version before 4.4.2. The audit tool lets us know that tar is a dependency of node-sass which is in our devDependencies of our package.json file. Let’s confirm this using the npm ls command:

coldwd at thoreau in ~/src/github.com/dencold/attainment-web on master
$ npm ls tar
attainment-vue@0.1.0 /home/coldwd/src/github.com/dencold/attainment-web
├─┬ firebase@5.11.1
│ └─┬ @firebase/firestore@1.2.2
│   └─┬ grpc@1.20.0
│     └─┬ node-pre-gyp@0.12.0
│       └── tar@4.4.8 
└─┬ node-sass@4.12.0
  └─┬ node-gyp@3.8.0
    └── tar@2.2.1 

So we have dependencies on tar in two places: firebase which is already on a version that includes the fix, and node-sass which mathes what we saw in the audit results. We can also use the npm view command to check out what the npm registry knows about the offending package:

coldwd at thoreau in ~/src/github.com/dencold/attainment-web on master
$ npm view node-sass

node-sass@4.12.0 | MIT | deps: 17 | versions: 135
Wrapper around libsass
https://github.com/sass/node-sass

keywords: css, libsass, preprocessor, sass, scss, style

bin: node-sass

dist
.tarball: https://registry.npmjs.org/node-sass/-/node-sass-4.12.0.tgz
.shasum: 0914f531932380114a30cc5fa4fa63233a25f017
.integrity: sha512-A1Iv4oN+Iel6EPv77/HddXErL2a+gZ4uBeZUy+a8O35CFYTXhgA8MgLCWBtwpGZdCvTvQ9d+bQxX/QC36GDPpQ==
.unpackedSize: 1.8 MB

dependencies:
async-foreach: ^0.1.3  glob: ^7.0.3           nan: ^2.13.2           stdout-stream: ^1.4.0  
chalk: ^1.1.1          in-publish: ^2.0.0     node-gyp: ^3.8.0       true-case-path: ^1.0.2 
cross-spawn: ^3.0.0    lodash: ^4.17.11       npmlog: ^4.0.0         
gaze: ^1.0.0           meow: ^3.7.0           request: ^2.88.0       
get-stdin: ^4.0.1      mkdirp: ^0.5.1         sass-graph: ^2.2.4     

maintainers:
- am11 <adeelbm@outlook.com>
- andrewnez <andrewnez@gmail.com>
- saperski <npm@saper.info>
- xzyfer <xzyfer@gmail.com>

dist-tags:
beta: 4.11.0    latest: 4.12.0  next: 4.8.3     

published a week ago by xzyfer <xzyfer@gmail.com>

From this, we can see that we are indeed on on the latest version 4.12.0. The output also lists the project’s homepage which is on github, checking out it’s issues, I found issue 2625 which breaks down what the package maintainers are doing to deal with this. They are waiting for their dependency, node-gyp, to fix it for them.

Dependency hell indeed. Ughhhh.

Checking up

Okay, after all those segues, it might be hard to remember that we did actually run a successful update. What do things look like now? If we run npm outdated again, here’s what the results look like:

coldwd at thoreau in ~/src/github.com/dencold/attainment-web on master*
$ npm outdated
Package       Current  Wanted         Latest  Location
bootstrap       3.4.1   3.4.1          4.3.1  attainment-vue
chartist       0.10.1  0.10.1         0.11.0  attainment-vue
vue-datetime    0.7.1   0.7.1  1.0.0-beta.10  attainment-vue
vue-fuse        1.5.2   1.5.2          2.0.2  attainment-vue
vue-moment      3.2.0   3.2.0          4.0.0  attainment-vue

Exactly as expected, all that’s left are the packages that have major updates left. Nice! Running a git status we see:

coldwd at thoreau in ~/src/github.com/dencold/attainment-web on master*
$ git status
On branch master
Your branch is up to date with 'origin/master'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   package-lock.json
	modified:   package.json

no changes added to commit (use "git add" and/or "git commit -a")

…and diffing package.json:

coldwd at thoreau in ~/src/github.com/dencold/attainment-web on master*
$ git d package.json

 package.json | 26 +++++++++++++-------------
 1 file changed, 13 insertions(+), 13 deletions(-)

diff --git a/package.json b/package.json
index 6bb946c..3382fba 100644
--- a/package.json
+++ b/package.json
@@ -9,26 +9,26 @@
     "lint": "vue-cli-service lint"
   },
   "dependencies": {
-    "bootstrap": "^3.3.7",
+    "bootstrap": "^3.4.1",
     "chartist": "^0.10.1",
-    "firebase": "^5.5.2",
-    "fuse.js": "^3.2.1",
-    "moment": "^2.22.2",
-    "vue": "^2.5.17",
+    "firebase": "^5.11.1",
+    "fuse.js": "^3.4.4",
+    "moment": "^2.24.0",
+    "vue": "^2.6.10",
     "vue-clickaway": "^2.2.2",
     "vue-datetime": "^0.7.1",
     "vue-fuse": "^1.5.2",
-    "vue-js-modal": "^1.3.16",
+    "vue-js-modal": "^1.3.31",
     "vue-moment": "^3.2.0",
-    "vue-router": "^3.0.1",
-    "vuex": "^3.0.1"
+    "vue-router": "^3.0.6",
+    "vuex": "^3.1.0"
   },
   "devDependencies": {
-    "@vue/cli-plugin-babel": "^3.0.4",
-    "@vue/cli-plugin-eslint": "^3.0.4",
-    "@vue/cli-service": "^3.0.4",
-    "node-sass": "^4.9.0",
+    "@vue/cli-plugin-babel": "^3.7.0",
+    "@vue/cli-plugin-eslint": "^3.7.0",
+    "@vue/cli-service": "^3.7.0",
+    "node-sass": "^4.12.0",
     "sass-loader": "^7.0.1",
-    "vue-template-compiler": "^2.5.17"
+    "vue-template-compiler": "^2.6.10"
   }
 }

We can see the changes correctly accounted for.

So…what’s the difference between update and upgrade?

Nothing! npm upgrade is an alias for npm update. As seen from the help output:

$ npm -h update
npm update [-g] [<pkg>...]

aliases: up, upgrade, udpate

However, there is, confusingly enough, an npm-upgrade which is an indpendent package that gives you an interactive mode, with changelog support.

Helpful resources

I found these articles/posts very helpful in writing up my findings:

Photo by Brandon Green on Unsplash