Photo by Moses Lee on Unsplash
Should I git ignore package-lock.json? Understanding package manager lockfiles
July 5, 2022 • 5 min readIf you, as a developer, have been through an onboarding process in a new project where you have to set up a myriad of tools and integrations, the chances that you have heard the phrase "works on my machine" are considerably high. Either during onboarding or a test phase for a feature, someone will say something similar to that. Sometimes the scenario where things work on your machine but not in anyone else's machine can be caused by the dependencies your project uses.
The higher the number of dependencies your project has, the easier it is to break things whenever we need to upgrade or downgrade them. If your team does not enforce a proper way of handling dependencies and sub dependencies it is likely that at some point you'll get into something famously known as "dependency hell".
In this post, we'll briefly discuss how lockfiles are considered an extremely important tool to help us avoid that kind of problem and how we can take advantage of them to both improve the development experience and decrease the total size of the modules bundle used in our projects.
For this post, we'll take Yarn package manager as an example for describing how things work in detail. However, be aware that the concept of lockfiles is not at all limited to the JavaScript environment. Whether you work with pip for Python, Bundler for Ruby, NuGet for .NET, and many other package managers the concepts explained here also apply, to a certain degree, to those environments.
How are we specifying versions for our dependencies?
When it comes to specifying versions for our dependencies, we can either set a fixed version number that should be downloaded or a range of acceptable versions. When adding dependencies in JavaScript projects it is common to see version ranges being specified in package.json
. Following the Semantic Versioning rules, we would do something like the example below.
{
"name": "our-brand-new-package",
"version": "1.0.0",
"description": "",
"devDependencies": {
"redux": "^4.0.0"
}
}
In this example, because of the ^
character, we are telling our package manager to install the latest minor release available for redux
. At the moment of writing this, the latest minor release for redux at v4 is 4.2.0
. Thus, when we run yarn install
we will download the version 4.2.0
for redux and also any sub-dependencies that redux uses for production mode.
Let's confirm this by looking at the yarn.lock
file generated after the install.
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@babel/runtime@^7.9.2":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.6.tgz#6a1ef59f838debd670421f8c7f2cbb8da9751580"
integrity sha512-t9wi7/AW6XtKahAe20Yw0/mMljKq0B1r2fPdvaAdV/KPDZewFXdaaa6K7lxmZBZ8FBNpCiAT6iHPmd6QO9bKfQ==
dependencies:
regenerator-runtime "^0.13.4"
redux@^4.0.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.0.tgz#46f10d6e29b6666df758780437651eeb2b969f13"
integrity sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==
dependencies:
"@babel/runtime" "^7.9.2"
regenerator-runtime@^0.13.4:
version "0.13.9"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==
You can see that as redux
depends on babel/runtime
and babel/runtime
depends on regenerator-runtime
, those two other pacakges were also downloaded and added to the list of dependencies in our lockfile, even though we have never mentioned them in our package.json
.
How does that generated lockfile help us?
By looking at the lockfile pasted above, you will notice that we can see the exact version that we are using at that moment and not only the version we have defined previously. Imagine the scenario where there is no place where we keep track of that exact version number and that between the day I installed redux
on my machine and the day my coworker did the same on their machine, there was a release of a minor version for that package. This means that my coworker will be working based on a different version of the same package even though we are working on the same codebase. Imagine now that instead of your coworker, the other part of this story is the server machine where your project runs. Imagine now that the contributors for that package you use as a dependency have released a minor version that was actually supposed to be a major version because it includes code that is incompatible with code you have written (yes not everyone follows Semantic Versioning properly).
In all scenarios listed above, we can see how easy it is to get ourselves into a "dependency hell", especially when we talk about projects that have hundreds (even thousands) of dependencies and let's not forget about all the sub-dependencies...
That is one of the main purposes of having a lockfile in our projects: guaranteeing that dependencies are the same across different machines and installations.
In the lockfile above, we can also see that we have a checksum hash (integrity
field) to guarantee that the contents of that dependency match with contents of the version defined in our package.json
for that same dependency.
Another important benefit provided by (most?) package managers with the usage of lockfiles is the deduplication
of dependencies. This happens, for instance, when we have two or more dependencies that rely on one or more common sub-dependencies. In this case, instead of downloading all the versions for those sub-dependencies, the package manager looks for a version that will work for both dependencies at the same time and downloads only that one, thus decreasing the amount of code we need to download before running our project.
Then, should I commit my lockfile to the remote repository?
Short answer: yes, definitely!
Longer answer: it is a good practice to always commit our lockfiles (yarn.lock
, package-lock.json
) into our repository for all the reasons listed above and also as an important way to keep track of how our dependencies and sub-dependencies have changed throughout time, which might also help with debugging things when our code breaks after migrating between versions.
There is some discussion regarding this when it comes to developing a library instead of an application since the lockfile will not be used by the consumers of that library. However, I agree that even when building libraries it is important to keep track of a lockfile in order to help improve contributors' experience. You can read more about this discussion in this blog post by James Kyle on Yarn's official website.