[TS] Setup TypeScript Monorepo Template
此篇為各筆記之整理,非原創內容,資料來源可見下方連結與文後參考資料:
- JavaScript and TypeScript Monorepos @ Frontend Master by Mike North
- How to Setup a TypeScript project using Rollup.js @ This Dot
monorepos 指的是在一個 git repository 中有多個 libraries/projects 在內,而不是把它拆成多個獨立的 git repository 去維護。
專案位置:pjchender/typescript-monorepo-template 另一個可參考的:fem-fullstack-ts
workspaces: setup with yarn
@@ -1,9 +1,12 @@
{
"name": "js-ts-monorepos",
"version": "1.0.0",
"main": "index.js",
"repository": "git@github.com:mike-north/js-ts-monorepos.git",
"author": "Mike North <michael.l.north@gmail.com>",
"license": "BSD-2-Clause",
- "private": true
+ "private": true,
+ "workspaces": [
+ "packages/*"
+ ]
}
在 npm 7 之後也有支援 workspaces,用法可以參考最下方的補充。
如果是多個不同性質的專案,可以設定成類似:
// package.json
{
"workspaces": ["client", "server", "shared"]
}
typescript: composite project
composite @ TypeScript > tsconfig
下述設定方式適合用在透過 tsc 編譯專案時,若是用其他編譯工具需自行調整。
我們不需要在每個 libraries/projects 中都建立各自的 tsconfig.json
,而是可以把它定義在一個地方,library 內的 tsconfig 就去繼承它就好。
在 tsconfig.json
的 compilerOptions
中加上 composite: true
表示它是其中一部分的設定檔,加上這個項目後:
- 若沒有指定
rootDir
預設會是帶有tsconfig.json
檔案的那個目錄 - 套用此項目的檔案一定要有
include
和files
declaration
預設會是true
{
"extends": "../tsconfig.settings.json",
"compilerOptions": {
"composite": true,
"outDir": "dist",
"rootDir": "src"
},
"include": [
"src"
]
}
執行 tsc
打包專案後,專案中會出現一支 tsconfig.tsbuildinfo
,這支檔案用來告訴 TypeScript 打包好的檔案和當前的原始碼(source code)是否一致。
實作步驟
-
在
packages
資料夾中建立tsconfig.settings.json
,這支檔案是要讓packages/types
和packages/utils
裡面的tsconfig.json
繼承用的// packages/tsconfig.settings.json
// 給 packages/ 中的 tsconfig 讀取用的共用設定檔
{
"compilerOptions": {
"module": "CommonJS",
"types": [],
"sourceMap": true,
"target": "ES2018",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"declaration": true
}
} -
packages/types/tsconfig.json
和packages/utils/tsconfig.json
則可以直接繼承packages/tsconfig.settings.json
,另外因為這裡面只是一部分的設定擋,所以要加上composite: true
// packages/types/tsconfig.json
// packages/utils/tsconfig.json
{
"extends": "../tsconfig.settings.json",
"compilerOptions": {
"composite": true,
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
} -
packages/tsconfig.json
則是打包檔案執行tsc -b .
時執行會吃的 TypeScript 設定檔:// packages/tsconfig.json
// tsc 會執行的 root 是這隻
// 這支會對所有的 packages 打包所有必要的檔案
{
"files": [], // 只需針對 packages 內的程式碼執行 build,而不需要針對 packages 內的所有檔案都產生對應的 .js 檔
"references": [
{
"path": "utils"
},
{
"path": "types"
}
]
} -
在
packages/types/package.json
和packages/utils/package.json
中添加測試用的 script:// packages/types/package.json
// packages/utils/package.json
"scripts": {
+ "build": "tsc -b .",
},
最終的專案結構會長像這樣:
├── package.json
├── packages
│ ├── tsconfig.json
│ ├── tsconfig.settings.json
│ ├── types
│ │ ├── package.json
│ │ └── tsconfig.json
│ └── utils
│ ├── package.json
│ └── tsconfig.json
├── tsconfig.json
只需專案的「根目錄」執行 yarn tsc -b packages
,即會針對 packages
資料夾中的每個 package 進行打包。
執行 tsc -b packages
時,會使用 packages/tsconfig.json
這支檔案。
testing: setup jest with babel
延伸閱讀:
- config-files#monorepos @ babeljs
建立 packages/.babelrc
:
// packages/.babelrc
{
"presets": [
[
"@babel/preset-env",
{
// babel 會打包成相容於 node 12 的程式碼
"targets": {
"node": 12
}
}
],
"@babel/preset-typescript"
]
}
接著建立 packages/types/.babelrc
和 packages/utils/.babelrc
,讓他們的設定檔都繼承自 packages/.babelrc
:
// packages/types/.babelrc
// packages/utils/.babelrc
{
"extends": "../.babelrc"
}
在 packages/types/package.json
和 packages/utils/package.json
中添加測試用的 script:
// packages/types/package.json
// packages/utils/package.json
"scripts": {
+ "test": "jest",
},
最終的資料夾結構長這樣:
├── packages
│ ├── .babelrc
│ ├── tsconfig.json
│ ├── tsconfig.settings.json
│ ├── types
│ │ ├── .babelrc
│ │ ├── package.json
│ │ └── tsconfig.json
│ └── utils
│ ├── .babelrc
│ ├── package.json
│ └── tsconfig.json
└── tsconfig.json
linter: setup ESLint
Mike North 根據過去經驗,強烈建議把 eslint 的設定擋放在專案的根目錄(.
)而不是 packages
這個 workspaces 中,避免有些編輯器找不到 eslint 的設定。以 VSCode 來說,它只會認得一個 eslint 的設定,而沒辦法知道不同 workspaces 內的 package 要套用不同的 eslint 設定。因此,ESLint 的設定會是 workspaces level 的,一個設定檔會適用所有的 workspaces 內的所有 packages。
實作步驟
-
安裝 eslint 和 @typescript-eslint
# -W 是指安裝在 workspaces
$ yarn add -WD eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser -
在根目錄建立
.eslintrc
,這支會給編輯器和其他 packages 中的 .eslintrc 使用// ./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"
}
} -
建立
/packages/types/.eslintrc
和/packages/utils/.eslintrc
,如果不同專案有需要個別啟用的規則,可以在這裡設定(例如 JSX):// packages/types/.eslintrc.js
// packages/utils/.eslintrc.js
module.exports = {
extends: '../../.eslintrc',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: __dirname, // monorepo 的話要加上這行,才不會去找到最外層
},
}; -
在
/packages/types/package.json
和/packages/utils/package.json
中建立 lint 的指令:"scripts": {
"build": "tsc -b .",
"test": "jest",
+ "lint": "eslint src --ext js,ts",
"clean": "rimraf dist *.tsbuildinfo"
} -
建立
.eslintignore
,將不需要 lint 的檔案忽略:packages/*/tests/*.ts
最後進到 /packages/types
或 /packages/utils
後,就可以透過 yarn lint
執行 eslint。
clean:快速移除打包好的檔案
透過 rimraf 這個套件來幫忙把專案 中把包好的檔案清除。
-
在根目錄執行
yarn add -WD rimraf
來安裝 rimraf# -W 是指安裝在 workspaces
$ yarn add -WD rimraf -
在
packages/types/package.json
和packages/utils/package.json
中加入清除用的指令:// packages/types/package.json
// packages/utils/package.json
"scripts": {
"build": "tsc -b .",
"test": "jest",
+ "clean": "rimraf dist *.tsbuildinfo"
},
packages manage: Lerna
透過 lerna 可以管理 workspaces 內的所有套件,包含執行所有 packages 內的 run script、建立各 packages 間的關聯等。
Lerna 一樣是屬於 workspaces 層級的套件:
yarn add -WD lerna
接著在專案根目錄(.
)建立 lerna.json
:
version
可以有幾種不同的做法- 定版號(稱作 lock step),這種方式會讓所有 workspaces 的 packages 在發布時有同樣的版號,適合用在每個 package 有一定程度的相依 性時
independent
:每個 package 可以有獨立的版號,適合用在每個 package 有獨立的 API 和改變時使用,例如只有某個 component 有升版,其他不動
// ./lerna.json
{
"packages": ["packages/*"],
"npmClient": "yarn",
"version": "0.0.1",
"useWorkspaces": true,
"nohoist": ["parcel-bundler"]
}
scripty:統一管理 package.json 中的 script
yarn add -WD scripty
基本使用
scripty 可以執行專案中 scripts
資料夾內的檔案,但要先將該資料夾內的檔案賦予執行的權限:
chmod -R +x scripts # scripts/ 資料夾內的所有檔案都可以被執行,權限會是 -rwxr-xr-x (755)
chmod +x scripts/foobar # scripts/foobar 這支檔案可以被執行
修改 ./packages.json
的設定:
- 在沒有指定 scripty 要執行檔案的位置時,scripty 自動會執行
./scripts
資料夾內對應的greet
檔:
// ./package.json
"scripts": {
"greet": "scripty",
},
實作步驟
先針對根目錄的 package.json
進行修改,當執行的檔案放在資料夾中時,可以在 package.json
中透過 scripty
來指定:
-
scripty 會執行
./scripts/workspace
資料夾內,對應的greet
、build
、clean
、test
和lint
的檔案:// ./package.json2
"scripts": {
"greet": "scripty",
"build": "scripty",
"clean": "scripty",
"lint": "scripty",
"test": "scripty"
},
// 告知 scripty 要執行檔案的位置
"scripty": {
"path": "./scripts/workspace"
}, -
資料夾結構長這樣:
├── scripts
├── packages
│ ├── build.sh
│ ├── lint.sh
│ └── test.sh
└── workspace
├── build.sh
├── clean.sh
├── greet
├── lint.sh
└── test.sh
接著針對 workspaces 內的各 packages 進行設定:
// ./packages/utils/package.json
// ./packages/types/package.json
"scripts": {
"build": "scripty",
"test": "scripty",
"lint": "scripty",
"clean": "rimraf dist *.tsbuildinfo"
},
// 告知 scripty 要執行檔案的位置
"scripty": {
"path": "../../scripts/packages"
},
conventional commit
conventional-changelog/commitlint @ GitHub
$ yarn add -WD @commitlint/cli @commitlint/config-conventional @commitlint/config-lerna-scopes commitlint husky lerna-changelog
# init husky
$ npx husky-init && yarn # Yarn 1
# test whether the commit message is valid
$ echo "build(api): change something in api's build" | yarn commitlint
# 根據 conventional commits 自動升版
$ lerna version --conventional-commits
補充 npm workspaces
workspaces @ npmjs
npm 7 支援 workspaces 的功能,讓開發者可以從單一個上層的 root package 管理底下多個 packages。
定義 workspaces
只需要在 package.json
中使用 workspaces
這個欄位,裡面指的是 root packages 裡面的其他 packages:
// package.json
{
"name": "my-workspaces-powered-project",
"workspaces": ["workspace-a", "packages/*"]
}
workspaces 中套件安裝的位置
當我們的資料夾結構是:
.
├── packages
│ ├── bar
│ └── foo
└── workspace-a
在 .
執行 npm install
時,會在根目錄的 node_modules
內會多一個名為 workspace-a
、foo
、bar
的捷徑,它會指向 ./workspace-a
、packages/foo
和 packages/bar
的專案位置。像是這樣:
.
├── node_modules
│ ├── bar -> ../packages/bar
│ ├── foo -> ../packages/foo
│ └── workspace-a -> ../workspace-a
├── packages
│ ├── bar
│ └── foo
└── workspace-a
對於會重複在多個 packages 中使用到的套件,實際 套件會安裝在根目錄的 ./node_modules
中。
直接把 workspaces 內的套件當成 package 匯入
由於會在根目錄的 node_modules
中建立捷徑,因此我們也可以直接把 workspaces
中定義好的套件 workspace-a
、packages/foo
和 packages/bar
直接匯入。例如:
// ./workspace-a/index.js
module.exports = 'workspace-a';
// ./packages/foo/index.js
module.exports = 'foo';
// ./packages/bar/index.js
module.exports = 'bar';
匯入這些 workspaces 中的套件:
// ./index.js
const a = require('workspace-a'); // 'workspace-a'
const b = require('foo'); // 'foo'
const c = require('bar'); // 'bar'
// ./packages/foo/index.js
const bar = require('bar'); // 也可以直接匯入 'bar'
module.exports = 'foo';
在 root package 執行 npm 指令
在執行 npm 指令時,可以透過 --workspace="package-name"
這個 option 來執行 workspaces 內某 package 的指令;如果是想要一次執行 workspaces 內的所有 npm script,則可以使用 --workspaces
(多 s
)。
例如,當執行:
# 只執行 workspace-a 裡的 npm script
$ npm run start --workspace=workspace-a
> [workspace-a] I am workspace-a
# 執行 workspace-a 和 bar 裡的 npm script
$ npm run start --workspace=workspace-a --workspace=bar
> [workspace-a] I am workspace-a
> [bar] I am bar
# 執行所有 workspaces,記得要加 s
$ npm start --workspaces
Reference
- JavaScript and TypeScript Monorepos @ Frontend Master by Mike North
- How to Setup a TypeScript project using Rollup.js @ This Dot