Kushal V Mahajan

Author of @turtlemint/redux-analytics.

Approaching in-house Modern UI Library: Startup`s Guide

I can list down the reasons for “why” to do in-house, but I’ll refrain. Since you are landing here I assume you will know your why’s.
In my research and finally getting the first iteration out, I find the details and how to part is what makes it worthy to share out to the community.
(TLDR;)
  1. Why a monorepo is required?
  2. Which monorepo tool to choose?
  3. The setup — bootstrapping (packaging), linting, development workflow (Storybook), testing, publishing. I’ll be using React as the base view library.
Let’s start with the first one.

Why a monorepo?

Monorepo is a repository with multiple packages. The real question we must be asking is why multiple packages in a single repository?
It gives us a chance to have common
  1. linting setup 🛠
  2. static type-checking ✅
  3. development workflow ( depends ) 📕
  4. the build system ( depends ) 📦
  5. testing setup ( depends ) 🐛
  6. dependency management ( package.json ) 👲
  7. publishing framework ( automatic semver ) 🚀
Did you notice the “depends” mention against a few items? Assume we are setting up a web and a native package both in the same repository. It cannot have the same build process by virtue of their platforms. Native would take a different route altogether to compile stuff to their respective IOS and android code.
The testing for DOM and testing for native code can be supported by different tools/packages in the market. Development workflow or the boilerplate used for source code is mostly going to be different.
Now, most of these points point to code reusability. Be it in the form of tooling or project utilities. Tagged along with this is code discoverability. Imagine the effort saved to hop up to different windows of projects and find everything inside one single repository.
Now, if I have to pull up more fancy terms developer agility is the other one. Having a thing built to release cycle falls under the agile cycle. Now, imagine the situation with multiple repositories and the process cycle being followed for each one of them for almost relatable things to be pushed out.
Painful! Isn’t it with multiple repositories?
Standard setup — Multiple repositories
Just imagine a common repository for these multiple packages. Can you figure out from the image below what’s common, what’s eased out? Even if you haven’t continue reading.
LINT and BUILD scripts at the root of the repo
Cons — Monorepo makes all this possible. It allows us to have multiple projects (or packages ) in the same repository. Of course, these are all pros. Cons that I can think of are increased pressure on linter for all these mini project folders, ever-growing git history object, increased time for build process as the number of projects and their source code grows.
Is that overwhelming? It certainly was for me when I started. This is only going to get beautiful though. We will discover the topics in details as we go.
I feel this must give a solid base to make a decision if a monorepo is the way to go for you. Moving on…
Which monorepo to choose? Spoiler — (Yarn Workspaces + Lerna)
Before yarn workspaces
After yarn workspaces
There are a few available in the market for package management and orchestration. The popular names that strike my head are Bazel, Lerna, Yarn workspaces, rush, pnpm. I’m going to cut this section short and share my reference right away here which made the decision easier for me back then. And having researched other sources, yarn workspaces plus lerna seems a courting. It has proven well to claims.
Summary — Lerna will be a publishing tool and taking care of semantic versioning of packages. yarn workspaces will be used for package management and local symlinking.

Monorepo setup

Bootstrapping — Lerna and Yarn workspaces
$ mkdir design-system
$ cd design-system
$ npx lerna init
You will see two files lerna.json and package.json created along with the empty folder packages.
Let’s incorporate yarn workspaces.
{
  "packages": [
    "packages/*"
  ],
  "version": "independent",
  "npmClient": "yarn",
  "useWorkspaces": true
}
The important change to notice here is the version set to independent This ensures that all our packages follow their semantic versioning.
Further, we need to tell our package.json to locate yarn workspaces.
{
  "name": "root",
  "private": true,
  "workspaces": [
    "packages/*"
  ],
  "devDependencies": {
    "lerna": "^3.18.4"
  }
}
Linting and transpilation — Typescript
Superset of Javascript
Typescript is a static type-checker and comes with almost always separate integrations of existing packages, tools. It has been a pain to set up and use initially. But I’ve come to terms. I would say worthy of my time in the end.
If you’re new to the decision of Typescript I urge you to spin up a separate research thread for the benefits of it and resume later exactly here. I strongly support the usage of it.
We will be using typescript-eslint package for linting support.
Typescript will be used for transpiling our code to specified ECMA script version. Yes. No babel for it!
Dependencies we are coming. Let’s get this over with.
yarn
yarn add react react-dom typescript prettier eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-config-react eslint-config-prettier eslint-plugin-prettier eslint-plugin-react eslint-plugin-react-hooks
The first command is short for yarn install. Now, is a good time to create a .gitignore
If you notice the above dependencies, mainly I’ve installed react, typescript, eslint and prettier. Rest is their related packages.
{
    "compilerOptions": {
        "target": "es5",
        "module": "commonjs",
        "lib": [
            "dom",
            "dom.iterable",
            "esnext"
        ],
        "rootDir": "packages",
        "baseUrl": ".",
        "strict": true,
        "esModuleInterop": true,
        "jsx": "react",
        "types": [],
        "forceConsistentCasingInFileNames": true,
        "strictBindCallApply": true,
        "strictFunctionTypes": true,
        "strictNullChecks": true,
        "noUnusedLocals": true,
        "noImplicitReturns": true,
        "sourceMap": true,
        "skipLibCheck": true,
        "declaration": true,
        "outDir": "dist"
    }
}
Let’s add some packages and source code to test if our basic linting works well.
I’m going to create two packages ui-core and ui-components in our setup.
Inside ui-components create a file - button.tsx with below code and you will see linter yell.
Now, ts(1110) is typescript’s way of telling us that you haven’t typed anything for the button props.
Next, create a folder button and moved button.tsx inside it. Also, I want you to focus on the attempted typing of e i.e. event. When I type React. how about a suggestion list from typescript or in this case React object itself.
Now, a golden rule to follow when working with typescript. For every dependency to use. There is gotta have type definitions for it. That’s the ONLY way of knowing if a package is typed.
These type definitions can either be available within the package itself (if the authors have used typescript in their development ) else there is a website https://definitelytyped.org/ where we can get type for almost all popular packages.
So run the following.
yarn add -W @types/react @types/react-dom
After this, if you observe the same line and try completing the React. type definition it will give a list of suggestions to choose from.
Completed button.tsx here
I know you’re wondering what that verbose syntax is about? But I’ll leave the Typescript learning to you.
Linting out of way, let’s write some more code to help us verify the build process via tsconfig file.
Transpilation
Add one file colors.ts to another package ui-core that we created earlier.
Something as simple as this would work.
export enum COLORS {
    PRIMARY = "#00a465",
    PRIMARY_LIGHT = "#0fb877"
}
Let’s add a script in package.json and run yarn run build
"build": "tsc -p ./tsconfig.json"
This will generate a dist folder at the root level for us containing the mirror hierarchy of our package folder. Typescript compiler compiled the code successfully to target version specified in tsconfig.json
But this not what we want. Do we?
We want the distribution to be separate and god knows, if we are working with a native project, the build process has to be separate too. So single tsconfig.json is not going to serve our monorepo needs.
Let’s add two copies of tsconfig.json and place them in each of the packages. You can copy these two from the links ui-components and ui-core
Final root tsconfig.json here.
I have deleted the entries for outDir and declaration from root tsconfig.json
Since I’m mentioning declaration, declaration:true entry generates the type definitions for your package. Since these are not needed anymore in the root, we can eliminate it. Also, if you’re wondering why to keep the root tsconfig.json , it’s because we still need common linting to work for both of our packages. I have to come back to on details for linting even further that I left out earlier.
Let’s add this script in our package.json and run yarn run build
"build": "yarn workspace ui-components tsc && yarn workspace ui-core tsc"
You will see two dist folders created inside the root of each package. yarn workspace is a command which can be run across individual packages. In this case, it combines two commands for each package. Important to add that there is a better way to achieve this i.e. without specifying each of the packages.
"build": "lerna exec --parallel -- rm -rf dist && lerna exec --parallel -- tsc -p ./tsconfig.json"
Unfortunately, the above failed for me while I’m myself doing the setup on a fresh project. On the final project shared above, it works just fine. So if it does for you, I would suggest you pick up the latter command for building your multiple packages.
Well well, our build process is set up. Let me touch the left out portion for linting. We talked about using typescript/eslint but we didn’t really configure it in the project.
You will need to add two files to the root of the project .eslint and .eslintignore. Also, it is a good time to add the much required package.json to each of the packages.
{
     "name": "ui-components",
     "version": "0.0.0"
}
and
{
    "name": "ui-core",
    "version": "0.0.0"
}
Both of our packages are named and set for independent versioning (remember?) with lerna.
The final step to this stage of the project is to add scripts for linting at the root package.json. These scripts can be combined into pre-commit hooks, pre-build hook to get unified formatting. Will leave that up to you.
"lint": "eslint './packages/**/*.{ts,tsx}'",
"lint:fix": "eslint './packages/**/*.{ts,tsx}' --fix"
Final package.json for me looks something like this. Hope, it’s the same for you.
{
  "name": "root",
  "private": true,
  "workspaces": [
    "packages/*"
  ],
  "scripts": {
    "lint": "eslint './packages/**/*.{ts,tsx}'",
    "lint:fix": "eslint './packages/**/*.{ts,tsx}' --fix",
    "build": "yarn workspace ui-components tsc && yarn workspace ui-core tsc"
  },
  "devDependencies": {
    "lerna": "^3.18.4"
  },
  "dependencies": {
    "@types/react": "^16.9.11",
    "@types/react-dom": "^16.9.4",
    "@typescript-eslint/eslint-plugin": "^2.7.0",
    "@typescript-eslint/parser": "^2.7.0",
    "eslint": "^6.6.0",
    "eslint-config-prettier": "^6.6.0",
    "eslint-config-react": "^1.1.7",
    "eslint-plugin-prettier": "^3.1.1",
    "eslint-plugin-react": "^7.16.0",
    "eslint-plugin-react-hooks": "^2.3.0",
    "prettier": "^1.19.1",
    "react": "^16.12.0",
    "react-dom": "^16.12.0",
    "typescript": "^3.7.2"
  }
}
Did you run yarn run lint yet? Did you get to test the commands and see what the linter/prettier can report back to you what your naked eyes miss while focusing on logic? Here’s what it reported to me
Almost all prettier (formatting) error for now. Shall we fix it with another command? I did it with yarn run lint:fix. To verify I ran yarn run lint again to check if this actually. works. Voila! my errors are gone.
✌️Congratulations! 🎉🍻 Our project supports common linting, one command build process for each package which completes this section.
Sigh! My project is linting properly. Trust me this is all tested, iterated and so simplified for you to read. It wasn’t the same for me the first time. I remember pulling hair quite a few times and it took days. Wish I had an end-to-end guide for all this modern recommended setup of tools.
Isn’t this long and detailed? I like pinning down every important detail just to avoid half baked information and ever-growing comment threads reporting back the issues. Although It’s possible if you’re using different versions of tools/packages, some things might change.
Despite this, I’m sure I was able to carry this article in a way which binds reasoning to the steps carried out and their importance.
This is not complete. No way! Remember, this guide is about startups 👨‍💻 🏢guide to building modern design-system. We have just started. I’m going to continue this post in another article. Until then show your ❤️ and feedback.
Here’s the link to the completed Turtlemint UI that I have released recently.

Tags

Topics of interest