Lerna Workspaces - Managing Projects With Multiple Packages

Lerna workspace allows to create/manage various packages, like app (react application), web (react.js application), common (common business logic/code) needs to be implemented both in react native and react.js.

Lerna workspace manages versioning so you can create a package for some of your functionality and want to share with other applications then you can easily integrate in other packages by adding that dependency in package.json like you do for other NPM/YARN packages.

Step by Step Lerna Integration -

If you are using Lerna for the first time, you need to install Lerna Globally.

npm install --global lerna

Let's start by creating Lerna Project,

npx lerna init // initialize lerna workspace

After finish initialization you will get following folder/files directory,

lerna-workspace
  /packages
  lerna.json
  package.json

packages - You can put your web (Web App), app (Mobile App), common (Common Components) inside this directory

lerna.json - Contain configuration for packages

package.json - Contain dependency and lerna workspace settings

Initially in package.json you will get package name "name": "root", we will change it to "name": "@workspace/root", make sure "private": true to share packages under the workspaceSettings.

package.json

{
  - "name": "root",
  + "name": "@workspace/root",
}

Now, Go to lerna.json change it to following,

{
  "packages": [
    "packages/*"
  ],
  + "version": "independent",
  + "npmClient": "yarn",
  + "useWorkspaces": true
 }

Let's change workspace settings in package.json, Change it to following

{
  "name": "@workspace/root",
  "private": true,
  "devDependencies": {
      "lerna": "^4.0.0"
  },
  + "workspaces": {
      + "packages": [
      + "packages/**"
      + ]
  + }
}

We have setup everything in lerna.json and package.json, now lets create React.js application and common component directory

cd packages
npx create-react-app components --template typescript // common component
npx create-react-app app --template typescript // react.js web application

Monorepo hoist package to root, so dependency you have installed, actually installed on root node_modules instead of node_modules on each app component package.

If you see the folder structure, it will looks like,

lerna-workspace
 /node_modules
 /packages
   /app
      package.json
      ...
   /components
      package.json
      ...
 lerna.json
 package.json
 yarn.lock

Now, think you have two application using same components, instead of design & develop components separately, you can add it to /components packages and use that package wherever your want, let's see,

create-react-app-config - CRACO - help us to modify web package configuration, so let's install it,

yarn add --dev craco -W

Now, Let's change the package name for the app and components.

/packages/app/package.json

/packages/app/package.json
{
  - "name": "app",
  + "name": "@workspace/app",
}

/packages/components/package.json

{
  - "name": "components",
  - "name": "@workspace/components",
}

Let's add components dependency into app/package.json

{
  "dependencies": {
    + "@workspace/components": "0.1.0",
      ...
  }
}

We are using craco, so we need to change few settings in app/package.json scripts to following,

{
  "scripts": {
    + "start": "craco start",
    + "build": "craco build",
    + "test": "craco test",
    + "eject": "craco eject"
  }
}

Now, let's switch to root package.json and add scripts, Lerna has powerful scripts commands if you type build here in root package.json it will build for all child packages at the same instance.

/package.json
{
  + "scripts": {
    + "start": "lerna exec --scope @workspace/app -- yarn start"
  + }
}

Now, let's execute it, execute - yarn start, it will give errors and you can't find the modules craco.config.js which we don't have yet.

For instance let's change scripts in /app/package.json to following,

{
  "scripts": {
    + "start": "react-scripts start"
  }
}

And try to execute yarn start it will load your react app successfully. So our web app runs perfectly using lerna workspace.

Now, let's add a button in the web app and perform increment operation and save count value into state.

app/src/App.js

function App() {
  const [count, setCount] = useState(0);
  return (
    <button
      onClick={() => setCount((prev) => ++prev)}
      >
      Increment
    </button>
  )
}

Run the web app, counter increment works perfectly.

Now, let's pull button component in components, go to components directory,

cd components
cd src
mkdir components

Create new file Button.tsx inside packages/components/src/components, add following code,

import * as React from "react";

interface Props {
 onClick: () => void;
}

const Button: React.FC<Props> = (props) => {
 return <button {...props}>Increment</button>;
};

export default Button;

Now, go to packages/components/src/index.tsx and change to following,

import Button from "./components/Button";
export  { Button };

Let's add to packages/app/src/App.js
+ import { Button } from "@workspace/components";

function App() {
  const [count, setCount] = useState(0);

  console.log(Button);
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        + Your count is {count}
        + <Button onClick={() => setCount((prev) => ++prev)} />
     </header>
   </div>
 );
}

export default App;

If you faced any compile error for App.tsx not found then, go to

packages/components/package.json and add

{
  + "main": "./src/index.tsx"
}

We need to hoist our packages so execute,

yarn lerna bootstrap // this will bootstrap application and make shared components/links components
yarn start

After yarn start you will faced error for loaders, because create-react-app webpack contain loaders, so we need to setup following,

cd packages/app/
touch craco.config.js

And add the following code in craco.config.js

const path = require("path");
const { getLoader, loaderByName } = require("@craco/craco");

const packages = [];
packages.push(path.join(__dirname, "../components"));

module.exports = {
 webpack: {
   configure: (webpackConfig, arg) => {
     const { isFound, match } = getLoader(
       webpackConfig,
       loaderByName("babel-loader")
     );
     if (isFound) {
       const include = Array.isArray(match.loader.include)
         ? match.loader.include
         : [match.loader.include];

       match.loader.include = include.concat(packages);
     }
     return webpackConfig;
   },
 },
};

As we have added craco.config.js so let's change scripts settings in /packages/app/package.json

{
  "scripts": {
    + "start": "craco start",
  }
}

And finally yarn starts, web app works fine with using Button (reusable code) from components package.

Lerna Scripts -

test scripts

Lerna allows you to run scripts and execute wherever you want to do in scripts. Let’s add some test scripts in root /package.json

// package.json
{
  + "scripts": {
    + "test": "lerna run test"
  + }
}

Also, add scripts in packages,

// packages/app/package.json
{
  + "scripts": {
    + "test": "echo app packages test scripts"
  + }
}
// packages/components/package.json
{
  + "scripts": {
    + "test": "echo component packages test scripts"
  + }
}

Now, if you run test script, lerna run test it will log run test scripts in two packages (app, components) and you will get log following,

lerna info Executing command in 2 packages: "yarn run test"
lerna info run Ran npm script 'test' in '@workspace/components' in 0.5s:
$ echo component packages test scripts
component packages test scripts
lerna info run Ran npm script 'test' in '@workspace/app' in 0.4s:
$ echo app packages test scripts
app packages test scripts
lerna success run Ran npm script 'test' in 2 packages in 0.9s:
lerna success - @workspace/app
lerna success - @workspace/components

scope scripts

So, you see, lerna runs test scripts in two packages. If you want to test script of specific packages you can do it by giving scope, Change root package.json,

// package.json
{
  + "scripts": {
    + "test": "lerna run test --scope=@workspace/app"
  + }
}

Now, let’s run the script npx run test, It will log following,

lerna notice filter including "@workspace/app"
lerna info filter [ '@workspace/app' ]
lerna info Executing command in 1 package: "yarn run test"
lerna info run Ran npm script 'test' in '@workspace/app' in 0.7s:
$ echo app packages test scripts
app packages test scripts
lerna success run Ran npm script 'test' in 1 package in 0.7s:
lerna success - @workspace/app

You see this time script executed in @workspace/component because we have specified scope.

You can can apply multiple packages in scope by specifying like,

scope with multiple packages

// package.json
{
  + "scripts": {
    + "test": "lerna run test --scope={@workspace/app,@workspace/components}"
  + }
}

It will log following -

lerna notice filter including ["@workspace/app","@workspace/components"]
lerna info filter [ '@workspace/app', '@workspace/components' ]
lerna info Executing command in 2 packages: "yarn run test"
lerna info run Ran npm script 'test' in '@workspace/components' in 0.6s:
$ echo component packages test scripts
component packages test scripts
lerna info run Ran npm script 'test' in '@workspace/app' in 0.3s:
$ echo app packages test scripts
app packages test scripts
lerna success run Ran npm script 'test' in 2 packages in 0.9s:
lerna success - @workspace/app
lerna success - @workspace/components

Lerna Versioning

Lerna contains packages, everytime you build/commit something, it allows you to increment the package version automatically using the following versioning script.

{
  + "scripts": {
    + "new-version": "lerna version --conventional-commits --yes",
  + }
}

Learn more about conventional commit and commitzen.

Conventional commit create Git Tag and ChangeLog and Increment package version for you so you can know what you changed in each release/commit. Let’s run a script, but before that commit your code and run the following.

Execute npm run new-version you will get following logs,

> [email protected] new-version /Users/kpiteng/lerna
> lerna version --conventional-commits --yes

lerna notice cli v4.0.0
lerna info current version 1.0.0
lerna info Looking for changed packages since v1.0.0
lerna info getChangelogConfig Successfully resolved preset "conventional-changelog-angular"

Changes:
 - @workspace/app: 1.0.0 => 1.0.1
 - @workspace/components: 1.0.0 => 1.0.1

lerna info auto-confirmed 
lerna info execute Skipping releases
lerna info git Pushing tags...
lerna success version finished

This will create CHANGELOG.md file for you in both packages, Let’s look it, Go to /packages/common/CHANGELOG.md you will find following,

/packages/common/CHANGELOG.md,

If you see packages/app/package.json you will see version incremented,

// packages/app/package.json
{
  "name": "@workspace/app"
  "version": "1.0.1"
}

// packages/components/package.json
{
  "name": "@workspace/components",
  "version": "1.0.1"
}

diff scripts

Lerna diff script allows the user to check a screenshot of what exactly changed since the last commit, it is more like Git, Bitbucket - it’s showing what you have changed before the commit. So to do that, lets add script in root package.json

// package.json
  {
    "scripts": {
      + "test": "lerna run test --since"
      + "diff": "lerna diff"
  }
}

Also, let’s change something in code, go to /packages/app/src/App.js,

// packages/app/src/App.js
function App() {
  + const [counter, setCounter] = useState(0);
}

Now, let’s run the script npx run diff you will get log following

> [email protected] diff /Users/kpiteng/lerna
> lerna diff

lerna notice cli v4.0.0
diff --git a/packages/app/src/App.js

 module.exports = () => {
   const [count, setCount] = useState(0);
+  const [counter, setCounter] = useState(0);
 }

Thanks for reading Blog!

KPITENG | DIGITAL TRANSFORMATION
www.kpiteng.com/blogs | [email protected]
Connect | Follow Us On - Linkedin | Facebook | Instagram

29