It is common knowledge that most of the frameworks like Node.js, React.js, Vue.js, Angular, etc. are all built with npm as its backbone. The npm registry maintains all the required libraries and dependencies for various frameworks.
It is a command-line utility for interacting with a repository that aids in package installation, version management, and dependency management. With npm, you can install a package with just a single command-line command.
In order to get the most out of npm repositories, it is essential to understand how the npm install command works, the order of the downloaded dependencies, and the node_modules folder structure.
Executing the npm install command
Once you execute the command npm install dependency module, will be downloaded to your device from the npm registry. There are 3 ways to execute this command.
- npm install—to fetch all dependencies mentioned in the dependency tree.
- npm install <dependency_name> or npm install <dependency_name>@<version> - to fetch a particular dependency by name and version (if no version is specified, then it pulls the latest version).
- npm install <git remote url> - to fetch a library pushed to github, bitbucket, or gitlab.
Simplifying npm install
Once you execute the npm install command, you will have your dependency module downloaded on your system; that’s its job. Many of the configuration parameters in the modules will have some direct impact on the installation. To keep track of what changes are happening during the installation, follow these steps:
Step 1: Check whether the node_modules folder exists or the package-lock.json folder. Once you spot the folder, trace the existing dependency tree and clone the tree. You can create an empty tree if you prefer.
Step 2: Fetch the relevant dependencies (dev, prod, or direct dependencies) from package.json and add them to the clone tree (from step-1). There are a few benefits to doing this:
- This process helps you find the difference between the trees and add the missing dependencies if needed.
- All the new dependencies will be added at the top of the tree, and that will help you make changes easily, if and when required.
- New dependencies are added without disturbing the other roots/branches of the tree, and so your params remain as undisturbed as possible.
Step 3: Compare the original tree (from step-2) with the cloned tree (step-1) and make a list of actions required for replicating the new tree in other node_modules. The required actions may be to install (new dependencies), update (existing dependency versions), move (change the location of the dependency within the tree), and remove (uninstall libraries that are not needed by the new tree). Execute all the commands identified, starting with the deepest one.
Folder-Structure in node_modules
There are 4 possible scenarios in the node_modules. The folder structure that npm follows varies according to these scenarios.
- No existing node_modules, package-lock.json or dependencies are available in package.json.
- No existing node_modules or package-lock.json, but package.json with a dependency list is available in package.json.
- No existing node_modules, but package-lock.json and package.json with dependency lists are available in package.json.
- The node_modules, package-lock.json and package.json with dependency lists are all available in package.json.
No existing node_modules, package-lock.json or dependencies is available in package.json
This scenario occurs when a JS framework application starts without any dependencies and then adds them one by one at a later stage. In this scenario, the dependencies are downloaded in order of installation.
For example, when you execute npm install <B> in a new application.
Here B is a dependency, and let’s assume it has an internal dependency on alpha@v2.0, then both get installed at the root level of the node_modules.
Inference: All the dependencies try to get a place in the root of the node_modules unless there is a conflict with the dependency existing in a different version.
Final resulting node_modules:
|_ B
|_ alpha @v2.0
No existing node_modules or package-lock.json, but package.json with a dependency list is available in package.json
In this scenario, an application has dependencies listed in package.json without a lock-file.
For example, when you execute npm install in the application directory, which has a package.json with dependencies like,
{
"dependencies":
{
"A": "1.0.0",
"B": "2.0.0"
}
}
Here, A internally depends on alpha@v1.0 and B depends on alpha@v2.0. All the new dependencies and the pre-existing dependencies try to get a place in the root of the node_modules unless there is a conflict with the same dependency in a different version. When such a conflict arises, it creates a sub-node_modules under each dependency and pushes conflicting internal libraries into it.
Final resulting node_modules:
|_ A
|_ alpha @v1.0
|_ B
|_ node_modules
|_ alpha @v2.0
No existing node_modules, but package-lock.json and package.json with dependency lists are available in package.json
Assume, A internally depends on alpha@v1.0 whereas, B depends on alpha@v2.0 and beta@v3.0.
package-lock.json snippet would look like,
{
"dependencies": {
"A": {
"version": "1.0.0"
"resolved": "NPM REGISTRY URL of A",
"requires": {
"alpha": "1.0.0"
}
},
"alpha": {
"version": "1.0.0",
"resolved": "NPM REGISTRY URL of alpha v1",
},
"B": {
"version": "2.0.0",
"resolved": "NPM REGISTRY URL of B",
"requires":
{
"alpha": "2.0.0",
"beta": "3.0.0"
},
"dependencies":
{
"alpha":
{
"version": "2.0.0",
"resolved": "NPM REGISTRY URL of alpha v2",
}
}
},
"beta":
{
"version": "3.0.0",
"resolved": "NPM REGISTRY URL of beta v3",
}
}
}
Irrespective of how the dependencies are ordered in package.json, the packages will be installed according to the tree structure defined by package-lock.json.
And the resulting dependency tree structure would be:
node_modules
|_ A
|_ alpha @v1.0
|_ B
| |_ node_modules
| |_ alpha @v2.0
|_ beta @v3.0
The node_modules, package-lock.json and package.json with dependency list are all available in package.json
The node_modules folder will be rearranged to match the incoming new tree from package-lock.json and installed in the order defined in the package-lock.json file.
Package.json vs. Package-lock.json
To understand the difference between package.json and package-lock.json, consider the following sequences of dependency installation in a new application without an existing dependency tree or node_modules in it:
Assume, A internally depends on alpha@v1.0 whereas, B depends on alpha@v2.0.
npmScenario-1Scenario-2Commandsnpm install A
npm install Bnpm install B
npm install Apackage.json{
"dependencies": {
"A": "1.0.0",
"B": "2.0.0"
}
}{
"dependencies": {
"A": "1.0.0",
"B": "2.0.0"
}
}package-lock.json{
"dependencies": {
"A": {
"version": "1.0.0",
"requires": {
"alpha": "1.0.0",
}
},
"alpha": {
"version": "1.0.0",
},
"B": {
"version": "2.0.0",
"requires": {
"alpha": "2.0.0",
},
"dependencies": {
"alpha": {
"version": "2.0.0",
}
}
}
}
}{
"dependencies": {
"A": {
"version": "1.0.0",
"requires": {
"alpha": "1.0.0",
},
"dependencies": {
"alpha": {
"version": "1.0.0",
}
}
},
"alpha": {
"version": "2.0.0",
},
"B": {
"version": "2.0.0",
"requires": {
"alpha": "2.0.0",
}
}
}
}node_modulesnode_modules
|_ A
|_ alpha @v1.0
|_ B
| |_ node_modules
| |_ alpha @v2.0node_modules
|_ A
| |_ node_modules
| |_ alpha @v1.0
|_ alpha @v2.0
|_ B
The above comparison depicts the roles of package-lock.json and package.json.
If the package alpha is imported from the JS application, like var alpha = require('alpha');, scenario-1 imports to v1 whereas, scenario-2 imports v2. So, the behavior of the code snippets might differ based on the imported file.
It is not package.json that determines the tree structure. The npm install command downloads dependencies in alphabetical order, as saved in package.json. The best practice is to push and maintain the package-lock.json into the source code (like git), to ensure the same dependency tree is being used by all members of the project.