Chained npm Package Releases (Concept)

Many developers working with JavaScript monorepos are familiar with tools like Lerna that let you run a single command to release multiple npm packages at once. These tools are great for monorepos that include packages that don’t directly depend on each other. There are, hovever, times where you have packages that do directly depend on each other. One great example is a design system.

What is a Direct Dependency?

When a monorepo contains a package (x) that depends on an exact version of another package (y) in the same monorepo, package x is said to have a direct dependency on package y. As an example, let's pretend that we have a packages directory in our monorepo that looks like the following:

For those of you who are familiar with design systems, this structure might look familiar. We are writing components in both Angular and React, but the components share the same styles, so both component packages import the styles package. Now, let’s look at the package.json files for angular-components and react-components:

Both component packages need the most up-to-date version of web-component-styles because the components and styles are written at the same time. In other words, the components are the HTML and JavaScript, and the missing piece (CSS) is in the web-component-styles package, which is tightly coupled to a specific version of angular-components and a specific version of react-components.

The angular-components and react-components packages have a direct dependency on the web-component-styles package.

Aside: The Difference Between “Newest” and “Latest”

I am using the term “newest” instead of “latest” so that there isn’t any confusion between the word “latest” and the @latest tag. For example, you might be running an alpha release of your packages. It's likely that your angular-components and react-components will rely on an alpha version of web-component-styles. In this case, the alpha release is the newest release. Running npm install react-components@latest is very different from running npm install react-components@alpha-some-git-sha.

The Conceptual Model of a Chained npm Package Release

At the time of this writing, I am not aware of any open-source tools that can handle releasing a set of packages where one package needs to install a new version of another package in the same monorepo. To keep the length of this article to a minimum, I’m going to talk about the conceptual model behind writing a script that can release packages for you, so there is no implementation code here.

Release Process

Below is a dependency tree of our packages. For some added complexity, I added docs “packages” for both the angular-components and react-components. "Packages" is in quotes because our docs will acutally be deployed to a web server when the package version number changes rather than being deployed to npm.

Let’s run through the steps of our release:

  1. Release a new version of web-component-styles
  2. — wait for web-component-styles to become available for installation —
  3. Install the new version of web-component-styles into react-components
  4. Release a new version of react-components
  5. — wait for react-components to become available for installation —
  6. Deploy docs for react-components
  7. Install the new version of web-component-styles into angular-components
  8. Release a new version of angular-components
  9. — wait for angular-components to become available for installation —
  10. Deploy docs for angular-components

Some of this work can be done concurrently. Since react-components and angular-components have no dependency on each other, they can be released at the same time. Also, the documentation websites for each component library can be deployed concurrently (assuming that you have two different websites).

Defining the “New Version”

The new version of a package is the version that you just released. This can be the @latest version, an alpha version, or any other specified tag. If you run npm publish without a tag, the tag is set to @latest. See the npm-publish docs for more information. Because you likely don't always want to use the @latest tag, you'll need to keep track of the tag of the package that you're releasing throughout the chained release process.

Let’s come back to the alpha release example. Your tag might be something like @alpha-4b2d9 where 4b2d9 is the git sha of the current commit. The new version here for web-component-styles is web-component-styles@alpha-4b2d9, not web-component-styles, nor web-component-styles@latest. When you install web-component-styles@alpha-4b2d9 into angular-components and react-components, you'll need to run npm install web-component-styles@alpha-4b2d9.

What is the Difference Between This Process and Lerna?

When you run lerna publish, Lerna will publish packages that have changed since the last release (@lerna/publish docs). This is very similar to our release process, except for one key piece: we have chained packages, which means that we have to run npm install <package-name@tag> in some of our packages during the release process.

Should I Avoid Lerna?

Not necessarily! Lerna (as well as similar tools) have many other powerful commands that you can use during development. The point of this article is to explain the basics of replacing the lerna publish script with your own custom script. We didn't cover some pieces of this release process, shuch as pushing your changes to a remote repository hosted on a service like GitHub.

Next Steps

In the next article, we will create a bash script for releasing packages in a design system. This article is an important foundation for understanding how the bash script works, as well as knowing where to modify the bash script for your use case.

Attributions

  1. The React logo is property of Facebook (License)
  2. The Angular logo is property of Google (License)
  3. The Netlify logo is property of Netlify (no license provided)
  4. The CSS logo is property of Rudloff (License)

Design System, Front-End Tooling, and Front-End Infrastructure Engineer at Qualtrics