Writing your first React UI Library - Part 1: Lerna
Published: 2020-06-16 20:53:19

![A Dragon with many heads](https://res.cloudinary.com/carlosdev/image/upload/v1591762244/react-lerna-css/lerna_uhzznq.png)

This is the first post on a series on how to make your own UI React Library.

## What are we going to do?

- Setup a new project using Lerna for multiple packages.
- Bootstrap all the skeleton project with all the packages we will need.
- Add necessary dependencies to packages.
- Wire our own centralized builder.

It might feel sometimes like we are battling with a multi-headed dragon but bare with me as it will pay dividends and the end!

## Pre-requisites

- Node v10+
- NPM v6+

## Lerna

This tool is amazing to manage Javascript libraries with multiple packages, the general idea is that each UI Component in your library will be a completely separate and independent package that can be installed on a project. We are parting ways with a monolithic library where all the code is in a single package in favor of splitting so our clients can _install only what they want™_

Recommendation is to install it globally since we are going to use few commands from it:

```sh
npm i -g lerna
```

### Create Initial project

```sh
# example name that I like
mkdir phoenix
cd phoenix

# Initialize empty package.json
npm init -y

# Initialize Lerna
lerna init
```

Example generated folder structure
![folder structure](https://res.cloudinary.com/carlosdev/image/upload/v1590605102/react-lerna-css/folder-structure_plqu2i.png)

### lerna.json

This is the central lerna configuration for the packages. You can read more about the Lerna configuration here: https://github.com/lerna/lerna#lernajson

Default values

```json
{
  "packages": ["packages/*"],
  "version": "0.0.0"
}
```

We are going to modify to this

```json
{
  "packages": [
    "packages/*"
  ],
  "version": "0.0.0",
  "hoist": true,
  "stream": true,
  "bootstrap": {
    "npmClientArgs": ["--no-package-lock"]
  }
}
```

`hoist`: Makes all dependencies on packages to be lifted up to the root so we de-dupe.
`stream`: Prints all the inner package logs when run.
`npmClientArgs`: Prevents generating package-lock.json on all these packages.

### packages/

This folder will host all the code of the packages we are going to release.

## Let's add some components!

### npm scopes

We want to publish these packages to npm and we want to avoid conflicts with others; For this, we will create a scope. Scopes are great to namespace closely related packages or as a form of veracity, for example `@babel/core`, scope in this case is `@babel`. For this example we will set the scope to: `@cddev`.

Read more of scopes here: https://docs.npmjs.com/about-scopes

#### Important Notes

- The scope `@cddev` is the one I used for this guide so it will be already taken, go ahead and create a new one; one that represents your passions and interests in life :)
- Before using a scope, save yourself a headache and create the **org** in npm first: https://www.npmjs.com/org/create

### Creating packages

Any UI Library is nothing without components so let's create a few packages using `Lerna`. We will create 4 packages for this guide:

- `@cddev/phoenix`: This will hold all the packages together in case someone wants to do a single import.
- `@cddev/phoenix-button`: A button component.
- `@cddev/phoenix-text`: A Text Component.
- `@cddev/phoenix-builder`: A Builder of all components, centralizes rollup, babel, post-css, etc.

**Note**
we are going to use the default `Lerna` folder structure on this tutorial.

```sh

# Using --yes to skip prompts
lerna create @cddev/phoenix --yes
lerna create @cddev/phoenix-button --yes
lerna create @cddev/phoenix-text --yes
lerna create @cddev/phoenix-builder --yes
```

### Wiring the React components with Lerna

We want build relationships <3 within our components, for example the main phoenix package will import all other packages and export them; We also want to add necessary dependencies to all packages to get started. Let's do it.

```sh
# Add phoenix-button dependency into phoenix
lerna add @cddev/phoenix-button --scope=@cddev/phoenix

# Add phoenix-text dependency into phoenix
lerna add @cddev/phoenix-text --scope=@cddev/phoenix

# We are going to use React for the two UI components, let's add it as dev dependency first for local testing
lerna add react --dev --scope '{@cddev/phoenix-button,@cddev/phoenix-text}'

# And as a peer dependency using major 16 version for consuming applications
lerna add react@16.x --peer --scope '{@cddev/phoenix-button,@cddev/phoenix-text}'

# We are also going to use an utility to toggle classes as needed on the components called "clsx"
lerna add clsx --scope '{@cddev/phoenix-button,@cddev/phoenix-text}'
```

With this you should now be able to see some changes across multiple `package.json` setting pointers among the packages so you can reference them.

### Let's write some test React code to export from the UI Components

`phoenix-button/lib/phoenix-button.js`

```jsx
import React from 'react';
const Button = ({ children }) => <button>{children}</button>;
export { Button };
```

`phoenix-text/lib/phoenix-text.js`

```jsx
import React from 'react';
const Text = ({ children }) => <p>{children}</p>;
export { Text };
```

`phoenix/lib/phoenix.js`

```jsx
import { Button } from '@cddev/phoenix-button';
import { Text } from '@cddev/phoenix-text';
export { Button, Text };
```

## The Builder

We could go ahead and publish these packages as they are in ES6 but some older clients might not understand this modern Javascript especially because we are using JSX. This needs to be compiled to a format that can be understood by older clients and for that we are going to use a bundler.

**Rollup** is a good options since it has a minimal API and their docs are great to get started.
https://rollupjs.org/guide/en/

To build these components, wouldn't it be neat to use it like this?

```json
"scripts": {
  "build": "phoenix-builder"
}
```

In this case the builder will be aware of everything passed to it given the context of where we call it.

For this we are going to be creating a command line executable in node.
https://developer.okta.com/blog/2019/06/18/command-line-app-with-nodejs

Let's modify our `@cddev/phoenix-builder/package.json` to let node know we are exposing an executable out of this package. In this case the executable is gonna be `phoenix-builder`.

`phoenix-builder/package.json`

```json
"bin": {
  "phoenix-builder": "./lib/phoenix-builder.js"
},
```

Next, we need to make changes into `phoenix-builder.js` with a dummy command to test things out:

`phoenix-builder/lib/phoenix-builder.js`

```js
#!/usr/bin/env node
console.log('Woo');
```

Finally, make the JS executable

```sh
chmod +x packages/phoenix-builder/lib/phoenix-builder.js
```

We should be able to wire the `phoenix-builder` to our individual components so we have the builder centralized with it's own configuration and be able to run for each component.

```sh
lerna add @cddev/phoenix-builder --dev --scope '{@cddev/phoenix,@cddev/phoenix-button,@cddev/phoenix-text}'
```

And then modify all these packages with a new `build` script
For example, `phoenix-button/package.json`

```json
"scripts": {
  "build": "phoenix-builder",
  "test": "echo \"Error: run tests from root\" && exit 1"
},
```

Next, we should be able to do a test run by doing:

```sh
lerna run build
```

You should be able to successfully see the three `Woo` messages in the console signaling us that it worked.

**Troubleshooting**
If you get an error `phoenix-builder: command not found` make sure you are exporting the `bin` command in `phoenix-builder` package.json

To make the running of the script easier, at the root level let's modify our scripts in our `package.json` and add:
`root/package.json`

```json
"scripts": {
 "build":  "lerna run build"
}
```

With this, we can run at the root `npm run build` and it should do the same without having to call `lerna` every time.

### Compile the JS with Rollup

Now that we have the builder wired up we can start adding **Rollup** and all other dependencies we need to compile our code!

Unfortunately Lerna doesn't support adding multiple packages in one command... sigh.

```sh
lerna add rollup --scope=@cddev/phoenix-builder
lerna add @babel/core --scope=@cddev/phoenix-builder
lerna add @babel/preset-env --scope=@cddev/phoenix-builder
lerna add @babel/preset-react --scope=@cddev/phoenix-builder
lerna add @rollup/plugin-babel --scope=@cddev/phoenix-builder
lerna add @rollup/plugin-node-resolve --scope=@cddev/phoenix-builder
```

You should now have all the necessary dependencies to write the `phoenix-builder.js`

We are going to use the Javascript API in rollup and produce 2 bundles:

1. CommonJS (CJS) for older clients.
2. ECMAScript Modules (ESM) for newer clients.

Let's start by modifying `phoenix-builder.js` with the following code:

`phoenix-builder/lib/phoenix-builder.js`

```js
#!/usr/bin/env node
const rollup = require('rollup');
const path = require('path');
const resolve = require('@rollup/plugin-node-resolve').default;
const babel = require('@rollup/plugin-babel').default;

const currentWorkingPath = process.cwd();
const { main, name } = require(path.join(currentWorkingPath, 'package.json'));

const inputPath = path.join(currentWorkingPath, main);

// Little workaround to get package name without scope
const fileName = name.replace('@cddev/', '');

// see below for details on the options
const inputOptions = {
  input: inputPath,
  external: ['react'],
  plugins: [
    resolve(),
    babel({
      presets: ['@babel/preset-env', '@babel/preset-react'],
      babelHelpers: 'bundled',
    }),
  ],
};

const outputOptions = [
  {
    file: `dist/${fileName}.cjs.js`,
    format: 'cjs',
  },
  {
    file: `dist/${fileName}.esm.js`,
    format: 'esm',
  },
];

async function build() {
  // create bundle
  const bundle = await rollup.rollup(inputOptions);
  // loop through the options and write individual bundles
  outputOptions.forEach(async (options) => {
    await bundle.write(options);
  });
}

build();
```

Now you can run:

```sh
npm run build
```

And you should be able to see compiled version of our components!
Hurray!!🎉🎉🎉🎉

Example of compiled code in `dist` folder on each UI Component package

![example of compiled code](https://res.cloudinary.com/carlosdev/image/upload/v1591766356/react-lerna-css/compiled_clxdmw.png)

## Conclusion

By now you should have a small library with 2 UI React components; One single library that imports them and a centralized builder. This is the skeleton of the overall UI Library. In the next parts we will work on adding Kitchen Sink/Documentation tooling, CSS Modules support and the final touches to be able to distribute our library.

## Resources

Code: https://github.com/davixyz/phoenix/tree/part1
Github: https://github.com/davixyz
Twitter: https://twitter.com/carloscastrodev