The challenge with NPM versioning

Since its release, Node Package Manager (NPM) has become the de facto standard for distributing most Javascript projects, from servers to frontend frameworks; and overall, it has vastly simplified developers lives. Unfortunately, it also introduced a number of challenges regarding versioning and dependency management.

Now, let’s say you are developing a new project and you are using NPM to find and manage your packages. Your framework will provide a command line interface (CLI) that lets you create a new project with a single command. Some tools will install an initial set of packages or generate a list for you to install at some point.

As you start writing code, you will start to add packages to provide useful tools or support new features. These packages, in turn, will install additional packages that they depend on. The result is that, not long after you start an entirely new project, there may be hundreds—and possibly thousands—of packages installed. Many of them without your knowledge.

NPM Dependency Graph: Top 100 dependent upon npm packages and their dependencies in 4 levels of depth.

One of the many great things about NPM is that it lets us take care of version updates with one simple command: `$ npm update`. Not only does this command let us update existing packages in one go, it also lets us do basic cleanup and remove old and unwanted packages.

Just like updating the operating system on your smartphone or laptop, running NPM’s install or update commands is a drama-free experience. However, when you update a device’s operating system, the device’s vendor has first made sure that the existing, updated, and new components work together. By contrast, in the NPM world there is no single, definitive source of truth and new versions and patches are released independently. This means that there is always a risk that your app will break when an updated component won’t work with an existing one. In addition, many components may use the same subcomponents, but require different and incompatible versions.

How NPM manages package versions

Before we look at solutions, we need to understand how NPM manages project dependencies. Let’s start our journey by looking at package.json. Many CLI framework tools create the file each time you create a new project, based on a project-specific template. For example, if you create a React project with the create-react-app command, the file will include the project name, an initial version, three react dependencies, and four basic scripts.

If you are running NPM 5 or higher, each time you install a local package as part of your project, NPM automatically updates the package.json file. Unless you have a specific reason for touching package.json, most of the time you can safely ignore it. For more information, take a look at Working with Package.json.

NPM version notation and semantic versioning

The first step in avoiding component incompatibility issues is to understand how versions are managed. When you open package.json and scroll to the dependencies section, you will notice that each component has been installed as a key:value pair (“package”: “^1.0.0”) indicating the package name, followed by the version number. SemVer, the semantic version numbering scheme used by NPM, uses these three digits to indicate the major, minor, and patch versions.

By default, the package version is prefixed with a ^ (caret) character, which instructs NPM how to handle the next package update. According to the NPM documentation, the caret “…allows for changes that are presumed to be additive (but non-breaking), according to commonly observed practices.”

The caret allows changes that do not modify the left-most non-zero digit in the indicated version. In other words, it will allow patch and minor updates for versions 1.x.x, patch updates for versions 0.1.x, and no updates for versions 0.0.x. Note that while the caret provides you with tremendous flexibility, it can also add additional complexity. In other words, most of the time you’ll be fine, but there’s always a chance that something could break.

Using package locks

One way to manage dependency versions is to take advantage of package locking using the package-lock.json file. This file specifies a version, location, and hash for every module and each of its dependencies, with the hash being used to verify the package integrity. NPM creates this file the first time you install a package, or recreates it if you accidentally delete it. NPM modifies and updates this file each time you run a command that modifies the package.json or node_modules, such as install, update, or remove.

A side benefit of package locking is that it lets you propagate your version of the module tree across your organization. In other words, if you commit your package-lock.json to a version- controlled repository, any team members who merge from the repo should be able to replicate the changes you made to the package map. It also ensures that whatever your operating system or how many developers pull the code, exactly the same packages and dependencies are installed. If you want to know more about this topic, take a look at Everything You Wanted To Know About package-lock.json But Were Too Afraid To Ask.

Installing specific package versions

For many of us, the best way to avoid problems is to follow the KISS (Keep it Simple Stupid) principle. The simplest way to get the exact package you want is to install that version. NPM also lets you install a specific package version by appending the @ symbol to a package name, followed by the version number, for example:

$ npm install package@1.1.2

In this case, you can use NPM install to download version 1.1.2 of your package. After you run the command, NPM will automatically make the necessary changes to the package.json and package-lock.json files.

NPM versioning best practices

In addition to the mechanisms outlined above, here is a short list of best practices that will help you manage your installed packages and will prevent NPM versioning issues. The list contains a number of useful NPM commands that run version management related utilities.

1. Update frequently and use package locking

I already discussed these guidelines in detail, but  I repeat them here because one of our human foibles is that we frequently overlook the simple and obvious. Either way, you should frequently run the NPM install and update commands to refresh your project dependencies. In addition, if multiple developers are working on the same project, make sure you use package locking to ensure that everybody is on the same page.

2. Know more about your package

When installing NPM packages, it is very easy to just fire and forget. In other words, once we’ve located the package we need, we install it without a great deal of thought and only remember that it exists if something breaks and it’s mentioned in a cryptic error message/trace. It is important to do your homework before installing the package.

3. Read the home page

Although the quality of the information may vary, most packages include a link to a project homepage. Also, a quick rule of thumb: if it doesn’t have a home page, this is a sign that the project is not maintained, and you should avoid it. To view a project home page, use the following CLI command:

4. Review the project readme

High quality projects usually have a well-maintained home page. If it doesn’t, the next best thing is to view the project’s readme. Most developers will update their project’s readme and commit it to their GitHub repo when they release new versions. To view the project repo/GitHub page, type:

npm repo

5. Get the issues list

NPM also has a handy command for display package issues. Using the following CLI command opens the project’s GitHub issues page.

npm bugs

6. See who’s responsible

Although not strictly version related, NPM packages include a list of the team responsible for maintaining it. To display the list, use:

npm view maintainers

7. Find outdated packages and dependencies

If you are considering updating some or all of your packages, use the following command in your project directory to see a list of outdated packages:

npm outdated

Since most projects can easily contain hundreds or even thousands of packages, to see when a specific package was updated, run the following command to get the exact time and date the package was last updated.

npm view time.modified

8. Read these articles

For a through introduction to NPM version management, take a look at A Beginner’s Guide to npm — the Node Package Manager. To learn more about semantic versioning, read Semantic Versioning: Why You Should Be Using it, and Understanding the NPM Dependency Model. Finally, to learn some really useful tips and tricks, see 10 Tips and Tricks That Will Make You an NPM Ninja.

Conclusion

Packages have revolutionized software development, but they also introduced challenges related to dependency and package management. Understanding how versions are managed by NPM and employing some package versioning best practices can help you avoid common pitfalls when updating packages in an app.


Arthur Schmunk
Block misconfigurations,
not deployments.