Make Sure These Node Modules Are Those You Wanted

These are not the modules you are looking for.

Sometimes, I don’t understand something, so I search the answer and sharing it is natural. Sometimes, I don’t think sharing it will be any use, until I realize that some people, even in my team, struggle with the same thing. This post is in the second category.

If you use NPM regularly, you must have noticed it adds tildes (~) or carets (^) in front of your dependencies' version number. You may also have noticed it creates a package-lock.json file. If you don’t know what any of these are, this post will shed some light.

A Cautionary Tale #

At the beginning of 2016, I joined a front-end development team and I discovered the joys1 of Node, NPM and Webpack. I was actually quite fond of the stack and made a little side project to get up to date.

Something funny occurred, though. I quite quickly detected a bug that no one else on the team had. Since only I saw that, we discarded it as a problem with my browser.

Fast forward to a few months later. The app is now released and the support team forwards a ticket with the exact same bug we had seen on my computer. So, it wasn’t a problem with my configuration, but it was specific to my computer, because I had joined the team a month after they had started working.

What’s that to do with anything? Well, with NPM, the moment you install the dependencies is not that trivial. See, NPM saves your dependencies in the package.json file, but in a manner that allows it to download downstream versions than the one declared.

This is what happened to me: for one of the dependencies, I had downloaded a later version than the one my colleagues were using. That version had introduced a bug—not easy to detect as it was a side effect for changes that shouldn’t have affected it. My computer wouldn’t have been a problem, but our CI downloaded just the same version to build the production release.

Now, figuring out a bug that appeared only on specific computers while your codebase is identical is not that obvious. I remember understanding the problem, but I can’t tell you how I thought of comparing the versions of the dependencies with my teammate.

That’s an example why you’d better take some time understanding how NPM works if you’re going to use it. Here are a few essential tips to begin with.

How You Can Declare Dependencies with NPM #

Let’s have a look at the valid version declarations for dependencies in a package.json file. From NPM’s documentation, all the following are valid:

 1{ "dependencies" :
 2  { "foo" : "1.0.0 - 2.9999.9999"
 3  , "bar" : ">=1.0.2 <2.1.2"
 4  , "baz" : ">1.0.2 <=2.3.4"
 5  , "boo" : "2.0.1"
 6  , "qux" : "<1.0.0 || >=2.3.1 <2.4.5 || >=2.5.2 <3.0.0"
 7  , "asd" : "http://asdf.com/asdf.tar.gz"
 8  , "til" : "~1.2"
 9  , "elf" : "~1.2.3"
10  , "two" : "2.x"
11  , "thr" : "3.3.x"
12  , "lat" : "latest"
13  , "dyl" : "file:../dyl"
14  }
15}

Most of those declarations are quite clear, but I’d like to focus a minute on the two following:

  • ~version “Approximately equivalent to version”
  • ^version “Compatible with version”

Examples should be clearer than any amount of text. If you need more information than the examples below, please check semver’s documentation.

Tilde Range #

Allows patch-level changes if a minor version is specified on the comparator. Allows minor-level changes if not.

  • ~1.2.3 := >=1.2.3 < 1.3.0
  • ~1.2 := >=1.2.0 < 1.3.0 := 1.2.x
  • ~1 := >=1.0.0 < 2.0.0 := 1.x

Caret Range #

Allows changes that do not modify the leftmost non-zero digit of the version.

  • ^1.2.3 := >=1.2.3 < 2.0.0
  • ^0.2.3 := >=0.2.3 < 0.3.0
  • ^0.0.3 := >=0.0.3 < 0.0.4

How You Should Declare Dependencies with NPM #

Well, I’m not in the place to tell you that, but I can give you the opinion I built based on [my experience][#tale]. In my tale, the difference was a patch, I think, and the bug was a side effect on something that was not on the release note.

My conclusion2 is that you should be extra-careful when handling your dependencies. For anything sensitive or production-ready, I’d recommend fixing the versions by avoiding the prefix in the dependencies' version. Don’t trust downstream versions until you’ve got a chance to test them, and don’t test only what the release note indicates.

In the Java world and, though Maven authorizes to declare dependencies versions as ranges, most developers I know (myself included) consider this a bad practice.

You need to your builds to be repeatable. Letting the build tool choosing on build time which version it’ll use is just incompatible with that.

If you actually don’t want NPM to add the tilde or caret prefix to the versions, you can use the following command: npm config set save-prefix=''.

A Note About the package-lock.json #

When working on my side project at the time of the [tale][#tale], I discovered Yarn. It’s an alternative to NPM that Facebook built due to limitations they faced. Many people back then were seduced because it was a hell of a lot faster to download dependencies.

But the thing that got my eye first was the yarn.lock file: when installing dependencies, Yarn creates a lockfile that tells which dependencies were effectively installed. That way, when another developer checkouts the projects and installs the modules, Yarn will look at the lockfile rather than the package.json and do the install.

Years have passed and NPM has integrated features similar to Yarn’s, including the following line when you install dependencies:

$ npm i
npm notice created a lockfile as package-lock.json. You should commit this file.

Inconsistencies between machines should no longer be a problem, now.

A teammate recently told me he added this file to the .gitignore. It makes sense for most generated files, but not this one: you should version it and share it with your team, to ensure consistent installs across machines.

No, don’t try to read or edit it. It’s for NPM, not humans. Just edit package.json and run npm i to update the lockfile, or do everything with an npm command if you know them well enough.

A Nice Tool for Experimenting #

Time to conclude this post, so I’ll just leave a funny link here: semver can help you understanding and testing your version specification through a nice dynamic interface. Just go there to play a bit: https://semver.npmjs.com/


  1. No irony here, I really enjoyed it. ↩︎

  2. I’m comforted in this view from the many comments I saw on StackOverflow while researching for this post. ↩︎