Creating Monorepos using Lerna

Today I want to share a library that I’ve recently learned called Lerna. It’s a tool for managing monorepos. Monorepos are repositories that contains multiple packages. Many projects like React and Babel develop their packages in a single repository.

Before we dive into learning more about Lerna, let us first create our root project directory.

mkdir lerna-project && cd lerna-project

Lerna Versioning Modes

Lerna has two versioning modes: Fixed and Independent mode.

Fixed mode will tie all package versions into single version kept inside lerna.json file under the version key. As mentioned in the documentation, the issue with this approach is that, when you bump one package with a new major version, the rest of the packages gets updated to the same version.

With the Independent mode, you get to independently increment the version of a package that has only been changed.

I prefer using the Independent mode, so for this example, I’ll be initializing a new lerna project using the lerna init command with the -i or --independent flag to set it to Independent mode.

The default mode is Fixed mode, so you may opt out to use the flag if you prefer having the same version for all the packages.

lerna init -i

By running the command above, it will create a package.json, lerna.json file and a packages folder.

.
├── lerna.json
├── package.json
└── packages

packages folder will contain all the packages. Lerna has a command for generating a new package for you which we will do in the next section.

Optional

You can optionally add .gitignore file and add node_modules, lerna-debug.log* and npm-debug.log* to the list of files to be ignored.

Make sure to git commit all the changes before proceeding.

Creating a Public Package

As mentioned previously, we can use the command lerna create to create a lerna-managed package.

lerna create @onoya/utils -y

In this example, I am scoping all my packages under @onoya.

If you’ll look at the packages folder, utils folder is being created instead of @onoya/utils. Lerna is able to detect that we’re trying to create a scoped package.

The -y or --yes flag is used to skip all prompts.

Let’s update the content of packages/utils/lib/utils.js file and add a simple add function:

function add(...values) {
    return values.reduce((sum, x) => sum + x);
}

module.exports = {
  add,
};

Don’t forget to commit the changes.

Publishing Lerna-Managed Packages

If you don’t have your project pushed to a git remote for some reason, you can still publish it with two steps.

First we have to bump the package version using lerna version command:

lerna version patch -y --no-push

The command above will bump the Patch version of @onoya/utils as we specified the patch semver bump. This means that the package version will be updated from 1.0.0 to 1.0.1. You may opt to not specify any semver bump and it will prompt you to specify a version. Remember to also remove the -y flag to prevent it from skipping the prompt.

The --no-push flag will prevent lerna from pushing the tagged commit to git remote.

You can also optionally add the --ammend flag to ammend the version changes to the current commit, instead of creating a new commit for version bumps. I do prefer separating the commit for version bumps, so I didn’t to use the flag.

Once we got the version bumped, we can publish it with lerna publish command.

lerna publish from-git -y

We specified from-git to publish packages tagged by lerna version.

Creating a Private Package

There are times we don’t want to publish a specific package to a registry. We can simply prevent it from being published by adding "private": true option inside the package.json file.

If you are creating a new package, you may just add the --private flag to automatically add the option.

lerna create @onoya/app --private -y

Adding a local package as a dependency to another local package

Let’s try to import the add function in @onoya/utils to @onoya/app. Normally, we would install @onoya/utils using npm install under @onoya/app project. But lerna has a command lerna add to easily add a package for a specific package.

One other advantage of using lerna add is that it creates a symlink, instead of actually installing the package from the registry if the package is part of the lerna-managed local package.

lerna add @onoya/utils --scope=@onoya/app

By add a --scope flag, we’re able to explicitly tell lerna to install @onoya/utils to @onoya/app.

lerna add can also be used to install third party packages.

Now we’re able to add @onoya/utils as our dependency. Let’s update the content of @onoya/app‘s main file (packages/app/lib/app.js):

const utils = require('@onoya/utils');

function app() {
    const sum = utils.add(2, 2, 2);
    console.log(sum);
}

app();

At this point, we can test it out if we’re able to successfully import the function from @onoya/utils by running this command on your terminal:

node ./packages/app/lib/app.js

It should log 6. You can also test it out if it is really being symlinked, by changing the operation used inside the add function from + to -, and re-run node ./packages/app/lib/app.js. It should now return -2.

Now if you try to publish it again, you’ll notice that the version does get bumped, but it won’t actually be published, since @onoya/app is a private repository. If in case you’ve committed the changes we’ve made to the add function in @onoya/utils package, it will only publish @onoya/utils.

We’re able to easily create a multi-package repository or a monorepo, with the help of Lerna.

I’ll be going through how we can handle prereleases with Lerna on a future post. So stay tuned!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.