Rafa Romero Dios
2 Feb 2023
•
7 min read
Working in frontend means working mainly in JavaScript. And working with big JavaScript projects means also working with other libraries (from now on dependencies or packages) that allows developing from scratch some features that have been already developed by other developers. This libraries are called dependencies in the Node world and it consists of all the packages that are used in the project. Managing this dependencies (and specially their versions) is a hard and a complex task to be done manually.
It's been a while since these libraries stopped being hosted in isolated server to be hosted and specially managed by a centrallized system called NPM (node package manager), created by Node. From those developer who have worked with Java, it's kind of Maven, but in the JavaScript world with the difference that their are all hosted in a centrar repository. NPM allow us to manage both monorepo and multirepo projects to define and configure, along other settings, the depedencies of the project.
Today we're gonna talk about how to manage these dependencies, how many types there are and specially how to handle the versions of this packages, which is, in fact, the main topic of this article.
Before starting, we need to locate ourselves: all we are talking about today it's located in the package.json file (once again, for those know the Java world, it would be the equivalent to the pom.xml
file). This file manages all the NPM related project info and settings, and is very very long and complex in someway. That's why today we will focus "only" in dependencies
All dependencies defined in package.json will be automatically handled (and probably downloaded, depending of its type) by NPM when running the command npm install
.
It's important to clarify that this article aims to summarize all the information available to the topics described before, that's why all the information you will find here is based mainly in the one available in the official documentation page of NPM among other articles and sources from the internet that you can consult in the bibliography
Dependencies (also called packages), as commented before, are the external libraries that our project works with. They can be of different types, as we will see then. All these dependecies are stored locally in our project in node_modules
folder. This folder, as you can imagine, is never included in our version control system. In other words, is included in our .gitignore
file (if you are working with Git).
Dependencies, depending basically on the scope or context where they will be executed (only in local, along with others,...) could be of different types. So let's see that types and which context is used with each one.
After describing each type of dependency, we will see package versioning.
dependencies
TLDR; dependencies are the libraries your project depends on
Let's start with probably the easiest one to understand: basic dependencies. Those ones are the ones that are independently used in our project and the ones that will be included with the packed version of our project. They are sine qua non condition to execute the project properly.
devDependencies
TLDR;
devDependencies
are the dependencies that are needed only during the development phase
The dependencies called devDependencies are those ones that are used during development phase but are not needed in other environments unless this one (development), i.e., they are not needed in production environment for instance. A common example of a devDependency
is any library used for testing (jest for instance)
peerDependencies
TLDR;
peerDependencies
are the dependencies that your package needs and is the same exact dependency as the person installing your package
The dependencies called peerDependencies
are those ones that are essential to execute our project but we assume that will be included by any of the other libraries in the execution context of the project. Said this, let's see an example to understand it better.
First of all, let's talk in a more schematic way. Let's say, our package X has a dependency called Y. And the library Y, has a peerDependency
called Z. Therefore, we need that the package X includes the dependency Z.
Let's say our project (a library for instance) need React to be executed. So we assume that our library will be used in a context (a webapp or a package for instance) that already includes React. That's why we will define React as peerDependency
, because besides is a required dependency, we don't want to be included in the packed version of our library but we assume will be already include by the package that includes as dependency our package.
bundledDependencies
TLDR; dependencies that will be bundled when publishing the package.
The dependencies called bundledDependencies
are similar to regular dependencies, but are used when we want to explicity indicate some dependencies that should be packed in our project. Let's see why:
Normal dependencies are usually installed from the npm registry. Thus bundled dependencies are useful when:
optionalDependencies
TLDR;
optionalDependencies
are the dependencies that are not needed for the execution of the library`
The dependencies called optionalDependencies
are those ones that does not make falling the npm install
script and are not critical for the execution of the library. This is useful for dependencies that, for instance, won’t necessarily work on every machine and you have a fallback plan in case they are not installed.
Semantic versioning or SemVer is a standard definition for defining versioning projects. It is the classic versioning based on three numbers X.Y.Z.
Code status | Stage | Rule | Example version |
---|---|---|---|
First release | New product | Start with 1.0.0 | 1.0.0 |
Backward compatible bug fixes | Patch release | Increment the third digit | 1.0.1 |
Backward compatible new features | Minor release | Increment the middle digit and reset last digit to zero | 1.1.0 |
Changes that break backward compatibility | Major release | Increment the first digit and reset middle and last digits to zero | 2.0.0 |
*Semantic versioning reference definition, from NPM Docs
As you probably are already thinking, semantic versioning is the one used by Node, NPM, and probably the most of the projects in the JavaScript ecosystem (and even outside the JavaScript world!).
We are introducing it because it's important to know how it works since NPM has a powerful way to handle the versions of our dependencies inside our project via package.json file.
We will see it deeper in a moment, but basically, you can handle the versions of the packages that NPM will download by using a series of operators when defining the dependency.
Let's see the main options to define the dependency version:
version
--> Must match version exactly>version
--> Must be greater than version>=version
--> Must be equals or greater than version<version
--> Must be lower than version<=version
--> Must be equals or lower than version~version
--> "Approximately equivalent to version": Allows patch-level changes if a minor version is specified on the comparator. Allows minor-level changes if not^version
--> "Compatible with version": Allows changes that do not modify the left-most non-zero digit in the [major, minor, patch] tuple. In other words, in a version defined with ^1.2.3 ^0.2.5 ^0.0.4
this allows patch and minor updates for versions 1.0.0 and above, patch updates for versions 0.X >=0.1.0, and no updates for versions 0.0.X. 1.2.x 1.2.0, 1.2.1, etc., but not 1.3.0*
--> Matches any version""
--> (just an empty string) Same as *version1 - version2
--> Same as >=version1 <=version2.range1 || range2
--> Passes if either range1 or range2 are satisfied.That way, as we said before, we can handle the version of the dependecy that will be included in our project and make it possible to handle a common issue: that the author of a dependecy included by ours updates it with a breaking change and therefore breaks our project.
package-lock.json
Besides using a standard versioning like SemVer can avoid some problems, sometimes some problems related to dependencies updates are impossible to avoid.
Let's say, for example, that we have a dependency on express defined by ^2.20.0 in package.json and that later the express team releases version 2.24.0. If someone now clones our repository and runs npm install
, they will get version 2.24.0.
However this can be a problem if the developers of the package "break" some functionality in the minor version which can cause our application to crash.
So, How to make sure your project built with same packages in different environments in a different time?
Since NPM v5, package-lock.json simply avoids this general behavior of updating minor or fix versions so that when someone clones our repository and runs npm install
on their machine, npm will look at package-lock.json and install the exact version of the package that we had installed, thus ignoring the ^ and ~ in package.json.
From NPM Docs
package-lock.json is automatically generated for any operations where npm modifies either the node_modules tree, or package.json. It describes the exact tree that was generated, such that subsequent installs are able to generate identical trees, regardless of intermediate dependency updates.
This file is intended to be committed into source repositories, and serves various purposes:
Describe a single representation of a dependency tree such that teammates, deployments, and continuous integration are guaranteed to install exactly the same dependencies.
Provide a facility for users to "time-travel" to previous states of node_modules without having to commit the directory itself.
To facilitate greater visibility of tree changes through readable source control diffs.
And optimize the installation process by allowing npm to skip repeated metadata resolutions for previously-installed packages."
So we have seen a comprenhesive explanation about dependencies in a NPM project. What the mean, how to handle the downloaded version, how Semantic Versioning works and how to handle possible issues with versioning.
Although it could seem an easy part of package.json file, it's delicate and it's important to define it properly to avoid non-desired problems.
Rafa Romero Dios
Software Engineer specialized in Front End. Back To The Future fan
See other articles by Rafa
Ground Floor, Verse Building, 18 Brunswick Place, London, N1 6DZ
108 E 16th Street, New York, NY 10003
Join over 111,000 others and get access to exclusive content, job opportunities and more!