loading...

Lerna - Semantic Versioning simple project setup

alecap7 profile image Alessandro Capogna ・5 min read

This document contains a step by step guide to implement a minimal example to test how lerna - semantic versioning works.

Inspired by this blog post.

Source code here.

Lerna project setup

I always prefer to not install dependencies as global if it is possible. So here i'm using lerna as a dev dependency:

yarn init
yarn add lerna -D
yarn lerna init --independent

Note: on initialization it is essential to set the independent flag. It allows us to manage sub-projects versioning independently instead of having a single version for all of them.

Lerna configs setup

With Semantic Versioning there is not one and only rule that establishes how the version of a project should be increased. However, lerna in combination with the Conventional Commits format proposes a solution to automatically determine semantic version bumps and generate change logs. In summary, if a project (or sub-project in our case) has been updated and its commit message implies:

  • a BREAKING CHANGE, then bumps the MAJOR number
  • a FEATURE, then bumps the MINOR number
  • a REFACTOR / BUG-FIX, then bumps the PATCH number

Conventional Commits flag setup

In the process of releasing changes, that is achieved by running

yarn lerna publish

lerna will check the updated sub-projects and will create and push a new commit with version bumps and change logs (it will also update packages inter-dependencies).

To instruct lerna that we have chosen to follow the Conventional Commits format we must specify:

// lerna.json
...
"command": {
  "publish": {
    "conventionalCommits": true
  }
}
...

Publish message setup

It can be also useful to customize the publish commit message like this

// lerna.json
...
"command": {
  "publish": {
    ...
    "message": "chore(release): publish"
    ...
  }
}
...

Otherwise, it will be "Publish" by default

Github private package registry setup (optional)

As final step, updated packages (if public, that is "private" field is setted to false or just missing in the relative package.json) will be published to a registry. To have them published on our private github registry you simply need to add this:

// lerna.json
...
"command": {
  "publish": {
    ...
    "registry": "https://npm.pkg.github.com/"
    ...
  }
}

Then, before a release, make sure you are logged into your github-npm account. Otherwise run

npm login --registry=https://npm.pkg.github.com/

Where your password will be a repo and packages scoped github personal access token.

Note: other configurations related to this step will also be made at sub-projects level. We will see it in the next section ...

An important note on the lerna publish command

As mentioned above when "private" field in a package.json is missing or setted to false lerna will try to publish your package on a registry.
If it is so and there is no configuration related to the registry url, lerna will try to publish it in the npm public registry by default.

This could be an unwanted behavior! 🙀

If you don't want to publish any package then consider using one of these two commands for the project release:

yarn lerna publish --skip-npm

Or

yarn lerna version

Otherwise make sure you wrote all the necessary configurations: define registry urls for what you want to publish or set packages as private for what you don't want to publish!

Final result

Your final config file will look like this:

// lerna.json
{
  "packages": ["packages/*"],
  "version": "independent",
  "command": {
    "publish": {
      "conventionalCommits": true,
      "message": "chore(release): publish",
      "registry": "https://npm.pkg.github.com/"
    }
  }
}

Coding packages

Here is the code for our three subprojects (thanks to which I will explain some concepts in the next section):

  • alpha
  • beta
  • usage

Where alpha and beta are independent packages, while usage depends on alpha and beta.

Note: remember to change @ your-username and github repo occurrencies

alpha

mkdir packages/alpha
touch packages/alpha/index.js
touch packages/alpha/package.json
// packages/alpha/index.js

module.exports = "alpa";
// packages/alpha/package.json

{
  "name": "@your-username/alpha",
  "repository": "git@github.com:your-username/your-repo-name.git",
  "version": "0.0.0",
  "publishConfig": {
    "registry": "https://npm.pkg.github.com/"
  }
}

beta

mkdir packages/beta
touch packages/beta/index.js
touch packages/beta/package.json
// packages/beta/index.js

module.exports = "beta";
// packages/beta/package.json

{
  "name": "@your-username/beta",
  "repository": "git@github.com:your-username/your-repo-name.git",
  "version": "0.0.0",
  "publishConfig": {
    "registry": "https://npm.pkg.github.com/"
  }
}

usage

mkdir packages/usage
touch packages/usage/index.js
touch packages/usage/package.json
// packages/usage/index.js

const alpha = require("@your-username/alpha");
const beta = require("@your-username/beta");
console.log(alpha + " " + beta);
// packages/usage/package.json

{
  "name": "@your-username/usage",
  "version": "0.0.0",
  "repository": "git@github.com:your-username/your-repo-name.git",
  "dependencies": {
    "@your-username/alpha": "^0.0.0",
    "@your-username/beta": "^0.0.0"
  },
  "publishConfig": {
    "registry": "https://npm.pkg.github.com/"
  }
}

From the project root directory, run

yarn lerna bootstrap

to install dependencies. Note that internal dependencies will be automatically linked by lerna.

Now cd to the usage package and run

node index

You should see "alpa beta" logged into the terminal.

Working with repo

Because commit messages affects how version is bumped and change logs are generated, each commit will be scoped to a particular feat or fix for a particular package.

Monorepo structure

We are going to commit monorepo boilerplate first

git add .
git reset -- packages

so that we are staging all files except our packages. Then

git commit -m "feat: created a lerna monorepo"

Note that for this particular commit the Conventional Commits format is irrelevant, but it can be a good exercise to apply it right now.

alpha package

git add packages/alpha
git commit -m "feat: added export of package name"

beta package

git add packages/beta
git commit -m "feat: added export of package name"

usage package

git add packages/usage
git commit -m "feat: added export of concatenated package dependencies name"

First publish

git push origin master
yarn lerna publish

Now we have our packages published at version 0.1.0 (remember how bumping works) and change logs generated per sub-project level.

Check it out!

Time to fix

You may have already noticed that the alpha package has a typo in the name it exports. is "alpa" instead of "alpha". We need to fix it

// packages/alpha/index.js

module.exports = "alpha";

Now we can release a patched version for alpha

git commit -am "fix: typo in message"
git push origin master
yarn lerna publish

You can see also usage package has bumped because of a dependency to alpha that has been automatically updated by lerna.

Conclusions

If you work in a team on a fairly complex project, I think having a code versioned according to these standards can simplify your life and save it in some cases! First, you have a history to go through and if the commits are self-explanatory you also have good documentation for free. Second (valid in general for any type of versioning), in case of emergency in production you can always switch back to a stable version of your product!

Discussion

pic
Editor guide