package-lock.json: The Complete Guide

What is package-lock.json? And why should you care?

Atharva Kulkarni
Pavesoft
5 min readAug 22, 2022

--

What is package.json?

package.json is a versioning file that primarily contains the list of dependencies (libraries) your node.js project needs to run.

It also includes other meta information like scripts, author & license information, description, project properties etc.

package.json

An Inherent Problem

In the above package.json, you can see that the “dependencies” object maps package-name to the version range.

The dependencies object contains the package name mapped to the version range

Dependencies and/or peer dependencies generally have a version range specified in the package.json file, not the exact version range.

This makes npm install non-deterministic. So, when you run npm install today, and then you run it again after 3 months, you may not end up with the same node_modules tree.

Moreover, if another developer clones your project, and runs npm install on it a few days later, they may have a different node_modules dependency tree. When multiple developers are working on the same repository (which is most likely the case in every organization), this might pose a big problem and lead to inconsistencies in the dependencies installed, or worse, breaking changes.

So, what’s the solution? First, let’s understand what the version range signifies. The version range is a string that contains one or more space-separated numbers. These numbers also contain some special symbols like ^ ~ < ||, e.g. ^1.0.4, ~2.3, 4.4.x, >=2.3.4, <1.0.9 ||

Different symbols denote different version updating strategies

These symbols tell npm different things:

Let's say I want to install a package “foo”. After I run npm i foo, my package.json file would mostly have an entry like this:

{
"dependencies":{
"foo": "^2.3.0",
...
...
}
}

Here, foo is installed with version 2.3.0 [major minor patch]. The caret symbol tells something more:

^2.3.0 — [Caret Symbol] This tells npm to upgrade to minor and patch versions, but not major versions. So, basically 2.3.4, 2.3.9, 2.4.5, 2.8 but not 3.0.0 onwards. (Upgrade to minor and patch, but not major)

~2.3.0 — [Tilde Symbol] This tells npm to upgrade to patch versions, but not minor and major versions. So 2.3.4, 2.3.9 but not 2.4.0 onwards. (Upgrade to patch, but not minor and major)

There are a lot of other symbols that denote different npm version updating strategies. The official npm website is a good reference.

So for "foo": "^2.3.0", running npm install a few days later might automatically upgrade the minor/patch version. This is undesirable…

But don't worry, we have package-lock.json to the rescue…

What is package-lock.json?

The lockfile is generated and re-generated when node_modules or package.json is changed

package-lock.json is a lockfile that contains information about the dependencies/packages with their exact version numbers (*important) that were installed for a node.js project.

  • It helps different developers working on the same repo to install the exact package versions installed previously, even if the packages have released new versions. This ensures the same node_modules tree across different machines/environments.
  • package-lock.json file is essentially used to lock dependencies to a specific version number.
  • This file is automatically generated (or re-generated) when there is a change in either the node_modules tree or package.json file.
  • Whenever we clone a repo and run npm i on a new machine, npm will first look to see if a package-lock.json file is present. If yes, it will proceed by installing the packages given in that file. Otherwise, it will look into the package.json file and start installing the required dependency packages. (📦 A caveat to this is explained later in the article)
package-lock.json file ensures the same node_modules tree across

Should you commit your package-lock.json?

Yes, this file should be committed to the source repository so that when developers clone your repo, they can install dependencies that exactly match the ones installed on your machine/environment. This is basically to replicate node.js environments as it is on different machines.

As of npm v7, the lockfile includes enough information about the entire package tree thus reducing the need to read package.json files, and thereby increasing performance as well

JFYI

I’ve just started on youtube, creating videos of beautiful javascript experiences. (PS. its not a coding tutorial channel…) Do check out its videos:

Why / When does npm install rewrite package-lock.json?

  • 📦 Caveat: npm install considers package-lock.json only if the package(s) to be installed are within the version range of package.json.
  • If the package version given in the lockfile is not in the version range of the package.json file, packages are updated & package-lock.json is overwritten.
  • If you want the installation to fail instead of overwriting package-lock.json, use npm ci.

For example,

You declare a dependency in package.json like:

"foo": "^2.3.0"

Then you do, npm install which will generate a package-lock.json with:

"foo": "2.3.0"

Few days later, a newer minor version of “foo” is released, say “2.4.0”, then this happens:

npm install — package-lock version is within the range (i.e. ^2.3.0) so 2.3.0 is installed
npm ci — This anyway only looks at the package-lock.json so 2.3.0 is installed

Next, you manually update your package.json to:

"foo": "^2.4.0"

Then rerun:

npm install — package-lock version is not within the range (i.e. ^2.4.0) so 2.4.0 is installed and the package-lock.json is re-written to now show:
"foo": "2.4.0"

npm ci — This anyway only looks at the package-lock.json, but since the version is not within the range, it throws an error.

npm ci command is similar to npm install, except it’s meant to be used in automated environments such as test platforms, continuous integration, and deployment — or any situation where you want to make sure you’re doing a clean installation of your dependencies. (Source: npm docs)

So in a nutshell:

  1. npm install is not deterministic, which poses a problem when you’re working on a repo (with multiple devs) containing thousands of dependencies.
  2. The package-lock.json file ensures that the same node_modules tree is generated every time npm install is run.
  3. A newer command npm ci ensures that it ALWAYS creates the same node_modules tree, otherwise throws an error.

--

--

Atharva Kulkarni
Pavesoft

Deeply researched articles on making digital assets in the 21st Century