Understanding Dependency Management with npm
In this chapter, we’re exploring package distribution within the npm ecosystem. By the end of this chapter, you’ll be adept at selecting and downloading the right packages for your application, all while adhering to the principles of semantic versioning. Let’s get started!
Exploring Semantic Versioning
When opening our project in VS Code, we get a nice overview of all our files. Let's begin with the package.json
file. I like to sort its properties alphabetically to make finding entries quicker. The first thing I want to discuss is the caret symbol in front of our typescript
dependency. This symbol is part of what's known as "Semantic Versioning," and understanding it is essential because it is often overlooked in tutorials.
The version number is made up of three components: the major version, the minor version, and the patch version.
According to the guidelines of "Semantic Versioning," a release that only contains bug fixes would increment the patch version. For example, version 5.0.2 would become 5.0.3.
When new features, performance improvements, or backward-compatible refactorings are added, it’s appropriate to create a minor release. This would change 5.0.2 to 5.1.0.
If changes are made that break compatibility with previous versions, the major version number would be incremented. In our case, 5.0.2 would become 6.0.0.
The caret symbol restricts upgrades to patch or minor versions, without allowing major version updates.
Alternatively, if we use the tilde symbol, npm will only upgrade to the latest patch version. So, if version 5.1.0 exists, we wouldn’t get it; however, version 5.0.3 would be installed.
Alternative Versioning Syntax
You can also use shorthand notations like 5.0.X
to grab the latest patch version or 5.X.X
to get the latest minor version along with its most recent patch.
When you change version numbers, it’s important to run npm update
to apply those changes. Unlike npm install
, which only downloads the compatible version, npm update
will ensure your package is upgraded to the latest version matching the specified range.
The Role of Lockfiles
If you’re curious about which version has actually been installed, you can check the package-lock.json
file. This Lockfile records the exact version installed, whether during the initial installation or after any updates. This ensures reproducibility for future installs.
In the package-lock.json
file, you'll see that typescript
was resolved to version 5.0.2, and you can also see where it was downloaded from, typically npmjs.org, the default registry for npm.
On npmjs.org, you can search for packages and find details about available versions. It’s also important to mention that tags can be used to specify versions. You can install a specific version by running npm install
with the version number or tag, like npm install -D typescript@5.0.2
or npm install -D typescript@latest
.
Updating Dependencies
I want to stress the importance of using npm update
. If you don’t have a package-lock.json
file and you declare TypeScript as a dev dependency with the tilde symbol, running npm install
will create this lockfile. It locks the TypeScript version to the latest patch level, which could be higher than the version you initially declared with the tilde. If you then switch to the caret symbol and run npm install
again, no changes will be made, because the previously installed and locked version is compatible. To update to the latest minor version, you’ll need to run npm update
to enforce an update in the package-lock.json
file based on your package.json
.
If you’re unsure which version of a dependency you have, the npm explain
command can help. This command provides information about the installed version, the type of dependency, and where it was defined—useful when working in monorepos.
Using the SemVer Calculator
To practice matching version numbers, visit semver.npmjs.com. You can experiment with different version ranges by entering the name of a package.
You can specify an exact version (e.g., 4.8.3
), use the tilde symbol for all patch versions within the same minor range, or use the caret symbol to match all minor versions in the same major range.
Additionally, the wildcard (*
) allows you to match any version, while the latest
tag provides the most recent version. You can also use comparison operators to specify a range of versions.
Dependency Types
Next, let’s explore the differences between standard dependencies, development dependencies, and peer dependencies.
Development Dependencies
Development dependencies, like typescript
, are used during design time (i.e., development). On the other hand, standard dependencies are required during runtime and are typically imported in the application code.
For example, if you use the popular date formatting library "Moment" in your main code, you’d install it as a standard dependency. You can do this by running npm install --save moment
. The --save
flag tells npm to add the package to the dependencies
section of your package.json
. As of npm version 5, this is the default behavior, so you can skip the --save
flag.
A useful tip: if you want to skip installing development dependencies, use npm install --omit=dev
.
Peer Dependencies
Now let’s discuss peer dependencies. In cases where a package is built on top of another, it’s a best practice to declare the host package as a peer dependency. This is often the case for plugins.
An example is the "moment-range" plugin, which expects the "moment" package to be installed before the plugin itself. That’s why "moment" is listed as a peer dependency—it acts as a host for the plugin.
Peer dependencies usually specify a broader version range to ensure compatibility with multiple versions. For example, "moment-range" requires a minimum version of 2 for "moment."
In short, peer dependencies are crucial for ensuring the compatibility and proper functioning of plugins and extensions with their host packages.
Transitive Dependencies
In addition to its peer dependency on "moment," the "moment-range" plugin has a regular dependency on "ES6 Symbol." This means that when you install "moment-range" in your project, "ES6 Symbol" will also be installed as a transitive dependency.
Transitive dependencies are indirectly required by a package through another dependency. Understanding transitive dependencies is important because they can quickly accumulate.
An npm command you might find helpful is npm explain
, which helps you track down the reason a dependency was added to your project.
The npm ecosystem and its nuances may seem overwhelming at first, but understanding these concepts helps set you apart as a skilled developer.
In the next chapter, we’ll focus on compiling TypeScript code for specific platforms and setting up your IDE for optimal development feedback.
Node Modules
The node_modules
directory is a core part of any Node.js project. It houses all installed packages and their dependencies, creating a full dependency tree required for the project. The npm tool generates this directory automatically during package installation (npm install
).
This directory can become quite large because it includes both explicitly installed and transitive dependencies. Since node_modules
can be regenerated anytime using package.json
and package-lock.json
, it's usually excluded from version control by adding it to .gitignore
. This keeps repositories lean and avoids unnecessary bloat.
What You Have Learned
Understanding Dependency Management with npm: You have learned how to manage your project’s dependencies using npm. This involves selecting, downloading, and updating the correct packages. You now understand the importance of version control in maintaining stability.
Using Semantic Versioning: You understand how semantic versioning works. The version number consists of major, minor, and patch versions. You now know that the caret (^
) allows updates to minor and patch versions, while the tilde (~
) only allows patch updates.
Managing Dependencies with Lockfiles: You have learned the role of package-lock.json
in locking dependencies to specific versions. This ensures reproducibility across installations. You now understand the importance of running npm update
to modify the lockfile and update your locked dependencies.
Understanding Dependency Types: You now know the difference between standard dependencies, development dependencies, and peer dependencies. Dependencies are needed during runtime. Development dependencies are needed only during development (design time) and peer dependencies are indirectly required by other dependencies.
Installing and Updating Dependencies: You’ve learned how to install dependencies with npm install
and update them with npm update
. You now know how to use shorthand notations like 5.0.X
to specify versions. The npm explain
command helps you track dependencies more easily.
Working with Node Modules: You now understand that node_modules
stores your dependencies. It should be excluded from version control to keep your repository lean. This directory can be regenerated at any time using package.json
and package-lock.json
.
Quiz
- What distinguishes a major version from a minor version?
- a) Major versions are for security updates, while minor versions are for performance improvements.
- b) Major versions change the API significantly (breaking changes), while minor versions introduce backward-compatible - features.
- c) Major versions define the last segment of a version number, while minor versions define the first segment.
- d) Major versions are for new features, and minor versions are for bug fixes only.
- Can you explain the difference between the tilde (
~
) and caret (^
) symbols in apackage.json
dependency list?
- a) Tilde updates to the latest minor version, while caret updates to the latest patch version.
- b) Tilde updates to the latest patch version within the current minor version, while caret updates to the latest minor version within the current major version.
- c) Tilde locks the version completely, while caret allows any updates.
- d) Tilde and caret are interchangeable in dependency management.
- What’s the difference between
npm install --save
andnpm install --save-dev
?
- a)
--save
installs runtime dependencies, while--save-dev
installs global dependencies. - b) Both are deprecated; they no longer affect dependency types.
- c)
--save
installs runtime dependencies, while--save-dev
installs development dependencies. - d)
--save
is for applications, and--save-dev
is for libraries.
- Why is
npm update
essential?
- a) It recompiles the TypeScript code in a project.
- b) It upgrades Node.js to the latest version.
- c) It updates outdated dependencies to their latest compatible versions.
- d) It clears the
node_modules
directory and reinstalls packages.
- What steps would you take to install a runtime dependency?
- a) Run
npm install <package>
- b) Run
npm install --save-dev <package>
- c) Run
npm install <package> --save-prod
- d) Run
npm install
- Why should the
node_modules
directory be excluded from version control systems like Git?
- a) It contains sensitive API keys and credentials.
- b) It contains unnecessary files for production.
- c) It can be regenerated from the
package.json
andpackage-lock.json
files, and it’s too large to store in a repository. - d) It is updated dynamically by Node.js during runtime.