Some time ago I’ve started working on React based UI library. I wanted to keep my components together but publish them to npm separately. I’ve decided to use monorepo.
Monorepo is a development strategy where code for many projects is stored in the same repository.
Advantages of monorepo:
- allows to share dependencies - each package can link to dependency stored in project root
- one project configuration for all packages - linter, editor settings, etc
- easier to refactor
- commit changes for multiple packages simultaneously - changes in one package breaks other? No problem, fix other package and commit it together with breaking changes.
Monorepo helps with keeping components in one place but there are other challenges:
sharing dependencies across packages- you need a tool for this
build system- build each package separately or all at once?
publishing packages to NPM
configuring one linter for all packages
development playground- you need to see what you build, right?
You can see configured repository here. Want to know details? Read on.
Each package can have custom dependencies. To install and hoist all dependencies for all packages at once you need Yarn or lerna. In this article we will stick with Yarn since it’s more common than Lerna.
Hoisting is a process of lifting an entity, in this case a dependency from package node_modules to root node_modules.
Getting Yarn to work in monorepo is easy, you need to add one line to
This will tell Yarn to install all dependencies in
packages/* and hoist them to root node_modules. To learn more, you can read docs about Yarn workspaces.
To install dependencies in all packages, run
yarn in project root or in any of the packages.
You can view this pull request to see how to configure ESLint and prettier or just keep on reading.
ESLint is a tool to check your code for common mistakes. Prettier is a tool that can format your code to make it more readable. Those two tools can coexist and if you configure them properly, they can save you a lot time spent on debugging and formating.
We will use create react app ESLint configuration, to install it, execute this in project root:
yarn add -W eslint-config-react-app firstname.lastname@example.org email@example.com firstname.lastname@example.org email@example.com firstname.lastname@example.org email@example.com firstname.lastname@example.org @email@example.com @firstname.lastname@example.org
After that, add eslint configuration. You can create
.eslintrc.json file or add
eslintConfig key to package.json, with the following settings:
At this point you can run
npx eslint packages --ext ts,tsx to lint your packages.
Validation in terminal? We can do better than that. All we need to do is to install eslint extension and select languages to validate.
If you work in a team, it’s good idea to let other know that they also need eslint extension. To do that, we can use
.vscode/extensions.json file with the following content:
Now, when any of your colleagues opens this project and is missing required plugins, he will get a nice popup with recommended extensions.
Last step is to add eslint configuration. We can do that in
.vscode/settings.json file. Create it and fill with the following settings:
At this point you should have a nice validation inside editor. To check if it works, you can add this code inside any of tsx files
<img src="icon.png " />. ESLint should let you know that you’re missing an
To get your code auto formatted, we will use a Prettier extension for VS Code. We could also add a prettier to project dependencies but if your team is using only VS Code then it’s fine to skip it.
To get started, add prettier to recommended extensions in
After that, you will get a popup inside VS Code with info about recommended prettier extension. To finish configuration, you need to enable auto formatting and eslint integration. ESLint integration allows prettier to use eslint config for formatting.
Add these settings to the
If you want to use linter in continuous integration tool, you can add lint commands to your
"lint": "eslint packages --ext ts,tsx",
This one was really tricky, at first I’ve used microbundle tool. It worked quite ok for small amount of packages. It had one big issue: 2-3 node processes were created for each package build. It killed my CPU when number of packages grew.
My next try was webpack. I got it to work but only partially. At this point I had a lot of dependencies used by build system. I started to look for a better solution.
I’ve found a solution in a TypeScript. Since version 3.0, TypeScript support project references.
Project references are like:
- TS Compiler: I’m starting compilation.
- Main package: Okey, you can compile me but I require these referenced packages, so please go and compile them first.
- TS Compiler: Okey, I’m going into button package. I’m starting compilation.
- Button package: Hey, you can compile me but I require icon package to be compiled first.
- TS Compiler: Okey, I’m going into icon package. I’m starting compilation.
- Icon package: Yeah, it’s bundle time!
At this point, compiler goes back to button, compiles, goes back to main package and finish process. In the result icon package was compiled first, then button and then the main package.
Project references is an array of objects, each object contains a path to a directory with
Visit TypeScript Project References docs to learn more.
First, add TypeScript in root directory:
yarn add email@example.com -W
We’re using version
3.3.4000 because newer versions are much more slower at the time of writing this article.
Next, add a build configuration
Configure project references in
files is set to empty array because this tsconfig should not compile any files. It’s used only as list of packages that compiler will go into and compile. Every package you want to compile should be listed in this file and should contain custom
Each package should contain a
package.json with at least the following keys:
Package should also contain a
tsconfig.json with the following config:
This config extends build configuration from root directory. It also configures input and output directory. At the end you have, a
references key, it’s array of dependencies that should be build before compiler can start compiling this package. Leave empty array if there are no dependencies.
The last requirement is to have
index.tsx file inside
The last part of build system are scripts inside
Quite a lot is going on here so let’s go line by line.
prebuildis a script that is always called before build, it removes all compiled files
buildcompiles packages to esm and commonjs format
build:cjscompiles files to commonjs format
build:esmcompiles files to esm format
dev:cjsstarts watching packages and compiles to cjs on change
dev:esmstarts watching packages and compiles to esm on change
module:cjsin tsconfig.json replaces words esnext to es5 for target key, and esnext to commonjs for module key. It also replaces out directory in each package from lib/esm to lib/cjs
module:esmis a reverse script version of
This setup have one disadvantage, you can’t run dev:cjs and dev:esm simultaneously.
That concludes our build system. It’s not overly complicated(except module:esm/cjs commands) and gets job done really fast(clear build is around 4s for 3 packages). It compiles to commonjs and esnext modules. Typings are also compiled although not bundled.
To add Storybook, execute this command in project root:
npx -p @storybook/cli sb init
It will install all dependencies and add
build-storybook commands to
To start Storybook, run
yarn storybook and
yarn dev:esm to make it reload on change.
Here is a sample story that uses one of our packages:
import React from "react"
Ready to publish your package on NPM? Yeah, you can go into each package directory and run
yarn publish but there is a better way. You can use Lerna - it’s a tool that helps with monorepo management. It also allows you to publish multiple packages at once.
Here you can see a pull request example how to add lerna to project.
To get started, add Lerna to project:
yarn add lerna -W
Lerna have custom hoisting system but since we use Yarn, we can leverage it’s workspaces power. Last step is too add two scripts to
Now you can run
yarn release to build packages and release changed packages on NPM.
Want to publish a test version? Use
yarn release --canary to release a canary version of each changed package.
- You can get whole build system using only TypeScript. Awesome!
- Lerna is useful for publishing packages
- Yarn is able to manage dependencies of multiple packages
- ESLint and Prettier are meant to be used together - less time spent on formatting and debugging
- Storybook is easy to setup playground, it also can be attached to pull request so your team members co do quality assurance tests