Craig Glennie

In this post I’m going to explore package-lock.json, which is created by modern versions of npm when you run npm install. I’ve written this to clarify my understanding of how the lock file works.

So what is it for?

The main purpose of the lock file is to enable reproducible builds, by locking the exact structure of your dependency tree, and also the exact version of every dependency (and their dependencies, and their dependencies, and so on). When installing packages npm will install packages that exactly match the lock file, unless they’re trumped by package.json (more on that later).

Why is it necessary?

Mostly because of range versions in semver. A range version is a version identifier that sets rules about what version of a package can be installed, rather than specifying the exact version. The most common range version is something like ^1.0.0, which means “install a minimum of version 1.0.0, but prefer any higher minor or patch versions, but don’t upgrade the major version”. You’ll have seen this, it’s the default npm adds to package.json when you npm install a package. Thus, installing with ^1.0.0 might get you version 1.2.3 of a package, but would not get you version 2.0.

This can become a problem with sub-dependencies (dependencies of a package that you depend on directly in package.json) and their dependencies. Imagine a dependency version tree like this, where the top level is dependencies listed in my package.json:

    "my-dependency": "1.0.2": {
        "sub-dependency": "~2.0"

In the above example I’ve pinned the exact version of my-dependency that I want to 1.0.2. But I can’t control the dependency versions that my-dependency declares for itself. If I run npm install today I might get sub-dependency 2.0. But then tomorrow sub-dependency releases version 2.1. If my colleague (or my build system) runs npm install after that release then they would get version 2.1 in their system. That might be fine - unless 2.1 introduced a bug or changed some behaviour. It’s particularly problematic for automated builds; it’s one thing for a developer to locally fix an issue caused by a version change, but it’s another thing to have your build system randomly break (maybe tests started failing with the new version) and you have to try to figure out why.

What if I want to use range versions in my package.json?

As of npm v5.2.0 you can use range versions in package.json: the version specifier in package.json trumps the version in package-lock.json. This was done because the previous behaviour was causing a lot of confusion; people expected package.json to be honored at all times, and were surprised that package-lock.json could override it.

What if I don’t want package.json to trump package-lock.json?

There’s a new command for this: npm ci (instead of npm install). This command installs packages strictly from package-lock.json, and ignores any new packages that could have been installed by the rules in package.json. As the name hints, this command is intended for your CI system: automated builds should exactly reproduce what’s committed in the package-lock.json. Use npm install locally / when manually installing packages, and npm ci for anything that’s automated.

npm tells me to commit package-lock.json, should I?

Yes. You need to commit package-lock.json whenever it changes, so that it is an up-to-date reference for your automated tools (which are running npm ci; see above) to use.

I have a PR and I can’t understand the diff of package-lock.json, what should I do?

Ignore it, probably. package-lock.json is for machines to understand, not people. npm claims the diffs are human-readable, but it still takes some effort to understand. I say don’t try to reconcile a complex diff manually, you’ll do your head in. If package-lock.json has changed in a PR it is equivalent to the developer saying “these are the new blessed dependencies for this project”. If you trust the developer you can probably accept the changes.

I’ve got a merge conflict with package-lock.json, what should I do?

Fortunately there’s tooling to help with this. Your options are:

  1. Tell npm to resolve the issue for you, or
  2. Install a helper that will do it automatically.

Either way it’s pretty easy