Monorepos for frontend

Use NPM Packages

Use NPM packages as the building blocks of your application. Having separate packages is a little bit more of work but:

  1. Force us to have clear interfaces
  2. Having packages that can run independently makes them easier to test
  3. Easier to refactor because of the clear dependencies

Each package is a building block. Each of them works independently of the rest. Each of them can be tested in an isolated environment.

  • Simple blocks: @grajal/ui-text, @grajal/ui-image,
  • Composition blocks: @grajal/ui-loginform,
  • Functionality blocks: @grajal/ui-loginprovider,
  • Utilities blocks: @grajal/ui-themes, @grajal/ui-utils,
  • Whole apps: @grajal/app-picturerating,

We have TONS of packages. Is there a way to manage them? Monorepos to the rescue.

Lerna

Lerna is a tool that optimizes the workflow around managing multi-package repositories with git and npm.

npm install -g lerna

Lerna is a tool to manage one Git repo with multiple NPM packages. The alternative, using one repository per NPM package…

  1. Harder to document and maintain: Difficult to know after a few years what packages are in use and which ones are deprecated
  2. Easier to refactor because you can commit across packages
  3. Tons of places where to open merge requests, issues etc.

I created a repository that contains scoped @grajal/[package-name] packages. In lerna, the packages are stored on the packages/ folder. This is how it looks:

1
2
3
4
5
6
7
8
9
10
── packages
├── Button
│   ├── dist
│   └── src
│   └── __tests__
├── Themes
│   ├── dist
│   └── src
│   └── __tests__
(...)

And each package is an independent NPM module.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
❯ cat Loginform/package.json
{
"name": "@grajal/ui-loginform",
(...)
"dependencies": {
"@grajal/ui-button": "^1.0.3",
"@grajal/ui-checkbox": "^1.0.3",
"@grajal/ui-container": "^1.0.3",
"@grajal/ui-input": "^1.0.3",
"@grajal/ui-loginprovider": "^1.0.3",
"@grajal/ui-text": "^1.0.3",
"@grajal/ui-themes": "^1.0.3",
(...)
},
(...)
}

The packages are written with js ECMA stage-2 and they are transpiled on a build process. The packages can be tested independently with jest+ex

1
2
3
4
5
Test Suites: 8 failed, 7 passed, 15 total
Tests: 17 passed, 17 total
Snapshots: 14 passed, 14 total
Time: 7.747s
Ran all test suites.

Lerna has three different steps:

  1. lerna bootstrap. For each package, installs external dependencies and links sub-packages together. That means that the scoped packages @grajal present in the repo will be symlinked on the node_modules/@grajal/ instead of installed. Ex. import(‘@grajal/ui-text’) -> import(‘../../ui-text’)
  2. lerna run [command]. Runs npm run [command] on each package. Big lerna users like Babel, Jest, React etc dont really use this command because it is faster to run global build scripts than spawning 30 build scripts in parallel. Example: On the packages I created
  • lerna run build will run npm run build on all packages, transpiling the code and exporting it on dist/
  • lerna run test Will run npm run test on all packages
  1. lerna publish. Publishes all packages to npm

NPM server

The lerna managed NPM packages need to be published somewhere so they can be consumed by the applications. On my tests I installed a verdaccio server as a private local NPM repository. In a real environment we will publish our lerna packages directly to the company NPM package repository. By default verdaccio runs on port 4873.

Change .npmrc to search for packages also on our local server

You will need to create a user

1
npm adduser --registry http://localhost:4873

You will end up with something like

1
2
3
4
5
6
> cat ~/.npmrc
save-exact=true
registry=http://npm-registry.bln.int.davidgrajal.com/
//npm-registry.bln.int.davidgrajal.com/:_authToken=WQUC9C68U2fMCVrmhv5cjW34QV8vrOu7KJHgnZaIr1eI
@grajal:registry=http://localhost:4873/
//localhost:4873/:_authToken="jxSHHbil0gKIUwo25mWNPemJvstiqSk8="

A new application with monorepo

Create a new application with react-create-app.

Add a couple of building blocks:

  • @grajal/ui-loginform This one allows the user to login and creates a localStorage key with the sessionID
  • @grajal/app-picturerating This is a React version of BB picture rating.

The app will display the loginform if the user is not logged in, and will display the picturerating app if the user is logged in.

Caveats

  • Initial setup is horrible
  • I was not able to make NPM resolve dependencies of the demo app. yarn was able to install the packages without trouble