Monorepos: Lerna, TypeScript, CRA and Storybook combined
Tags: typescript, react, monorepo, storybook
Published: 2019-10-31 16:16:10

# Let’s be lazy:
[repository on github](https://github.com/shnydercom/lerna-typescript-cra-uilib-starter)
That’s the code for the starter repository.
Also made this repository a template repository
[<img alt="image of a template button" src="https://thepracticaldev.s3.amazonaws.com/i/lzitp0wshy80lwgn7c8v.PNG">](https://github.com/shnydercom/lerna-typescript-cra-uilib-starter/generate)

This post details _why_, how to prevent errors and how to do it yourself. It is useful if you want to set up a monorepo for an existing codebase, or if you run into errors when extending your monorepo.

## Updated to use react-scripts v4.0.2!
With this update, the template contains:
* the latest React@^17.0.1 and storybook
* some example stories and components in the UI library part
* those components can use css and scss, and CSS gets built into the output folder, along with type definitions
* _modifying_ the UI library triggers a storybook hot reload, _building_ the UI library triggers a CRA hot reload

## So, for the not-so-lazy:
If you've been using ReactJS in more than one project or are building multiple Apps, you've probably come across lerna already. Since setting up webpack can be tricky, the choice is usually to use create-React-app as long as possible. So we're going to look at how this works with a centralised TypeScript config that we'll also use for our ui component library, which we'll put in a separate repository. We’ll use yarn since we’ll make use of yarn workspaces as well.

`yarn init`

a private package as the root of our monorepo. Why private? Because private packages don’t get published to npm, our root is only there for organizing everything, and lastly defining yarn workspaces only works in a private package.

![Lerna logo](https://thepracticaldev.s3.amazonaws.com/i/94iwtvt9yqqksdv3ssyq.png)
# Introducing: Lerna

First of all, you’ll need to install lerna, and while you can do that globally, I recommend installing it in your monorepo unless you (and the contributors to your monorepo) want to author lots of monorepos with lerna and it’s part of your standard toolset.

`yarn add lerna -D`

Now we have lerna, which gives us organization tools for monorepos. For example initialization:

`yarn lerna init`

This will create a `lerna.json` file and a `packages` folder. Since we’ll use yarn workspaces, we need to define yarn as our `npmClient` and set `useWorkspaces` to true. Our `lerna.json` will end up looking like this:
```json
{
  "packages": [
    "packages/*"
  ],
  "version": "0.0.0",
  "npmClient": "yarn",
  "useWorkspaces": true
}
```

And that is all the configuration we need for lerna.

Since we’re using yarn workspaces, we need to modify our `package.json`, by adding:

```json
"workspaces": [
    "packages/*"
  ],
```

Note: your `packages`-folder doesn’t need to have that name. You could also have your ui-lib, apps and server code in different subfolders. For using workspaces and lerna together, you should however define them in both `lerna.json` and `package.json`.

![Storybookjs logo](https://thepracticaldev.s3.amazonaws.com/i/arv15fm07g2q9bo7wcpn.png)
# Project Setup: UI component library package

Initializing sub-packages in monorepos is pretty similar to normal repos, with one thing to note when setting the name. You just change into the directory:

`cd packages && mkdir my-monorepo-ui-lib && cd my-monorepo-ui-lib`

And initialize a package:

`yarn init`

But with the name `@my-org/my-monorepo-ui-lib`. This is using a feature called _npm organization scope_ and requires you to set up an organization with npmjs.com if you want to publish as the `@my-org` organization.

This is not mandatory, but it shows a source for bugs when we’re developing monorepos:
* The package name _isn’t_ always the same as the directory name
* Configuration files and script parameters sometimes need a _package name_, sometimes a _directory name_
* You can use this syntax even if you never intend to publish

## Quick and dirty package installation
We want to build reusable react components in our UI library, but later our create-react-app package will determine which version of react we will use. That’s why react and react-dom can only be a `peerDependency` in the UI library. Storybook is our way to quickly try out our react components, so we’ll add that as a `devDependency`:

`yarn add react react-dom -P`
`yarn add @storybook/react babel-loader -D`

This is how we’ve always been doing it, right? Turns out, now there’s a `node_modules` folder in our _ui-lib_ package, with `react`, `react-dom` and `@storybook` inside. But we wanted to have our packages at the root, so lerna will help us do that from the root package:

`cd ../..`
`yarn lerna bootstrap`

Now there’s a node_modules folder at the _root_, containing `react`, `react-dom` and `@storybook`. The `node_modules` folder inside our ui-lib package is still there, it contains a `.bin`-folder with storybook’s *command line (bash/cmd)* scripts for starting and building. All tools executing *command line* scripts such as storybook, tsc and create-react-app are not necessarily aware that they’re run in a monorepo, they execute commands on the operating system and are usually built for “normal” npm repos. 

## Troubleshooting bash and cmd scripts: storybook, tsc, react-scripts

Inside ui-lib, if we try to run

`yarn start-storybook`

it will execute the script but tell us that we have no storybook configuration file yet:

> Create a storybook config file in "./.storybook/config.{ext}

We get the same error if we add it as a script in ui-lib’s `package.json` (naturally):

```json
  "scripts": {
    "story": "start-storybook" 
  },
```

Let’s fix that error by creating the file `packages/my-monorepo-ui-lib/.storybook/config.js`

```typescript
import { configure } from '@storybook/react'
 
const req = require.context('../src', true, /\.story\.(ts|tsx)$/)
 
configure(() => {
  req.keys().forEach(filename => req(filename))
}, module);
```

and `packages/my-monorepo-ui-lib/src` folder, that can be empty for now. Inside our ui-lib, running

`yarn start-storybook`

and

`yarn story`

works fine now, although it’s empty.

The difference becomes clear once we go to the root and try to run command line scripts from there:

`cd ../..`
`yarn start-storybook`

and we have the same error as before. The reason is that the `node_modules-folder` at the root also contains the command line script, and tries to look for a storybook config relative to the _root_ package. Lerna will help us here as well, at the root we can call

`yarn lerna run story --stream`

That command will run ‘story’ relative to all _packages_ in parallel, and ‘stream’ the script output to the console. This only works for so-called ‘lifecycle scripts’, i.e. scripts defined in one of the sub-packages' `package.json`, so the following command will not work:

`yarn lerna run start-storybook`

This is also the reason you’ll see scripts defined such as

`"tsc": "tsc",`

but it’s generally better to choose a different name to avoid confusion, especially because a lot of people install tsc and other tools globally.

![initial App preview](https://thepracticaldev.s3.amazonaws.com/i/ow8q7vh3j0wqjwe2ry9w.png)
# Project Setup: CRA App

Take caution when using CRA for new packages in combination with yarn workspaces:

`cd packages`
`create-react-app my-monorepo-cra-app`

This will throw an error, since _CRA_ copies files out of the `node_modules` folder where it’s installed in (here: `packages/my-monorepo-cra-app/node_modules`), while _yarn workspaces_ make sure everything gets installed in the root-`node_modules`-folder. So in the root `package.json` delete

```json
  "workspaces": [
    "packages/*"
  ],
```

and add it back in after you’ve run CRA. Then in the root folder run 

`yarn lerna bootstrap`

and your dependencies will neatly be moved to the root-`node_modules`. Running

`yarn lerna run start --stream`

will start your CRA-App, the JavasScript version of it.

![Typescript logo](https://thepracticaldev.s3.amazonaws.com/i/72pugvruhcbxvlr79oj2.png)
# Adding Typescript

Monorepos can help centralize configuration, so we’ll create a general tsconfig.json at the root of our monorepo. It would be great if we could use that in every subproject, but CRA needs to make some assumptions about its TypeScript setup, so it adds/overwrites the values inside tsconfig. That’s also good news, since it doesn’t just overwrite the file - and we can extend from another tsconfig. In our library project on the other hand we are more free, we can change the webpack there if we have to.

## How to structure your typescript-configurations
This decision depends on how many packages and what types of typescript-packages you want in your monorepo:
* One CRA App, one UI library: Go for 
	* one tsconfig.json at the root with cosmetic settings like `removeComments`; settings that don’t conflict with CRA and which aren’t library-specific, like library export
	* one extending from that, autogenerated in your CRA package
	* Lastly one for your library that sets `“outDir”:”lib”` and configures declaration export. This needs to correspond with the settings in the lib’s `package.json`:
```json
  "main": "./lib/index.js",
  "types": "./lib/index.d.ts",
```
* Many CRA Apps: Same structure as the one above. The reason is, that right now using CRA means that you’ll have to recompile your library to make changes in your CRA App. When running `react-scripts start` though, the `node_modules`-folder is also being watched, so you can run `tsc` in your library in watch mode after starting CRA
* Many libraries: Create an additional `tsconfig.lib.json` at the root, where you generalize your export settings. If one of your libraries depends on another one of your libraries, have a look at typescripts [path-mapping](https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping) and [project references](https://www.typescriptlang.org/docs/handbook/project-references.html) features

Apart from typescript, create-react-app supports css, scss and json-imports out of the box with just a little bit of configuration. We’ll add a `typings.d.ts`-file at the root for those types, so those file types are importable by default:

```typescript
declare module "*.json" {
  const value: any;
  export default value;
}
declare module '*.scss' {
  const content: any;
  export default content;
}
declare module '*.css' {
  interface IClassNames {
    [className: string]: string
  }
  const classNames: IClassNames;
  export = classNames;
}
```

This is the minimal tsconfig.json we could work with:
```json
{
  "exclude": ["node_modules"],
  "files":  ["./typings.d.ts"],
  "compilerOptions": {
    "jsx": "react",
    "esModuleInterop": true,
    "skipLibCheck": true
  }
}
```

We want to use typescript in all our packages, which is done by the `lerna add` command:

`yarn lerna add typescript -D`

We include `skipLibCheck` as well, because we want tsc to run fast.

# UI-library with storybook and typescript

When structuring our UI library, it’s good to follow a consistent pattern. The goal is to just run ‘tsc’ and have working Javascript, no webpack needed if we can avoid it by clear structure.

It’s especially important to:
* Separate concerns by usage (utils in one folder, React components in another)
* Prevent cyclic imports/exports (utils exported before react components - if you use factories don’t put them in utils, export them after react components)
* Make it easy for the next person to extend the library (group your react component with its story and its unit test)

So your folder structure may end up looking like this:

![index.ts in every folder, *.spec.ts and *.story.ts in component folders](https://thepracticaldev.s3.amazonaws.com/i/6al70b6b28rbkh3q3ap1.png)

Any file named `index.ts` is either a leaf in the file tree and exports unit-tested code or is a branch and exports its subfolders. Unit-tests and stories are not exported and their files can be excluded from the compiled code via configuration. Here’s an example of what the files may look like:

![export * from utils; export * from './myuihelper'](https://thepracticaldev.s3.amazonaws.com/i/bpv0d35tn6j2cqd1rgml.png)

However, we do need webpack for one thing: Storybook’s configuration for typescript. And since we’re at it, we can add support for scss and some file types as well.

`cd packages/my-monorepo-ui-lib`
`yarn add @babel/core @types/storybook__react awesome-typescript-loader babel-loader node-sass sass-loader source-map-loader style-loader -D`

Bootstrapping is not needed because we’re using yarn workspaces, and our packages can be found at the root’s `node_modules` folder. 

Directly adding it inside the package is a workaround for an error in `lerna add` in combination with organization scopes:

> lerna WARN No packages found where @babel/core can be added

The cleaner option would be to use `lerna add` with the `--scope` parameter, however this has been incompatible with how we set the organisation scope. The command would be:

`yarn lerna add @babel/core @types/storybook__react awesome-typescript-loader babel-loader node-sass sass-loader source-map-loader style-loader --scope=@my-org/my-monorepo-ui-lib -D`

Are you wondering, what the `--scope`-parameter is all about?
Here, `--scope` is the installation scope parameter, `@my-org` the npmjs-organization scope. So all those packages will be added to our UI library package.

![file webpack.config.js inside packages/my-monorepo-ui-lib/.storybook](https://thepracticaldev.s3.amazonaws.com/i/mzv9m2wkt91dbh1y3mx3.png)

Our UI lib’s webpack config is comparatively short:
```typescript
const path = require('path');
module.exports = {
  module: {
    rules: [{
        test: /\.scss$/,
        loaders: ["style-loader", "css-loader", "sass-loader"],
        include: path.resolve(__dirname, '../')
      },
      {
        test: /\.css/,
        loaders: ["style-loader", "css-loader"],
        include: path.resolve(__dirname, '../')
      },
      {
        enforce: 'pre',
        test: /\.js$/,
        loader: "source-map-loader",
        exclude: [
          /node_modules\//
        ]
      },
      {
        test: /\.tsx?$/,
        include: path.resolve(__dirname, '../src'),
        loader: 'awesome-typescript-loader',
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf|svg)$/,
        loader: "file-loader"
      }
    ]
  },
  resolve: {
    extensions: [".tsx", ".ts", ".js"]
  }
};
```

And we could use a minimal tsconfig.json that just extends from our root tsconfig.json, and puts the output in the `lib`-folder:

```json
{
  "include": [
    "src"
  ],
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "outDir": "lib",
    "declaration": true
  }
}
```

This allows us to compile typescript-files and run storybook, but we want to do more! (to do less later on...)

For our library project, we need to emit declaration files (the files ending in *.d.ts). Otherwise we’ll receive errors such as:

> Could not find a declaration file for module '@my-org/my-monorepo-ui-lib'. '.../lerna-typescript-cra-uilib-starter/packages/my-monorepo-ui-lib/lib/index.js' implicitly has an 'any' type.
my-monorepo-cra-app:   Try `npm install @types/my-org__my-monorepo-ui-lib` if it exists or add a new declaration (.d.ts) file containing `declare module '@my-org/my-monorepo-ui-lib';`  TS7016

For clarification: Webpack isn’t used in our build-process, tsc is. The Webpack we’re configuring is used by storybook.

# Typescript with CRA
The limits of centralizing our typescript configuration is determined by create-react-app’s use of typescript. At the time of writing this article, switching a CRA App from Javascript to Typescript is done by changing the index.js file to index.tsx and adding all the needed dependencies. Check CRA’s documentation for changes: https://create-react-app.dev/docs/adding-typescript

Inside our CRA-package, we run

`yarn add typescript @types/node @types/react @types/react-dom @types/jest -D`

then we copy our minimal `tsconfig.json` from the ui-lib over to the CRA App package. If we run

`yarn start`

Now, CRA’s `compilerOptions` will be added to our `tsconfig.json`.

# Loading a component from our UI library
Now it’s time to load our UI library into our CRA App, it will be installed by running:

`yarn lerna add @my-org/my-monorepo-ui-lib`

But as you might have noticed, we haven’t done much build setup for the library yet. Why didn’t we do that earlier? The reason is pretty simple: CRA, lerna and Storybook are evolving, and so is typescript, npm and even Javascript. And with _ES6 modules_, we have a powerful new feature built into the language, replacing earlier module management solutions. The only problem is that it’s not 100% adopted, but as we want to be a good library provider, we offer a fallback. So let’s export our library to ES6 modules - and an “older” module management system. Otherwise we’ll run into errors such as:

> Unexpected token “export” 

If you want to deep-dive into that topic, [this blog about nodejs modules and npm](https://adrianmejia.com/getting-started-with-node-js-modules-require-exports-imports-npm-and-beyond/) is a good start.

Npm as our package management solution has also been around since before ES6 and typescript’s rise, so we can set different entry points for our library project inside `package.json`:
* “main” is the oldest one, it’ll point to our pre-ES6 export (“./lib/index.js”)
* “types” is the place where our type declarations can be found ("./lib/index.d.ts")
* “module” is the entrypoint for our ES6 modules ("./lib-esm/index.js")

Our project is written in typescript from the start, so we’re bundling the declarations with our package. If you’ve seen yourself importing `@types`-packages, this is because those projects are written in Javascript at the core, and type definitions have been added later on.

So we set a `tsconfig.esm.json` up to export as an ES6 module:

```json
{
  "include": [
    "src"
  ],
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "lib-esm",
    "module": "esnext",
    "target": "esnext",
    "moduleResolution": "node",
    "lib": ["dom", "esnext"],
    "declaration": false
  }
}
```

This does the following: 
* Our modules will go into the `lib-esm`-folder, which we specified as our `module`-entrypoint in `package.json`. 
* Our module resolution strategy is “node”. If we don’t set it we’ll get an error such as:

> src/index.ts:1:15 - error TS2307: Cannot find module './utils'.
1 export * from './utils';
        
* Setting "esnext" targets latest supported ES proposed features: That means [“features to be developed and eventually included in the standard”](https://github.com/tc39/proposals)

This way, our library has one export for the latest Javascript features and one that is downwards compatible, so our library can have a bigger range of consumers. Note that for our own final App, CRA uses babel under the hood for compatibility in different browsers.

We’re already emitting our declarations in the `lib`-folder, so we won’t emit them another time here.

Finally, we’ll add a library-build-script in our library `package.json`:
```json
"libbuild": "tsc && tsc --build tsconfig.esm.json"
```

And we’re ready to add our library package to our CRA package. We can set a wildcard for the package version so that it’s always going to be the latest version.
```json  
"dependencies": {
    "@my-org/my-monorepo-ui-lib": "*",
```

In our CRA App we can now add the component from the library, fully type-checked:

![<MyNewComponent text="this is our library component"/> in a react render function](https://thepracticaldev.s3.amazonaws.com/i/ebl6vlm4sm8rc8k0pu75.png)


And because monorepos should make our lifes easier, we’ll add scripts in our root-`package.json` to start storybook, and execute the library build before starting our CRA app:

```json
  "scripts": {
    "story": "lerna run story --stream",
    "prestart": "lerna run libbuild --stream",
    "start": "lerna run start --stream"
  }
```

This will hopefully prevent the most common errors you can run into with this monorepo-setup. If you have additional tips, feel free to add them in the comments!