Monorepo using Lerna, Conventional commits, and Github packages
Tags: lerna, githubpackages, npm, javascript
Published: 2021-09-23 20:22:35

---
title: Monorepo using Lerna, Conventional commits, and Github packages
published: true
description: Easily manage npm packages versioning, publication, and changelog generation.
tags: lerna, githubpackages, npm, javascript
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pkat0pwprem78qbqqzd7.png
---

## Prerequisites

Some [Javascript](https://developer.mozilla.org/en-US/docs/Web/JavaScript) and [Git](https://git-scm.com/) knowledge and a [Github](https://github.com/) account. Also, [NodeJS](https://nodejs.org/es/) has to be installed on your computer. If you don’t have it installed already I recommend doing it using a version manager such as [nvm](https://github.com/nvm-sh/nvm).

## Context

### Monorepo

A monorepo (mono = single, repo = repository) is an approach for managing multiple software projects inside the same repository, often called packages.

### Lerna

[Lerna](https://github.com/lerna/lerna) is a tool for managing JavaScript projects with multiple packages.

### Conventional commits

[Conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) are a convention built on top of commits that consist of a set of rules to follow when writing commit messages. To specify the nature of the changed code, a set of instructions that conform to the [SemVer](https://semver.org/) (Semantic Versioning) specification must be followed.

### Github packages

[Github packages](https://github.com/features/packages) is the package registry of Github. It allows developers to store software packages for some of the most used package registries (Npm, Docker, Maven…). In our case, we'll use the npm one.

## What are we going to build?

We will create a monorepo that will contain two projects (packages). After making changes to any of the projects we will commit them following the conventional commits specification.

After finishing making changes, we'll use Lerna in conjunction with conventional commits for analyzing the commit history and detecting which packages have changed, the level of affectation of these changes, and determining the versions that have to be bumped and published to the registry.

## Hands-on

### Setting up the monorepo

The very first thing to do is to create a new Github repository. I will call it `monorepo`.

![Github repository creation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z532o08znnjhlqcrp1nd.png)

Clone the repository, navigate to the root folder, and execute the following command to initialize the npm project.

```shell
$ npm init
```

After that, install Lerna as a dependency and execute the command to initialize the Lerna project:

```shell
$ npm install --save lerna

$ lerna init --independent
```

The following [lerna.json](https://github.com/lerna/lerna#lernajson) file will be generated. This file is used to configure the different options supported by Lerna. The --independent flag is important because we want that each package in the repo is versioned independently instead of having a single version for all the packages.

```json
{
  "packages": [
    "packages/*" <-- folder where the packages will be located
  ],
  "version": "independent" <-- versioning strategy
}
```

In order to avoid publishing the `node_modules` folder to the repository, create a `.gitignore` file with the following content:

```txt
node_modules
```

Our project structure should look like this:

```txt
/
  .gitignore <-- avoid publish certain files to the repository
  package.json <-- Lerna installed in the root dependencies
  lerna.json <-- Lerna configuration file
  packages/ <-- folder where the packages will be located
```

Now, let's publish these initial changes to the repository following the conventional commits specification (notice that we're using `feat` as the commit type and `root` as the scope). Later, in the `scope` section of the commit, we'll set the name of the affected package but since the current changes are global we'll just pick a name like `root` or any other one that you prefer:

```shell
$ git add .
$ git commit -m "feat(root): adds npm, lerna and packages"
$ git push
```

### Creating the packages

We will create the following two packages:

- `date-logic`: It will export a function that returns the current date.
- `date-renderer`: It will use the `date-logic` to print the current date to the console.

### Package 1 (date-logic)

Create a new folder named `date-logic` inside the `packages` folder, navigate to it, and execute `npm i` to generate its own `package.json` file. After that, apply  the following changes:

1. Add an [npm scope](https://docs.npmjs.com/cli/v7/using-npm/scope) to the `name` attribute to indicate who's the owner of the package. In my case, `@xcanchal`.
2. Add the `repository` attribute, with the URL to the Github repository.
3. Add the `publishConfig.registry` attribute pointing to the Github Packages registry. This specifies the npm registry where the packages will be published.

The `package.json` should look like the following:

```json
{
  "name": "@xcanchal/date-logic", <-- @{scope}/{package-name}
  "version": "1.0.0",
  "description": "A package that returns the current date",
  "main": "index.js",
  "repository": "https://github.com/xcanchal/monorepo", <-- repo
  "publishConfig": { <-- publish config
     "@xcanchal:registry": "https://npm.pkg.github.com/xcanchal"
  }
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Xavier Canchal",
  "license": "ISC"
}
```

Now, we'll implement a very simple script for the `date-logic` package. Create a new `index.js` file with the following content:

```js
module.exports = function getDate() {
  return new Date();
};
```

Let's push the changes to the repo (remember that we have to follow the conventional commits specification). Because the changes are about adding a new feature to the `date-logic`package, we will use the `feat` type of commit and the `date-logic` scope:

```shell
$ git add .
$ git commit -m "feat(date-logic): creates package"
$ git push
```

We will now publish the very first version of the package to the Github Packages npm registry, so we can install it from the second package that we'll implement later (the `date-renderer`).

### Authentication in Github Packages and npm

Before being able to publish packages we have to set up a Github Personal Access Token and modify the `.npmrc` config file to be able to authenticate when executing `publish` or `install` commands.

1. Go to your "Github > Settings > Developer settings > Personal access tokens" and click "Generate new token". Once in the form, set a descriptive name and check the _write:packages_,(_read:packages_ implicit) and _delete:packages_ permissions:

![Personal access token form](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mqykkm1j1e9t4v57k0wk.png)

_You can learn more about Github packages authentication in the [docs](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-npm-registry#authenticating-to-github-packages)._

2. Add the following lines to the `.npmrc` file, which is an [configuration file for npm](https://docs.npmjs.com/cli/v7/configuring-npm/npmrc):

```shell
@xcanchal:registry=https://npm.pkg.github.com/xcanchal
always-auth=true
//npm.pkg.github.com/:_authToken={YOUR_GITHUB_TOKEN}
```

Finally, we can publish our `date-logic` package. To do so, execute the following command from the package folder:

```shell
$ npm publish
```

We’ll see the following output (notice that the version `1.0.0` has been published):

```shell
npm notice 
npm notice 📦  @xcanchal/date-logic@1.0.0
npm notice === Tarball Contents === 
npm notice 61B  index.js    
npm notice 400B package.json
npm notice === Tarball Details === 
npm notice name:          @xcanchal/date-logic                    
npm notice version:       1.0.0                                   
npm notice filename:      @xcanchal/date-logic-1.0.0.tgz          
npm notice package size:  397 B                                   
npm notice unpacked size: 461 B                                   
npm notice shasum:        4e48d9d684539e0125bf41a44ae90d6c6fc4b7df
npm notice integrity:     sha512-DowuECiLPHd55[...]/LV5T/2pFqucQ==
npm notice total files:   2                                       
npm notice 
+ @xcanchal/date-logic@1.0.0
```

Let’s check how this looks in Github. Open a browser and navigate to your Github repository. There, you can see published packages on the bottom-right of the page:

![Github repository page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oc6shzrzvy171xg6ylcs.png)

By clicking the package name you will be redirected to the details page. There, some information such as the installation instructions, the versions published, or the download activity is available.

![Github package page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vf9g9ea3nybhb7bdkq61.png)

### Package 2 (date-renderer)

Now, let’s implement our second package: the `date-renderer`. Create a new `date-renderer` folder under `packages` and repeat the same steps that we did for the `date-logic` package.

Then, install the `date-logic` package as a dependency (remember, the `date-renderer` will use the date-logic to print the value to the console).

```shell
$ npm install --save @xcanchal/date-logic
```

Great, we have installed a package of our Github packages registry! After that, we will create a new `index.js` file and add the following code, which is a simple script that imports the `date-logic` package and executes the function exported there to print the date to the console.

```js
const getDate = require('@xcanchal/date-logic');

(() => {
  console.log(`Date: ${getDate()}`);
})();
```

We can test it to check that it works correctly:

```shell
$ node index.js

// -> Date: Wed Sep 22 2021 22:50:51 GMT+0200 (Central European Summer Time)
```

Our project structure now should look like this (this is how a typical Lerna project looks like):

```txt
/
  package.json
  lerna.json
  packages/
    date-logic/
      index.js
      package.json
    date-renderer/
      index.js
      package.json <-- date-logic installed as a dependency
```

Let’s publish the `date-renderer` package to the Github Packages registry too by running `npm publish` from the package folder.

### Modifying packages

Let’s make some changes to our packages. Modify the code in the `index.js` file of the `date-logic` package to render the date formatted according to a given a locale and some options:

```js
module.exports = function getDate(
  locale = 'en-US',
  options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }
) {
  return new Date().toLocaleDateString(locale, options);
};
```

Before pushing these changes we have to determine the commit type since it will impact the consumers that use our package. Since we have changed the return type of the function from a `Date` object to a `String` we may consider this as a breaking change. In order to specify it using conventional commits, the body of the footer has to be multi-line and the footer line must start with “BREAKING CHANGE:”

```shell
$ git add .

$ git commit -m "feat(date-logic): returns localized date string
BREAKING CHANGE: changes the return type of the getDate function"

$ git push
```

### Leveraging the power of Lerna

Execute `git log` to see the three different commits that we have made up until now (from newest to oldest):

```shell
commit 7decbab3aab121c2235e3fa8fd79fe30ad4350c4 (HEAD -> main, origin/main, origin/HEAD)
Author: Xavier Canchal <xaviercanchal@userzoom.com>
Date:   Thu Sep 23 13:45:02 2021 +0200

  feat(date-logic): returns localized date string
    
  BREAKING CHANGE: changes the return type of the getDate function

commit d2497bbb357d41b0f4ed81e9a5f1af45b38e5fce
Author: Xavier Canchal <xaviercanchal@userzoom.com>
Date:   Thu Sep 23 12:48:59 2021 +0200
  
  feat(date-renderer): creates package

commit 857efc7057941c254f97d7cf2d49b4f8eae3b196
Author: Xavier Canchal <xaviercanchal@userzoom.com>
Date:   Thu Sep 23 09:48:02 2021 +0200
  
  feat(date-logic): creates package
```

Now, we will use Lerna to analyze the conventional commits history to detect which packages have changed and the level of affectation of those changes to determine the appropriate version to be bumped.

Execute the following command from the root folder of the monorepo (notice the `--conventional-commits` flag).

```shell
$ lerna version --conventional-commits
```

Some logs will appear and Lerna will list the packages that will be versioned and will ask for confirmation:

```shell
[...]

Changes:
 - @xcanchal/date-logic: 1.0.0 => 2.0.0

? Are you sure you want to create these versions? (ynH)
```

If we confirm by pressing the `y` key, Lerna will update the `version` attribute in the `date-logic`‘s `package.json` and will push a tag to Github. See the output:

```shell
lerna info execute Skipping releases
lerna info git Pushing tags...
lerna success version finished
```

If we visit the tags page of our Github repo, we can see the created tag:

![Github repository tags](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6la847hlg3dtdtgpd2mk.png)

But there's more! Lerna also generated a particular `CHANGELOG.md` for the `date-logic` package with all the changes history. Pretty neat, right?

![Package changelog](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jn747fskfjnztlpry8oe.png)

We still haven’t published this new version `2.0.0`. To do it we’ll use another Lerna command: `lerna publish` with the `from-git` argument. This argument tells Lerna to decide which versions have to be published by looking at the Git tags, which are used as the source of truth.

But first, we have to extend the Lerna configuration by adding the registry URL under the `commands.publish.registry` attribute in our `lerna.json` file, which now looks like this:

```json
{
  "packages": [
    "packages/*"
  ],
  "version": "independent",
  "command": {
    "publish": {
      "registry": "https://npm.pkg.github.com/xcanchal"
    }
  }
}
```

Commit and publish the Lerna configuration change:

```shell
$ git add .
$ git commit -m "feat(root): adds publish registry to lerna config"
$ git push
```

And execute the Lerna publish command:

```shell
$ lerna publish from-git
```

Which will ask for confirmation too, like in the `version` stage (add a `--yes` flag if you want to autoconfirm):

```shell
[...]

Found 1 package to publish:
 - @xcanchal/date-logic => 2.0.0

? Are you sure you want to publish these packages? (ynH)
```

We confirm and we get the following output:

```shell
[...]

Successfully published:
 - @xcanchal/date-logic@2.0.0
lerna success published 1 package
```

Let’s visit our repository packages page and see how our package now has two different versions published:

![Github package page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cbs5nm4n40w6vgpbkxkp.png)

Now we can use the new version of the `date-logic` package in the `date-renderer`. Update the `date-renderer`'s `package.json` to target from the version `2.0.0` and up and execute `npm install`.

```json
{
...
  "dependencies": {
    "@xcanchal/date-logic": "^2.0.0"
  }
...
}
```

Navigate to the `date-renderer` package folder and execute node `index.js` to see the updated result:

```shell
$ node index.js
// -> Date: Thursday, September 23, 2021
```

And that’s it!

## Conclusion

What have we covered in this article?

- Conventional commits specification.
- Using Github packages as an npm registry.
- Configuring authentication in Github packages and npm.
- Using Lerna in conjunction with conventional commits to version and publish packages, and get a nice CHANGELOG.md file as a bonus.

## Next steps

- Setting up a commit syntax checker (e.g. [commitlint](https://github.com/conventional-changelog/commitlint)) to avoid human mistakes that could impact the versioning due to wrong commit history.
- Automate the package versioning and publication workflow when pushing new code to the repository using Github actions.
- Publish different types of versions: beta versions when pushing to development and final versions when pushing to master as part of the previous Github action. See Lerna’s --conventional-prerelease and --conventional-graduate flags.

The last two steps are covered in this [following article](https://dev.to/xaviercanchal/automatic-versioning-in-a-lerna-monorepo-using-github-actions-4hij).

#### Have you ever used a monorepo for managing packages? Did you use Lerna or any other tool? Don’t hesitate to leave some feedback!

--- 

Follow me on Twitter for more content [@xcanchal](https://twitter.com/xcanchal)

Buy me a coffee:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iey45sdhab43tl0q4m7x.png)