DEV Community

loading...
Cover image for Lerna Workspaces - Managing Projects With Multiple Packages

Lerna Workspaces - Managing Projects With Multiple Packages

kpiteng
CEO w KPITENG | Digital Transformation | Web, Mobile, AR/VR, AI/ML, Blockchain App Development | UI/UX Design, IoT Solutions In India, USA
・7 min read

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.

Please download full source code from our GitHub.

Related Article - Share Code Between React Native & React.js

Learn more about Yarn Workspaces - Monorepo Approach

Step by Step Lerna Integration -

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

npm install --global lerna
Enter fullscreen mode Exit fullscreen mode

Let's start by creating Lerna Project,

npx lerna init // initialize lerna workspace
Enter fullscreen mode Exit fullscreen mode

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

lerna-workspace
  /packages
  lerna.json
  package.json
Enter fullscreen mode Exit fullscreen mode

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",
} 
Enter fullscreen mode Exit fullscreen mode

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

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

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/**"
      + ]
  + }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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",
}
Enter fullscreen mode Exit fullscreen mode

/packages/components/package.json

{
  - "name": "components",
  - "name": "@workspace/components",
}
Enter fullscreen mode Exit fullscreen mode

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

{
  "dependencies": {
    + "@workspace/components": "0.1.0",
      ...
  }
}
Enter fullscreen mode Exit fullscreen mode

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

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"
  + }
}
Enter fullscreen mode Exit fullscreen mode

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

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>
  )
} 
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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

packages/components/package.json and add

{
  + "main": "./src/index.tsx"
} 
Enter fullscreen mode Exit fullscreen mode

We need to hoist our packages so execute,

yarn lerna bootstrap // this will bootstrap application and make shared components/links components
yarn start
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
   },
 },
};
Enter fullscreen mode Exit fullscreen mode

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

{
  "scripts": {
    + "start": "craco start",
  }
} 
Enter fullscreen mode Exit fullscreen mode

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"
  + }
}
Enter fullscreen mode Exit fullscreen mode

Also, add scripts in packages,

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

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
Enter fullscreen mode Exit fullscreen mode

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"
  + }
}  
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}"
  + }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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",
  + }
}
Enter fullscreen mode Exit fullscreen mode

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,

> lerna@1.0.0 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
Enter fullscreen mode Exit fullscreen mode

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

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

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);
}
Enter fullscreen mode Exit fullscreen mode

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

> lerna@1.0.0 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);
 }
Enter fullscreen mode Exit fullscreen mode

Please download full source code from our GitHub.

Thanks for reading Blog!

KPITENG | DIGITAL TRANSFORMATION
www.kpiteng.com/blogs | hello@kpiteng.com
Connect | Follow Us On - Linkedin | Facebook | Instagram

Discussion (0)