package-lock.json: The Complete Guide
What is package-lock.json? And why should you care?
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.
An Inherent Problem
In the above package.json
, you can see that the “dependencies”
object maps package-name 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 ||
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?
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 orpackage.json
file. - Whenever we clone a repo and run
npm i
on a new machine, npm will first look to see if apackage-lock.json
file is present. If yes, it will proceed by installing the packages given in that file. Otherwise, it will look into thepackage.json
file and start installing the required dependency packages. (📦 A caveat to this is explained later in the article)
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 readpackage.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
considerspackage-lock.json
only if the package(s) to be installed are within the version range ofpackage.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
, usenpm 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 installednpm 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:
npm install
is not deterministic, which poses a problem when you’re working on a repo (with multiple devs) containing thousands of dependencies.- The
package-lock.json
file ensures that the same node_modules tree is generated every timenpm install
is run. - A newer command
npm ci
ensures that it ALWAYS creates the same node_modules tree, otherwise throws an error.