packages

Automating package publishing in JavaScript projects

No Comments

One thing has always been a thorn in my side. Having to remember to bump the package version for my code releases.

For certain projects, this is an essential part of the release because it ensures that customers will receive the latest changes. Moreover, we are obligated to bump the package version whenever we make any change. And that’s fine, because other developers have to be sure that they will always have the same content when pulling the package with the same version, at different times. In short, if we create some kind of package that will be shared with others, versioning is not just a good practice but a necessity. However, manual bump of the package version could led to several common problems:

  • When we forget to do it, it requires another pull request review just for that simple change. It may take time.
  • Developers forget the bump, their peers forget to review, then we have released the “same version” of our application.

These problems continue to grow with every new developer coming to the project.

In this article, we will learn how we can automate package versioning and publishing with Commitizen and Lerna.

Terminology

Before we start, let’s go through some of the concepts and tools.

What is Sematic Versioning?

You have probably seen different kinds of versioning many times. If you have ever fiddled with files like package.json, you must have seen versioning annotation like, for example, >=1.2.1. Each package manager has its own flavor of versioning annotation, but all of them have one thing in common – Semantic Versioning or SemVer in short.

SemVer works by structuring each version identifier into three parts, MAJORMINOR, and PATCH, and then putting these together using the familiar “MAJOR.MINOR.PATCH” notation. Each of these parts is managed as a number and incremented according to the following rules:

  • PATCH is incremented for bug fixes, or other changes that do not change the behavior of the API.
  • MINOR is incremented for backward-compatible changes of the API, meaning that existing consumers can safely ignore such a version change.
  • MAJOR is incremented for breaking changes, i.e. for changes that are not within the backwards compatibility scope. Existing consumers have to adapt to the new API, most likely customizing their code.

Semantic Versioning

Conventional Commits

Conventional commits

The Conventional Commits specification is a lightweight convention on top of commit messages. It provides an easy set of rules for creating an explicit commit history, which makes it easier to write automated tools on top. With these tools we can do things like:

  • Automatically generate changelogs.
  • Automatically determine semantic version bumps.

The commit message should be structured as follows:

<type>[optional scope]: <description>

[optional body]

[optional footer(s)]

A simple real-world example can look like this:

fix(inventory): fixed table scrolling

Commitizen

Commitizen is a simple wizard which guides the developer during the commit message creation, gathering all required information for well-formed conventional commits.

Lerna

Lerna is a tool that optimizes the workflow around managing multi-package repositories with Git and npm. It allows us to manage our project using one of two modes: Fixed or Independent.

Project setup

We will use a simple create-react-app monorepo template with yarn workspaces. You can also use your own monorepo for which you want to introduce automatic package publishing.

Having that said, let’s start with cloning a cra-typescript-monorepo-template repository. Additionally, we have to install our dependencies via yarn command.

For easier tracking of changes, push this repo to your personal GitHub account, or any other source control platform.

Now that we have our repo ready, let’s see what our project structure looks like:

This project has a workspace called packages that contains two packages: app and shared.

The shared package is a dependency of an app package. We can confirm that looking into the app’s package.json:


After a brief explanation of our example project, we can continue with setting up the remaining tools.

Installing Lerna

Lerna is a CLI (command line interface), so let’s install it globally:

npm install -g lerna

After successful installation, the next step is to initialize Lerna in the project. We will use independent mode with -i. That way, Lerna will increment package versions independently of each other:

lerna init -i

This creates a lerna.json configuration file in the root of the project and adds Lerna to devDependencies in the package.json.

In order to set up Lerna with yarn workspaces, we need to extend our lerna.json, by adding yarn as our npmClient and specifying that we are using yarn workspaces.

By default, Lerna points its packages to the packages folder, so we are good here:

Lerna provides the run command which will run an npm script in each package that contains that script.

For instance, let’s say all of our packages follow the structure of the app package:

and in each package.json we have the test npm script

then Lerna can execute each test script with:

lerna run test --stream

*— stream flag just provides output from the child processes

Let’s now add Lerna run commands in the root package.json, for running, building, and testing our project.

Simply performing yarn test, we should see the following output:

lerna tests

Installing Commitizen

It is possible to write commits in conventional style, but why bother? Commitizen can help us format commit messages with a series of prompts that are used to generate a commit message. Later, these commit messages will be analyzed to determine the next version.

First, let’s install the Commitizen CLI tools globally:

npm install -g commitizen

Next, we need to choose an adapter to create the changelogs. The adapter tells us which template our contributors should follow. We will use the conventional changelog adapter:

commitizen init cz-conventional-changelog -D -E

-D; –save-dev: Package will appear in devDependencies.
-E; –save-exact: Saved dependencies will be configured with an exact version rather than using npm’s default semver range operator.

In case you prefer npx instead of installing Commitizen:

npx commitizen init cz-conventional-changelog -D -E

The above command does three things for us:

  1. Installs the cz-conventional-changelog adapter npm module
  2. Saves it to package.json’s dependencies or devDependencies
  3. Adds the config.commitizen key to the root of our package.json

Now we are set to run our first commits through Commitizen.

commitizen

Once we have Commitizen installed, let’s also set Lerna to read conventional commits by additionally configuring the lerna.json file:

  1. Add the publish command and set it to conventional commits.
  2. Add the version command commit message to be correct format.

Installing Verdaccio

In order to have somewhere to publish our packages and for better understanding, we need some npm registry.

Verdaccio is not the only tool for a private npm registry. I choose it because it is the easiest to set up.

Verdaccio is a simple, zero-config-required local private npm registry.

Setting up Verdaccio is simply about executing steps from the official documentation:

npm install -g verdaccio
npm set registry http://localhost:4873
npm adduser --registry http://localhost:4873
// run verdaccio
verdaccio

If everything goes well, we should see an empty registry when we navigate to
http://localhost:4873.

verdaccio empty registry

Publishing packages

After we successfully set up Lerna, Commitizen, and Verdaccio, it is time to publish our first package. But before we really publish something, let’s once more revisit our packages.

As we already mentioned, shared package is a dependency of the app. However, we want only shared to be published. For Lerna to know which package to publish and which not, we have to scope our packages.

Scoping packages

Alright then, for shared packages, we will add a "private": false field in the package.json file and for the app, we will set it to true. That way only shared packages will be published to our private registry.

Just for a start, let’s commit the changes we made so far and publish the initial version of our shared package.

Simply, add the files to be committed:

git add --all

Commit with git cz and answer the questions.

commitizen questions

After that, execute:

lerna publish

When run, this command calls lerna version behind the scenes. A few seconds later, our console output should inform us that we have successfully published the shared package:

lerna initial publish

Our personal instance of Verdaccio should look like this:

verdaccio with package in registry

Dependencies

Let’s now make a small change to the Text.tsx component and change the html tag h1 to h2. Commit with git cz and answer the questions.
We are ready to publish these changes. Since we used Conventional Commits convention, we do not have to worry about modifying CHANGELOG.md and figure out the proper version of new releases. Simply run:

lerna publish

lerna publish

Here we can see that even though we did not make any changes to the app, it had its version patched because it depends on the shared package.
The new version of the shared package should reflect in Verdaccio as well.

That’s it! If we check the commits on GitHub, we can see that it added a Publish commit where it increased the version of both, app and shared packages, to 0.1.2.

lerna publish commit

Let’s also have a look at the generated CHANGELOG files:

app changelog after lerna publish

shared changelog after lerna publish

Installing Commitlint

We have Lerna to take care of the package versions. However, we also need to make sure that all commits have the correct format. Therefore, we will use commitlint.

Commitlint will help us adhere to a commit convention. Supporting npm-installed configurations, it makes sharing of commit conventions easy.

Let’s install commitlint with the conventional format, and configure it:

yarn add @commitlint/{cli,config-conventional} -D -W
echo "module.exports = {extends: ['@commitlint/config-conventional']};" > commitlint.config.js

Alternatively, the configuration can be defined in .commitlintrc.js, .commitlintrc.json, or .commitlintrc.yml file or a commitlint field in package.json.

Installing Husky

Husky is a tool that allows us to easily wrangle Git hooks and run the scripts we want at those stages.

To install Husky, execute:

yarn add husky -D -W

Once the package is installed, we will add Git hooks directly into our package.json via the husky.hooks field:

Using commit-msg gives us exactly what we want:

  • It is executed whenever a new commit is created.
  • Passing Husky’s HUSKY_GIT_PARAMS to commitlint via the -E|–env flag directs it to the relevant edit file. -e would default to .git/COMMIT_EDITMSG.

If we now try a git commit with a message that is not well formatted …

git commit -m "install commitlint and husky"

… Husky and commitlint will stop us with the following error:

husky and commitling error message

However, if we commit with Commitizen …

git cz

successfully commit with commitizen

… voilà, commit passed successfully! 🙂

Conclusion

For anyone working on a fairly large project, having a code versioned according to these standards can definitely make life easier. First, there is a history to go through. Second, if the commits are self-explanatory, there is also a good documentation for free. Last but not least, we no longer have to remember to bump the package version for our code releases.

The final project can be found on GitHub.

Dragan loves to write code in a high-paced and challenging environment with an emphasis on using best practices to develop high quality software that meets project requirements. He currently prefers to develop with React and TypeScript. He enjoys learning new technologies and sharing findings with his colleagues.

Comment

Your email address will not be published. Required fields are marked *