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 --amend
flag to amend 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 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!