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.
Prerequisite's
Optional
use nvm to manage node versions
touch .nvmrc
v14.18.2
once installed calling nvm use
from root will enable the correct version if not already enabled
Setup
use npm workspaces
add to package.json
{
"name": "my-workspaces-powered-project",
"workspaces": [
"packages/*"
]
}
add deps to workspace
npm i <workspace-dep>
dev
npm i -D <workspace-dev-dep>
add deps to package
npm i <package-dep> -w <workspace-name>
dev
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
packages
├── <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
packages/
{
"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
packages/<package-?>
{
"extends": "../tsconfig.settings.json",
"include": ["src"],
"compilerOptions": {
"composite": true,
...
}
}
Tasks
Managing task flows with lerna and npm
install:
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:
scripts
├── 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
#!/bin/sh
npx lint-staged
Appling lint tasks only to staged files
tool: lint-staged
npm i -WD lint-staged
Lint
tool: eslint
install (with ts deps):
npm i -WD eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
setup ignores, touch .eslintignore
add
node_modules
add base config to project root not packages root, this helps IDEs like vscode find the lint file
touch .eslintrc
{
"env": {
"es2021": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking"
],
"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
Format
tool: prettier
install:
npm i -WD prettier
touch .prettierignore
add
node_modules
Dev
touch scripts/workspace/dev.sh
add
#!/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
Build
touch scripts/workspace/build.sh
#!/usr/bin/env sh
echo "┏━━━ 📦 Building Workspace ━━━━━━━━━━━━━━━━━━━"
npx tsc -b packages
```bash
#### Version
`touch scripts/workspace/version.sh`
```bash
#!/usr/bin/env sh
echo "┏━━━ 📋 CREATING VERSION ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
npx lerna version
lerna.json
{
"packages": [
"packages/*"
],
"command": {
"version": {
"allowBranch": [
"main"
],
"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.
Package
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": [
"language",
"other-thing"
],
"main": "dist/index.js",
"files": [
"dist"
],
"dependencies": {},
"scripts": {
...
}
}
Publish
locally we can use verdaccio to allow us to experiment with what out publish prcess will look like without polluting npm.