Nodejs Monorepo (Workspace) 终极指南
简介
Monorepo 是一种管理项目的方式,它将多个项目放在一个仓库中,一般来说是一个主要项目/核心项目和一些依赖项目、一些边缘项目,这样可以方便地进行 CI/CD 等操作。
但我个人认为,Monorepo 是对跨仓库 DevOps 的机制不完善的一种妥协。
本文会在 Nodejs 环境下利用 Workspace 实现 Monorepo,以及对配置的详细理解和介绍。
所需工具
-
yarn
-
eslint(可选)
-
prettier(可选)
我们使用 yarn workspace,这是因为 npm 的 workspace 会在每个 package 的目录各自创建一个 node_module
,造成极大的浪费和不便。
tsconfig
- exclude 表示从 include 解析时忽略的文件。不要忽略 spec.ts 否则 eslint 会漏掉它。
我们的例子是一个核心库和一个业务程序。
依赖关系
monorepo
- packages
- @monorepo/core
- @monorepo/server
``@monorepo/core被
@monorepo/server` 依赖。
package.json
配置
monrepo
1{
2 "name": "monorepo",
3 "version": "1.0.0",
4 "description": "A monorepo in TypeScript",
5 "private": true,
6 "workspaces": [
7 "packages/*"
8 ],
9 "scripts": {
10 "lint": "eslint . --ext .ts",
11 "prettier-format": "run-script-os",
12 "prettier-format:win32": "prettier --config .prettierrc \"./packages/**/*.ts\" --write",
13 "prettier-format:darwin:linux": "prettier --config .prettierrc 'packages/**/*.ts' --write",
14 "prettier-format:default": "prettier --config .prettierrc 'packages/**/*.ts' --write",
15 "prettier-watch": "run-script-os",
16 "prettier-watch:win32": "onchange \"packages/**/*.ts\" -- prettier --write {{changed}}",
17 "prettier-watch:darwin:linux": "onchange 'packages/**/*.ts' -- prettier --write {{changed}}",
18 "prettier-watch:default": "onchange 'packages/**/*.ts' -- prettier --write {{changed}}"
19 },
20 "devDependencies": {
21 "@types/bcrypt": "^5.0.1",
22 "@types/jest": "^29.5.6",
23 "@types/node": "^20.8.7",
24 "@typescript-eslint/eslint-plugin": "^6.8.0",
25 "@typescript-eslint/parser": "^6.8.0",
26 "eslint": "^8.52.0",
27 "eslint-config-prettier": "^9.0.0",
28 "eslint-plugin-jest": "^27.4.3",
29 "eslint-plugin-prettier": "^5.0.1",
30 "husky": "^8.0.3",
31 "jest": "^29.7.0",
32 "nodemon": "^3.0.1",
33 "onchange": "^7.1.0",
34 "prettier": "^3.0.3",
35 "rimraf": "^5.0.5",
36 "run-script-os": "^1.1.6",
37 "ts-jest": "^29.1.1",
38 "ts-node": "^10.9.1",
39 "typescript": "^5.2.2"
40 }
41}
前三行实际上都可以去掉。
-
private
用于禁止包被发布。防止误操作。 -
workspaces
字段设置 workspace 下有哪些包,这里使用了通配符,也可以展开写。 -
devDependencies
是开发所需依赖。packages
中的包会自动继承这些依赖。
@monorepo/server
1{
2 "name": "@monorepo/server",
3 "version": "1.0.0",
4 "description": "The package containing some utility functions",
5 "scripts": {
6 "build": "tsc",
7 "start:dev": "npx nodemon --watch './src/**/*.ts' --exec 'ts-node' src/index.ts",
8 "start": "yarn build && node dist/index.js",
9 "test": "jest",
10 "test:dev": "jest --watchAll"
11 },
12 "husky": {
13 "hooks": {
14 "pre-commit": "yarn test && yarn prettier-format && yarn lint"
15 }
16 },
17 "keywords": [],
18 "author": "",
19 "license": "ISC",
20 "devDependencies": {
21 "@types/bcrypt": "^5.0.1",
22 "@types/jsonwebtoken": "^9.0.4"
23 },
24 "dependencies": {
25 "@monorepo/core": "^1.0.0",
26 "bcrypt": "^5.1.1",
27 "fastify": "^4.24.3",
28 "jest-cucumber": "^3.0.1",
29 "jsonwebtoken": "^9.0.2",
30 "reflect-metadata": "^0.1.13",
31 "sqlite3": "^5.1.6",
32 "typeorm": "^0.3.17"
33 }
34}
这里需要注意的是 "@monorepo/core": "^1.0.0",
在其依赖列表。
其它没什么可说的。安装所需的依赖即可。
@monorepo/core
1{
2 "name": "@monorepo/core",
3 "version": "1.0.0",
4 "main": "dist/index",
5 "types": "dist/index",
6 "files": [
7 "dist"
8 ],
9 "scripts": {
10 "build": "tsc",
11 "test": "jest",
12 "test:dev": "jest --watchAll"
13 }
14}
这里重点关注 main
, types
, files
都加上了 dist。这样才能让 @monorepo/server
通过 import xx from '@monorepo/core'
的方式引入。 否则会提示找不到,必须通过 import xx from '@monorepo/core/dist'
来引入。
找不到包之类的报错如何排查 由于 yarn 的 workspace 统一使用项目根目录的 node_modules,因此直接到那里去执行 ls,排查包是否存在,以及
package.json
中的配置。
jest
jest
可以根据需求决定是否要配置。如果要继承根目录配置,参考此问题
如果配置了 jest.config.[ts|js]
,则应当加入到 .eslintrc
的 ignorePatterns 中。
eslint
.eslintignore
里添加node_modules
,dist
,coverage
一般而言根目录放一个就行,不需要在 package 单独配置。
.eslintrc
1{
2 "root": true,
3 "parser": "@typescript-eslint/parser",
4 "plugins": [
5 "@typescript-eslint",
6 "prettier",
7 "jest"
8 ],
9 "ignorePatterns": [
10 "jest.config.ts"
11 ],
12 "extends": [
13 "eslint:recommended",
14 "plugin:@typescript-eslint/recommended",
15 "prettier"
16 ],
17 "parserOptions": {
18 "project": [
19 "./tsconfig.json",
20 "./packages/server/tsconfig.json",
21 "./packages/core/tsconfig.json"
22 ]
23 },
24 "rules": {
25 "prettier/prettier": 2,
26 "@typescript-eslint/no-explicit-any": "warn",
27 "@typescript-eslint/no-floating-promises": [
28 "error"
29 ]
30 },
31 "env": {
32 "browser": true,
33 "node": true,
34 "jest/globals": true
35 }
36}
eslintrc 是支持级联查找的,所以放置 root
标志让其在此处(项目根目录)停止。
注意 parserOptions.project
需要列出所有 tsconfig.json
。其它按需配置即可。
tsconfig.json
这位更是重量级。
{
"files": [],
"include": [],
"exclude": [
"**/dist/**",
],
"references": [
{
"path": "./packages/server",
},
{
"path": "./packages/core",
}
],
"compilerOptions": {
"composite": true,
"target": "es5",
"module": "commonjs",
"lib": [
"es6"
],
"allowJs": true,
"strict": true,
"esModuleInterop": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"types": [
"node",
"jest"
],
"skipLibCheck": true
},
}
exclude
选项用于排除不需要编译的文件。 references
选项表示编译此项目需要先编译哪个别的项目。 composite
使得编译产物增加 tsconfig.tsbuildinfo
,从而能够增量编译。增加 d.ts
从而能够发布类型信息。
由于开启了 reference 模式,直接执行 tsc
只会编译顶层项目(对应的 build info 也会生成)但 packages 中的项目不会被编译。
这个时候我们应当使用 tsc --build
(简写 tsc -b
)来编译。会按照 ref 顺序拓扑排序依次编译。
可以在顶层的 package.json
的 scripts
加入:
1"build": "tsc --build",
你会看到网上有的开源项目虽然配置了 workspace,但是每次都需要手动重新编译依赖然后重新启动 nodemon,这就是因为没配置好 reference.
@monorepo/server 的 tsconfig
1{
2 "include": [
3 "src/**/*"
4 ],
5 "references": [
6 {
7 "path": "../core"
8 }
9 ],
10 "extends": "../../tsconfig.json",
11 "compilerOptions": {
12 "rootDir": "src",
13 "outDir": "dist",
14 },
15}
@monorepo/core 的 tsconfig
1{
2 "include": [
3 "src/**/*"
4 ],
5 "extends": "../../tsconfig.json",
6 "compilerOptions": {
7 "rootDir": "src",
8 "outDir": "dist",
9 },
10}
至此基本上就配置得差不多了。
lerna
当 package 越来越多,这些包的管理和发布也变复杂。lerna 是一个构建管理工具,可以简化发布、变动日志的生成等。
lerna watch
也是一个比较好用的功能,可以在依赖变化时自动构建并运行指定的命令,而且不再需要为不同的平台手动适配命令。
不过多数情况原生的 tsc 其实也够用了。
可以使用 npx lerna init --dryRun
尝试。本文不赘述。