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

  1. 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.
  1. Can you explain the difference between the tilde (~) and caret (^) symbols in a package.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.
  1. What’s the difference between npm install --save and npm 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.
  1. 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.
  1. 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
  1. 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 and package-lock.json files, and it’s too large to store in a repository.
  • d) It is updated dynamically by Node.js during runtime.