5 January 2022, 05:02

magic of mono repos

1.5 package

The value proposition of a well setup mono-repo, N packages with the maintainence overhead on 1.5. That's it, that's the goal. Below describes one of many approaches to achieve that using typescript as the primary language.



use nvm to manage node versions

touch .nvmrc


once installed calling nvm use from root will enable the correct version if not already enabled


use npm workspaces

add to package.json

  "name": "my-workspaces-powered-project",
  "workspaces": [

add deps to workspace

npm i <workspace-dep>


npm i -D <workspace-dev-dep>

add deps to package

npm i <package-dep> -w <workspace-name>


npm i -D <package-dep> -w <workspace-name>

add path to script in each "scripts" block in the package.json, for the workspace this is located at the root, for packages there is one in each package.

a tool like scripty would be ideal here, but they are currently working on >= npm 7 support, so add the the path to each corresponding key to call the script directly.

run chmod +x -R scripts after adding any additonal scripts to the directory.

Folder Structure

   ├── <package-a>
   |-- <package-b>
   └── <package-c>

Clean Up

install rimraf at workspace level,

add to scripts for each package:

rimraf dist *.tsbuildinfo

TypeScript Config

install deps:

use composite ts projects to speed up build times

base ts config in packages root


    "compilerOptions": {
        "allowJs": true,
        "allowSyntheticDefaultImports": true,
        "esModuleInterop": true,
        "inlineSourceMap": false,
        "listEmittedFiles": false,
        "listFiles": false,
        "module": "esnext",
        "moduleResolution": "node",
        "noFallthroughCasesInSwitch": true,
        "noUnusedLocals": false,
        "noUnusedParameters": false,
        "pretty": true,
        "resolveJsonModule": true,
        "skipLibCheck": true,
        "strict": true,
        "target": "es2015",
        "traceResolution": false,
        "types": ["node", "jest"],
    "compileOnSave": false,
    "exclude": ["node_modules"]

reference ts config

    "files": [],
    "references": [
        { "path": "<package-a>" },
        { "path": "<package-b>" }

ts config per package


  "extends": "../tsconfig.settings.json",
  "include": ["src"],
  "compilerOptions": {
    "composite": true,


Managing task flows with lerna and npm


npm i -WD lerna

npx lerna init

run commands with lerna:

npx lerna run <script> --scope <package-name>

run commands with npm:

npm run <script> -w ./packages/<package-directory>

npm run <script> -w <package-name>

Optional setup a scripts folder like so:

├── packages
└── workspace

mkdir -p scripts/{packages,workspace}

  • for each workspace task add to scripts/workspace
  • for each package task add to scripts/packages

Running tasks based on git hooks

tool: husky

npm i -WD husky

npx husky add .husky/pre-commit

npx lint-staged

Appling lint tasks only to staged files

tool: lint-staged

npm i -WD lint-staged


tool: eslint

install (with ts deps):

npm i -WD eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin

setup ignores, touch .eslintignore



add base config to project root not packages root, this helps IDEs like vscode find the lint file

touch .eslintrc

  "env": {
    "es2021": true
  "extends": [
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "ecmaVersion": 12
  "plugins": ["@typescript-eslint"],
  "rules": {
    "prefer-const": "error",
    "@typescript-eslint/no-unsafe-member-access": "off",
    "@typescript-eslint/no-unsafe-call": "off",
    "@typescript-eslint/no-unsafe-assignment": "off"

in each package directory touch .eslintrc

a thin lint config extending from base, additional per package rules can be added here.

  "extends": "../../.eslintrc",
  "parserOptions": {
    "project": "tsconfig.json"

touch scripts/workspace/lint.sh

#!/usr/bin/env sh
echo "┏━━━ 🕵️‍♀️ LINT WORKSPACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
npm run lint --workspaces

touch scripts/packages/lint.sh

#!/usr/bin/env sh
echo "┏━━━ 🕵️‍♀️ LINT $(pwd) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
eslint src --ext ts,js


tool: prettier


npm i -WD prettier

touch .prettierignore




touch scripts/workspace/dev.sh


#!/usr/bin/env sh
echo "┏━━━ 🏗️ DEV WORKSPACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
npx lerna run dev --parallel

touch scripts/packages/dev.sh

#!/usr/bin/env sh
echo "┏━━━ 🏗️ DEV PACKAGE $(pwd) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
tsc -w


touch scripts/workspace/build.sh

#!/usr/bin/env sh
echo "┏━━━ 📦 Building Workspace ━━━━━━━━━━━━━━━━━━━"
npx tsc -b packages

#### Version

`touch scripts/workspace/version.sh`

#!/usr/bin/env sh
echo "┏━━━ 📋 CREATING VERSION ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
npx lerna version


  "packages": [
  "command": {
    "version": {
      "allowBranch": [
      "ignoreChanges": ["**/__fixtures__/**", "**/__tests__/**"],
      "message": "new version created"
  "version": "independent"

key things here, version independant ensures each package has seperate verisons, command.version.allowBranch ensures versions can only be created on the "main" branch, check out more info on that in the lerna docs.


ensure there is an appropriate files section in package.json, below covers the required properties of name, version as well some other properties that might be useful like liscence and keywords.

  "name": "<package-a>",
  "version": "1.0.0",
  "description": "does something awesome",
  "license": "MIT",
  "repository": "@<my-org>/<package-a>",
  "keywords": [
  "main": "dist/index.js",
  "files": [
  "dependencies": {},
  "scripts": {


locally we can use verdaccio to allow us to experiment with what out publish prcess will look like without polluting npm.


← SOLID typescript