secture & code

Monorepo with Lerna and Yarn Workspaces

A monorepo, contrary to what its name suggests, is not a bonobo jumping from branch to branch of our repository. A monorepo is a repository that hosts multiple projects.

Hosting several projects in a single repository may seem like a big mess, but it offers some advantages. Obviously, we are talking about related projects, it would not make much sense to put in the same repository projects for different clients, for example.

monorepo

Advantages

Suppose we start a new project. It must include a mobile app created with React Native, a web app with Vue and a desktop app created with Electron. In addition, we want to have a mocks server to be able to work locally, regardless of whether the backend is to be developed. As you can already guess, most of the code of the applications will be in many cases the same, only the visual presentation with the components, routers, etc, of each framework will be different. On the other hand there are many dependencies that would also be repeated in several projects.

Working in separate repositories, it is more than likely that we will repeat a lot of code between the three applications, in addition to having to set up a mocks server for each one, which is anything but efficient.

Tools for managing a monorepo

Lerna

Lerna is a Javascript/Typescript oriented monorepo tool, used among others by React or Jest. Among other things it allows us to:

  • Execute tasks, either for the entire repository or for one or several packages.
  • Caching of the result of the tasks. We can, for example, do a build and then make changes to a specific package. When redoing the build, will only be made from the modified package, caching the rest.
  • Versioning and publishing. Lerna allows us to launch and publish releases of our packages, keeping the semantic versioning separate for each of the packages.

Yarn Workspaces

Yarn Workspaces is used by Lerna at a lower level. It allows us to launch a single installation of dependencies and ensures interdependencies between packages. It uses a single yarn.lock for all packages, thus ensuring better stability between them. 

When we use one of our packages as a dependency on another, Yarn creates a symbolic link in node_modules to the package we depend on, always using our most up-to-date code.

Creating a monorepo with Lerna

We are going to create a React application and a pure Typescript utility application. First we will install Lerna globally:

Bash
$ npm i -g lerna

To create our monorepo with Lerna, we place ourselves in the directory of our project and launch the following command:

Bash
$ lerna init --independent --packages="packages/*" --skip-install

What are these modifiers?

  • --independent. With this flag we tell Lerna to maintain a separate versioning for the packages
  • --packages. We indicate the route where to find our packages.
  • --skip-install. It does not perform dependency installation. By default Lerna uses npm, but we will use Yarn.

Once initialized, we install the dependencies with Yarn:

Bash
$ yarn install

This will leave us with the following directory tree:

vzsmfAwnvs93HJZ1I6NHFLZTxJTYNukvfTS WWP6PLzcB NEZUmWYD38KIHAWWGU8sNHgMfsPW lTezbym7XI539sOcZnUDsb0r3qfuavk3KhR19YfqAhG5GwBdYynm2UAy sJPkYi0Pd sanJaZEVXk

React application

We will create our packages folder, and then, placing ourselves inside the newly created directory, we will create a React application:

Bash
$ create-react-app awesome-react --template typescript

Once the whole create-react-app party is finished, we notice that a file has not been created. .lock in the React application directory, and that, in addition, the folder node_modules corresponding to our application has only one folder:

lBJZZ4FJB94OaxKE fi OGHK8es jAXsHWoZ0eCotDV1cFTt07LHvivqCnTI NSj5BMFiMnY0pgJC60ve9nfpF

It is convenient to give an identifying prefix to your app in its package.json:

JSON
"name": "@awesome/awesome-react.",

Our dependencies are located in the node_modules folder of the root directory and are registered in the file yarn.lock also from the root directory.

We can also see that our application is referenced in the file yarn.lock:

34YegCVItgVzcXFgnboJ3niLaLO4NqQV0GKlDWYFPERN9Orr9KjU0WQK8evTn6miIOYMiW3 OhU4t MbHghj5Y84FZ7JzVDJpkWizjz8CMnBRD9 cn1 W49cVtJfV jD5cNQMzCn5Zrp063h7iuE0Ec

To launch our application we can do as usual, go to its directory and launch yarn start. But let's take advantage of Lerna's bounties.

In the file package.json directory, we can centralize the scripts of all our packages by using the command lerna exec. In this case we will create the following script to launch our application:

JSON
"scripts": {
   "start:react": "lerna exec --scope @awesome/awesome-react -- yarn start"
 }

What is happening here? Via --scope we indicate which package is the one we are interested in, in this case @awesome/awesome-react is the name of our React application. Then, after the double dash, we indicate the command we want to launch from the package in question.

Utility package

We are going to create a utility package for use in the React application. To do this we will create a folder called utilities in the directory packages. Placing ourselves in that folder, we initialize the Typescript project. Let's first create the file package.json:

Bash
$ yarn init

After answering a few questions we will have created our package.json that we will have to edit. First let's install Typescript:

Bash
$ yarn add -D typescript

We edit the package.json. It will look like this:

JSON
{
 "name": "@awesome/utilities",
 "version": "0.1.0",
 "description": "Some awesome utilities",
 "main": "dist/AwesomeCounter.js",
 "types": "dist/Awesomecounter.d.ts",
 "license": "MIT",
 "private": true,
 "scripts": {
   "build": "tsc"
 },
 "devDependencies": {
   "typescript": "^5.2.2"
 }
}
  • On the property main we indicate the transpiled file of our utilities.
  • The entry types indicates the file path .d.ts that will be generated when creating the build.
  • We create a script to make the build of our package.

In the file tsconfig.json we also have to make some modifications:

JSON
"declaration": true,
"sourceMap": true,
"outDir": "./dist", 
  • statement: true will cause a .d.ts file to be generated.
  • sourceMap: true will generate a sourceMap file for debugging.
  • outDir: "./dist" indicates dist as the directory of the generated files.

Once this is done, we create a src folder and a utility class:

TypeScript
// AwesomeCounter.ts
export default class AwesomeCounter {
 static increase(amount: number): number {
   return amount + 1;
 }
}

We already have a great utility that we can't live without. Software development will never be the same again.

Next we make the build. From the folder package/utilities:

Bash
$ yarn build

Or we can create a script in the file package.json of the root:

JSON
"build:utilities": "lerna exec --scope @awesome/utilities -- yarn build"
Bash
$ yarn build:utilities

This action will generate the following file tree:

pVPTnA 5mLhHd4ii0w2uluFPMozLuQQQrnV1zcldyooXIqcM39P0SbGPFnU7FUua6hpbtb3G16y2veLtMzk P1ITCxselqNr vUAmPxyvbUbpxMPaJXHoURB7VQuNaiS8l38leCJOu63 cBfaZvs 6fQ

Use of the utility package

We are going to create an amazing counter application in our React app. First we will add our internal dependency in the package.json of our application as follows:

JSON
"@awesome/utilities": "*",

To make our packages available to the other packages in our project, we must launch a yarn install. This will create symbolic links in the folder node_modules of the root of the project:

Once this is done, we are going to create a component in the React application. Being extraordinarily original and creative, we will call it Counter, for example. What you are going to see next does not make much sense (none, actually), but it is simply to exemplify the use of a package inside another package in a monorepo environment:

TSX
import { useState } from 'react';
import AwesomeCounter from '@awesome/utilities'


const Counter = (): JSX.Element => {
 const [count, setCount] = useState<number>(0);


 const increaseCounter = (): void => {
   setCount(AwesomeCounter.increase(count));
 };


 return (
   <>
     <p>{count}</p>
     <button onClick={increaseCounter}>Click!</button>
   </>
 );
};


export default Counter;

The counter utility is imported in exactly the same way as any other dependency as it is available alongside the others.

We import our counter in the file App.tsx created by default (because there is no need to complicate your life):

TSX
import React from 'react';
import logo from './logo.svg';
import './App.css';
import Counter from './components/Counter'.';


function App() {
 return (
   <div className='App'>
     <header className='App-header'>
       <img src={logo} className='App-logo' alt='logo' />
       <p>
         Edit <code>src/App.tsx</code> and save to reload.
       </p>
       <a
         className='App-link'
         href='https://reactjs.org'
         target='_blank'
         rel='noopener noreferrer'.'
       >
         Learn React
       </a>
       <Counter />
     </header>
   </div>
 );
}


export default App;

We launch the React application, and if everything went well, we will be able to see in our browser an amazing counter that increases every time we click on the button. Fascinating.

image 2 1

Versioning

Lerna allows us to automatically version our packages independently. To do this we must previously make a commit with the files that we have modified and then we execute the following command:

Bash
$ lerna version

We will get a prompt as the following for each package affected by the changes:

Bash
$ lerna version
info cli using local version of lerna
lerna notice cli v7.3.0
lerna info versioning independent
lerna info Looking for changed packages since @awesome/awesome-react@0.1.1
? Select a new version for @awesome/awesome-react (currently 0.1.1) (Use arrow keys)
 Patch (0.1.2)
  Minor (0.2.0)
  Major (1.0.0)
  Prepatch (0.1.2-alpha.0)
  Preminor (0.2.0-alpha.0)
  Premajor (1.0.0-alpha.0)
  Custom Prerelease
  Custom Version

We had made a modification in the utilities package, on which the React application depends, so it asks us for the version of the two packages. We select the version type, for example ‘Minor’. Once we have selected the version type in both cases we are asked for confirmation.

Bash
? Select a new version for @awesome/utilities (currently 0.1.1) Minor (0.2.0)

Changes:
 - @awesome/awesome-react: 0.1.1 => 0.2.0 (private)
 - @awesome/utilities: 0.1.1 => 0.2.0 (private)

? Are you sure you want to create these versions? (ynH)y
lerna info execute Skipping releases
lerna info git Pushing tags....
lerna success version finished

We can check our repository to see that a new commit and we have new labels:

hLc9Fb O cSOupa9T0WGfSudSNzuvhtQJLL6gb43TC qBxKFUIENw7

2dRrwXUZX7E oF

Deprecated commands

In many Lerna tutorials you will see the commands lerna add, lerna link y lerna bootstrap. You should note that these commands have been removed in Lerna version 7..

References

Lerna: https://lerna.js.org/

Yarn Workspaces: https://classic.yarnpkg.com/lang/en/docs/workspaces/

Semantic Versioning: https://semver.org/

Salesman of creepelos

Picture of Dani Cabal

Dani Cabal

Trainee tamer
Picture of Dani Cabal

Dani Cabal

Trainee tamer

We are HIRING!

What Can We Do