Locking transitive Dependencies with NPM

  • July 24, 2017
Table Of Contents

As a developer I am lazy. I don’t build everything by myself because others have done it already. So, when I come upon a problem someone has already solved and that someone put that solution into some library, I simply pull that library into my own - I declare a dependency to that library.

This post describes an important caveat when declaring “soft” dependencies using NPM and how to lock these dependencies to avoid problems.

package.json

In the javascript world, NPM is the de-facto standard package manager which takes care of pulling my dependencies from the web into my own application. Those dependencies are declared in a file called package.json and look like this (example from an angular app):

"dependencies": {
    "@angular/animations": "~4.2.4",
    "@angular/common": "^4.0.0",
    ...
  }

Unstable Dependencies

In the package.json you can declare a dependency using certain matchers:

  • "4.2.4" matches exactly version 4.2.4
  • "~4.2.4 matches the latest 4.2.x version
  • "^4.2.4 matches the latest 4.x.x version
  • "latest" matches the very latest version
  • ">4.2.4" / "<=4.2.4" matches the latest version greater than / less or equal to 4.2.4)
  • * matches any version.

Matchers like ~ and ^ provide a mechanism to declare a dependency to a range of versions instead of a specific version. This can be very dangerous, since the maintainer of your dependency might update to a version that does no longer work with your application. The next time you build your app, it might fail - and the reasons for that failure will be very hard to find.

Stable Dependencies with package-lock.json

Each time I create a javascript app whose dependencies are managed by NPM, the first thing I’m doing is to remove all matchers in package.json and define the exact versions of the dependencies I’m using.

Sadly, that alone does not solve the “unstable dependencies” problem. My dependencies can have their own dependencies. And those may have used one of those matchers to match a version range instead of a specific version. Thus, even though I declared explicit versions for my direct dependencies, versions of my transitive dependencies might change from one build to another.

To lock even the versions of my transitive dependencies to a specific version, NPM has introduced package locks with version 5.

When calling npm install, npm automatically generates a file called package-lock.json which contains all dependencies with the specific versions that were resolved at the time of the call. Future calls of npm run build will then use those specific versions instead of resolving any version ranges.

Simply check-in package-lock.json into version control and you will have stable builds.

Not Working?

NPM doesn’t generate a package-lock.json? Or the versions in package-lock.json are not honored when calling npm run build? Make sure that your NPM version is 5 or above and if it isn’t, call npm install npm@latest (you may also provide a specific version to npm install, if you prefer :)).

Written By:

Tom Hombergs

Written By:

Tom Hombergs

As a professional software engineer, consultant, architect, general problem solver, I've been practicing the software craft for more than fifteen years and I'm still learning something new every day. I love sharing the things I learned, so you (and future me) can get a head start. That's why I founded reflectoring.io.

Recent Posts

Inheritance, Polymorphism, and Encapsulation in Kotlin

In the realm of object-oriented programming (OOP), Kotlin stands out as an expressive language that seamlessly integrates modern features with a concise syntax.

Read more

Publisher-Subscriber Pattern Using AWS SNS and SQS in Spring Boot

In an event-driven architecture where multiple microservices need to communicate with each other, the publisher-subscriber pattern provides an asynchronous communication model to achieve this.

Read more

Optimizing Node.js Application Performance with Caching

Endpoints or APIs that perform complex computations and handle large amounts of data face several performance and responsiveness challenges. This occurs because each request initiates a computation or data retrieval process from scratch, which can take time.

Read more