If you work with a frontend, your project usually contains package.json file. Do you know what it's for and why this file is so important? Let's take a closer look at package.json.
In earlier times, when you met a representative of some company, you were likely to receive a business card. Small piece of paper contained all necessary information: name of the person, contact, company details, etc. File called package.json is something similar to a business card, but it describes your project. Let's see how the file can look.
{
"name": "react-modal",
"version": "3.15.1",
"description": "Accessible modal dialog component for React.JS",
"main": "./lib/index.js",
"module": "./lib/index.js",
"repository": {
"type": "git",
"url": "https://github.com/reactjs/react-modal.git"
},
"homepage": "https://github.com/reactjs/react-modal",
"bugs": "https://github.com/reactjs/react-modal/issues",
"directories": {
"example": "examples"
},
"scripts": {
...
},
"authors": [
"Ryan Florence"
],
"license": "MIT",
"devDependencies": {
...
},
"dependencies": {
...
},
"peerDependencies": {
...
},
"tags": [
...
],
"keywords": [
...
],
"engines": {
"node": ">=8"
}
}
The above file comes from react-modal, an open-source library. As you can see, it contains some data about the project (name
, version
, description
, etc.) and lists all external projects that were used to build the library (devDependencies
, dependencies
, peerDependencies
).
Dependencies
Currently, software projects aren't created in a vacuum. Otherwise, a development of new software would not be effective because you would need to reinvent the wheel repeatedly. In reality, most projects use previously developed solutions and build new user functionalities on top of them. This is called dependencies.
Versions of dependencies
Each dependency contains a name and a version: "react-dom": "^16.13.1"
. As you can see, the version is not just a number, but it has some characters. There is a special logic behind this and we will take a look at it:
-
16
- in our example, it is a major version. Usually, number that represent the major part is changed when incompatible or really big, new functionalities are added the project. -
13
- it is a minor version. When small, new functionalities are added to the project, the minor version is usually incremented. -
1
- it is a patch version. If the code contains some bugs, after the deployment of fixes, the patch number is changed. -
^
- it means that only minor and path number can be increased. For example,"17.10.2"
wouldn't comply this whereas"16.14.2"
is ok and can be used by our project.
There are more options for special characters:
-
>
- it means that all versions higher than the given can be used. For example,"17.10.2"
is ok, but"15.11.3"
isn't - the number is lower than our initial version"16.13.1"
. -
~
- it means that only patch number can be increased. For example,"16.14.2"
is not correct, because minor version is changed, but"16.13.2"
is ok - the difference is only for the path part. -
no character
- it means that only the exact version is valid.
If you want to to play around with the versions, I recommend this site: https://semver.npmjs.com/
Installing dependencies
Let's assume that we have a package.json file and we want to run our project. One of the necessary actions that we need to take is to install all dependencies. It can be done by npm install
. As a result, a new node_module folder will be created, and it will contain all packages from the section dependiencies
of package.json. If your dependencies are dependent on other packages, they also will be downloaded and placed in node_module folder, even though they are not explicitly listed in your package.json - it is good to know that installing packages from dependencies
section is transitive.
Sometimes when we use a lot of external packages, node_module folder can have an immense size. Because it is easy to recreate that folder (by installing the dependencies), you shouldn't include it to the repository - all team members are able to execute npm install
to download necessary dependencies.
You can also notice that there is a new file added in your solution: package-lock.json. In the dependiencies
section of that file, all dependant packages will be listed (even those that are indirectly dependent). I will explain package-lock.json more deeply later in this post.
Types of dependencies
In our example, react-modal has three types of dependencies: devDependencies
, dependencies
, and peerDependencies
. Let's clarify the differences between them.
-
dependencies
- as we mentioned earlier, all packages fromdependencies
section are installed along with its dependencies. If I want to install package X (as eitherdependencies
ordevDependencies
) that has Y asdependencies
, and Y has package Z asdependencies
, all three packages (X, Y, and Z) will be installed and placed in node_modules. If you want to add a new package to your solution, you can usenpm install NameOfThePackage
. -
devDependencies
- whiledependencies
are transitive,devDependencies
aren't. In the above example, when X is defined asdependencies
ordevDependencies
in your project and both Y and Z packages are defined asdevDependencies
, they will not be added to the solution - only X will be downloaded. If you want to add a new package as adevDependencies
to your solution, you can usenpm install --save-dev NameOfThePackage
. Development dependencies as a name suggest are helpful when it comes to develop the project locally. When it is deployed to the production, they are not useful and won't be added to node_module. So when you use a commandnpm install --production
, and X is defined asdevDependencies
it won't be downloaded.
Given the above dependency classification, the question may arise of what type to use. There is a lot of opposite views on this topic, so I would like to discuss it more deeply. Let's assume that you want to build a React application and you used Create React App (CRA) tool for a configuration. When you open the package.json file, you can notice that there are only dependencies
, not devDependencies
:
...
"dependencies": {
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.1.1",
"@testing-library/user-event": "^13.5.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
}
...
Some of the Internet suggestions say that you should not use dependencies
for packages that are focused on local development activities, such as testing libraries, code formatters, etc. But as you can see above, @testing-library/jest-dom that is helpful for writing tests is added as dependencies
. To complicate matters further, when you visit, for example, prettier or eslint projects, you can see that on https://www.npmjs.com (official npm package repository) the instruction for installation is the following: npm i eslint
. However, the eslint documentation says that you should use npm install eslint --save-dev
. So, which one is better to use?
Before answering the question, let's separate the types of applications that you can build with the help of package.json file:
- user applications: when you use, for example, React and your application is accessible for users by the browser, we can say that it is a user application.
- developer application: when you build a piece of code that will be used by other developers, we can assume that you are working on a developer application. For example, react-modal is a developer application, because the main goal of it is to reuse its functionalities in external projects.
Having this distinction in mind, we can finally answer our question. When you build a developer application, you should add all packages which your app actively uses as an imported external functionality as dependencies
. So, when you build a modal library that uses on its foundation react-modal, react-modal should be placed in dependencies
section. But when you use prettier to format your code, you should download it as devDependencies
. The above suggestions are valid when you build a developer application, for user applications there are some changes: it doesn't matter what type of dependencies you use. This is the reason why CRA placed all external packages in dependencies
.
If you're observant, you probably noticed that we haven't discussed one type of dependencies yet: peerDependencies
. I deliberately delayed explaining this because we need a distinction of application types which has just been introduced. peerDependencies
are used for developer applications. Let's assume that our project uses package X and there is a high probability that other developers that will download our package also use X:
In that case, we can require a specific version of X package from developers who use our code. We can do it by writing: "X": "^2.14.0
in the peerDependencies
section. As a result, the project that uses our package won't be able to download X with a lower version. If you, for example, tried to download X in "1.10.2"
version you will receive the error:
Found: x@1.10.2
node_modules/x
x@"1.10.2" from the root project
Could not resolve dependency:
peer x@"^2.14.0" from yourProject@1.0.0
Why do we need package-lock.json
As we mentioned earlier, when you run npm i
you can see that package-lock.json file is added to your project. The main goal of this file is to store information about all dependencies and versions that your project uses. The file is able to contain nested dependencies (for example, our project is dependent on X, X on Y, and Y on Z - in package-lock.json all three dependencies, X, Y, and Z will be added). To understand why package-lock.json is so important take a look at the picture:
We have a project that has several dependencies. If you remember the version principles, you should know that, for example, Z package can have not only "7.8.9"
version but also all versions with minor number increased. So, let's assume, that there is a release of a new version for Z package: "7.12.3"
. The same situation can be for our direct dependant: A package. When one user run npm i
before a release of an updated version, the up-to-date version won't be installed. As a result, we can end up with a situation, when we use different versions of dependencies for our project (depending on when npm i
took place):
Why might this situation be a problem? Let's assume that package A has a new functionality in version "10.23.8"
that wasn't there before. Dorothy decided to use this new feature in a code and merged her changes to a repository. When Adam tried to run the code, there is an error - he used version "10.11.12"
. package-lock.json as a name suggests, lock all versions of used dependencies so that we are sure that the entire team will use the same version of external packages. It is important that package-lock.json should be included in our repository to use the power of locking dependencies' versions.
Scripts
Another thing that is specified in package.json file is a section called scripts
. When you create a React application using CRA, you can see the following code:
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
}
When you, for example, run npm start
the command will be translated to react-scripts start
because of the section scripts
. There are a huge number of script's names that are supported by npm, but you are also free to specify your own names. Thanks to it, all team members that download your project with package.json file will be able to run the script with the custom name by using npm run CustomNameOfTheScript
.
Summary
As you can see, one package.json contains many important things for your project. I hope that this post has made you more aware of the intricacies that may lie behind this file.
Top comments (0)