15

Monorepo using Lerna, Conventional commits, and Github packages

 2 years ago
source link: https://dev.to/xaviercanchal/monorepo-using-lerna-conventional-commits-and-github-packages-4m8m
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Prerequisites

Some Javascript and Git knowledge and a Github account. Also, NodeJS 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.

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 is a tool for managing JavaScript projects with multiple packages.

Conventional commits

Conventional commits 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 (Semantic Versioning) specification must be followed.

Github packages

Github 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 making some changes to any of them, we will use Lerna in conjunction with conventional commits to analyze the commit history to determine which packages have changed, the level of affectation of these changes, and determine 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.

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

$ npm init
Enter fullscreen modeExit fullscreen mode

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

$ npm install --save lerna

$ lerna init --independent
Enter fullscreen modeExit fullscreen mode

The following lerna.json 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.

{
  "packages": [
    "packages/*" <-- folder where the packages will be located
  ],
  "version": "independent" <-- versioning strategy
}
Enter fullscreen modeExit fullscreen mode

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

node_modules
Enter fullscreen modeExit fullscreen mode

Our project structure should look like this:

/
  .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
Enter fullscreen modeExit fullscreen mode

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, we'll set the name of the affected package but since the current changes are global, we just want to use a name like root or any other one that you prefer to categorize them:

$ git add .
$ git commit -m "feat(root): adds npm, lerna and packages"
$ git push
Enter fullscreen modeExit fullscreen mode

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 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:

{
  "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"
}
Enter fullscreen modeExit fullscreen mode

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

module.exports = function getDate() {
  return new Date();
};
Enter fullscreen modeExit fullscreen mode

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

$ git add .
$ git commit -m "feat(date-logic): creates package"
$ git push
Enter fullscreen modeExit fullscreen mode

We will now publish the very first version of the package to the Github Packages npm registry, so we can install it in 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:

You can learn more about Github packages authentication in the docs.

  1. Add the following lines to the .npmrc file, which is an configuration file for npm:
@xcanchal:registry=https://npm.pkg.github.com/xcanchal
always-auth=true
//npm.pkg.github.com/:_authToken={YOUR_GITHUB_TOKEN}
Enter fullscreen modeExit fullscreen mode

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

$ npm publish
Enter fullscreen modeExit fullscreen mode

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

npm notice 
npm notice 📦  @xcanchal/[email protected]
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/[email protected]
Enter fullscreen modeExit fullscreen mode

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:

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

Package 2 (date-renderer)

Now, let’s implement our second package, the date-renderer. Create a new date-renderer inside the packages folder 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).

$ npm install --save @xcanchal/date-logic
Enter fullscreen modeExit fullscreen mode

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.

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

(() => {
  console.log(`Date: ${getDate()}`);
})();
Enter fullscreen modeExit fullscreen mode

We can test it to check that it works correctly:

$ node index.js

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

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

/
  package.json
  lerna.json
  packages/
    date-logic/
      index.js
      package.json
    date-renderer/
      index.js
      package.json <-- date-logic installed as a dependency
Enter fullscreen modeExit fullscreen mode

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:

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

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:”

$ git add .

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

$ git push
Enter fullscreen modeExit fullscreen mode

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):

commit 7decbab3aab121c2235e3fa8fd79fe30ad4350c4 (HEAD -> main, origin/main, origin/HEAD)
Author: Xavier Canchal <[email protected]>
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 <[email protected]>
Date:   Thu Sep 23 12:48:59 2021 +0200

  feat(date-renderer): creates package

commit 857efc7057941c254f97d7cf2d49b4f8eae3b196
Author: Xavier Canchal <[email protected]>
Date:   Thu Sep 23 09:48:02 2021 +0200

  feat(date-logic): creates package
Enter fullscreen modeExit fullscreen mode

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 published.

From the root folder, execute the following command (notice the --conventional-commits flag).

$ lerna version --conventional-commits
Enter fullscreen modeExit fullscreen mode

Some logs will appear and Lerna will ask for confirmation in a prompt, showing the packages that will be versioned:

[...]

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

? Are you sure you want to create these versions? (ynH)
Enter fullscreen modeExit fullscreen mode

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:

lerna info execute Skipping releases
lerna info git Pushing tags...
lerna success version finished
Enter fullscreen modeExit fullscreen mode

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

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

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 a 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:

{
  "packages": [
    "packages/*"
  ],
  "version": "independent",
  "command": {
    "publish": {
      "registry": "https://npm.pkg.github.com/xcanchal"
    }
  }
}
Enter fullscreen modeExit fullscreen mode

Commit and publish the Lerna configuration change:

$ git add .
$ git commit -m "feat(root): adds publish registry to lerna config"
$ git push
Enter fullscreen modeExit fullscreen mode

And execute the Lerna publish command:

$ lerna publish from-git
Enter fullscreen modeExit fullscreen mode

Which will also prompt, like in the version stage:

[...]

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

? Are you sure you want to publish these packages? (ynH)
Enter fullscreen modeExit fullscreen mode

We confirm and we get the following output:

[...]

Successfully published:
 - @xcanchal/[email protected]
lerna success published 1 package
Enter fullscreen modeExit fullscreen mode

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

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.

{
...
  "dependencies": {
    "@xcanchal/date-logic": "^2.0.0"
  }
...
}
Enter fullscreen modeExit fullscreen mode

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

$ node index.js
// -> Date: Thursday, September 23, 2021
Enter fullscreen modeExit fullscreen mode

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

(Will be covered in an upcoming article)

  • Setting up a commit syntax checker (e.g. 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.

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

Thanks for reading!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK