Monorepo

Monorepo工程管理方案

mutirepo vs monorepo

  • mutirepo : 一个包对应一个仓库
  • monorepo : 多个包对应一个仓库
    常见monorepo管理工具:
  • pnpm
  • npm
  • yarn
  • Lerna
  • Nx
  • Rush
  • ……

pnpm monorepo

1
touch pnpm-workspace.yaml
1
2
3
4
# 子包位置
packages:
  - "packages/*"
  - "apps/*"

在工程根目录下执行初始化

1
pnpm --workspace-root init

环境版本锁定

1
2
3
4
5
6
7
// 跟工程 package.json

"engines": {
    "node": ">=22.12.0",
    "npm": ">=10.9.0",
    "pnpm": ">=10.14.0"
  }
1
2
# .npmrc
engine-strict=true

TypeScript

1
pnpm add -Dw typescript @types/node
1
touch tsconfig.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// tsconfig.json
{
    "compilerOptions": {
        "baseUrl": ".",
        "module": "esnext",
        "target": "esnext",
        "types": [],
        "lib": ["esnext"],
        "sourceMap": true,
        "declaration": true,
        "declarationMap": true,
        "noUncheckedIndexedAccess": true,
        "exactOptionalPropertyTypes": true,
        "strict": true,
        "verbatimModuleSyntax": false,
        "moduleResolution": "bundler",
        "isolatedModules": true,
        "noUncheckedSideEffectImports": true,
        "moduleDetection": "force",
        "skipLibCheck": true
    },
    "exclude": ["node_modules", "dist"]
}

代码风格与质量检查

prettier

1
2
pnpm -Dw add prettier
touch prettier.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// prettier.config.js

/**
 * @type {import('prettier').Config}
 * @see https://www.prettier.cn/docs/options.html
 */
export default {
    // 指定最大换行长度
    printWidth:120,
    // 指定缩进空格数
    tabWidth:2,
    // 是否使用制表符进行缩进
    useTabs:false,
    // 是否在语句末尾添加分号
    semi:true,
    // 是否使用单引号 (true:单引号,false:双引号)
    singleQuote:false,
    // 对象属性引用是否使用单引号
    quoteProps:"as-needed",
    // 在JSX中是否使用单引号 (true:单引号,false:双引号)
    jsxSingleQuote:false,
    // 多行时尽可能打印尾随逗号  
    trailingComma:"none",
    // 在对象括号内使用空格
    bracketSpacing:true,
    // 在对象括号内使用空格
    bracketSameLine:false,
    // 箭头函数参数括号
    arrowParens:"avoid",
    requirePragma:false,
    insertPragma:false,
    proseWrap:"preserve",
    htmlWhitespaceSensitivity:"css",
    vueIndentScriptAndStyle:false,
    endOfLine:"auto",
    rangeStart:0,
    rangeEnd:Infinity,
}

prettier忽略项

1
touch .prettierignore
1
2
3
4
5
6
// .prettierignore
dist
public
node_modules
.local
pnpm-lock.yaml

prettier脚本命令

1
2
3
"script":{
  "lint:prettier":"prettier --write \"**/*.{js,ts,mjs,cjs,json,tsx,css,less,scss,vue,html,md}\"",
}

ESLint

1
pnpm -Dw add eslint@latest @eslint/js globals typescript-eslint eslint-plugin-prettier eslint-config-prettier eslint-plugin-vue
类别 库名
核心引擎 eslint
官方规则集 @eslint/js
全局变量支持 globals
TypeScript支持 typescript-eslint
类型定义(辅助) @types/node
Prettier集成 eslint-plugin-prettier, eslint-config-prettier
Vue支持 eslint-plugin-vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
touch eslint.config.js


import { defineConfig } from "eslint-define-config";
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
import eslintPluginPrettier from "eslint-plugin-prettier";
import eslintPluginVue from "eslint-plugin-vue";
import globals from "globals";
import eslintConfigPrettier from "eslint-config-prettier";

const ignores = [
"**/dist/**",
"**/node_modules/**",
".*",
"scripts/**",
"**/*.d.ts",
];

export default defineConfig(
// 通用配置
{
ignores, // 忽略项
extends: [
eslint.configs.recommended,
...tseslint.configs.recommended,
eslintConfigPrettier,
], // 继承规则
plugins: {
prettier: eslintPluginPrettier,
},
languageOptions: {
ecmaVersion: "latest",
sourceType: "module",
parse: tseslint.parser,
},
rules: {
// 自定义
"no-var": "error",
},
},
// 前端配置
{
ignores,
files: [
"apps/frontend/**/*.{ts,js,tsx,jsx,vue}",
"packages/components/**/*.{ts,js,tsx,jsx,vue}",
],
extends: [
...eslintPluginVue.configs["flat/recommended"],
eslintConfigPrettier,
],
languageOptions: {
globals: {
...globals.browser,
},
},
},
// 后端配置
{
ignores,
files: ["apps/backend/**/*.{ts,js}"],
languageOptions: {
globals: {
...globals.node,
},
},
},
);

拼写检查

VsCode插件:Code Spell Checker

1
pnpm -Dw add cspell @cspell/dict-lorem-ipsum
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
touch cspell.json

{
"import": ["@cspell/dict-lorem-ipsum/cspell.ext.json"],
"caseSensitive": true,
"dictionaries": ["custom-dictionary"],
"dictionaryDefinitions": [
{
"name": "custom-dictionary",
"path": "./.cspell/custom-dictionary.txt",
"addWords": true
}
],
"ignorePaths": [
"**/node_modules/**",
"**/dist/**",
"**/build/**",
"**/lib/**",
"**/docs/**",
"**/vendor/**",
"**/public/**",
"**/static/**",
"**/out/**",
"**/tmp/**",
"**/*.d.ts",
"**/package.json",
"**/*.md",
"**/stats.html",
"eslint.config.mjs",
".gitignore",
".prettierignore"
]
}

git提交规范

1
2
3
4
touch .gitignore

# .gitignore

commitizen

用于检查提交

1
pnpm -Dw add @commitlint/cli @commitlint/config-conventional commitizen cz-git
  • @commitlint/cli 是 commitlint 工具的核心
  • @commitlint/config-conventional 是基于 conventional commits 规范的配置文件
  • commitizen 提供一个交互式撰写commit信息的插件
  • cz-git 是国产工具
1
2
3
4
5
6
7
8
9
// package.json
 "scripts": {
 "commit":"git-cz"
 },
 "config": {
"commitizen": {
"path": "node_modules/cz-git"
}
},

配置 cz-git

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
touch commitlint.config.js

/** @type {import('cz-git').UserConfig} */
export default {
extends: ["@commitlint/config-conventional"],
rules: {
// @see: https://commitlint.js.org/#/reference-rules
"body-leading-blank": [2, "always"],
"footer-leading-blank": [1, "always"],
"header-max-length": [2, "always", 108],
"subject-case": [0],
"type-enum": [
2,
"always",
[
"feat",
"fix",
"docs",
"style",
"refactor",
"perf",
"test",
"build",
"ci",
"chore",
"revert",
"wip",
"workflow",
"types",
"release"
]
]
},
prompt: {
types: [
{ value: "feat", name: "新功能:新增功能" },
{ value: "fix", name: "修复:修复缺陷" },
{ value: "docs", name: "文档:更新文档" },
{ value: "refactor", name: "重构:重构代码(不新增功能也不修复 bug)" },
{ value: "perf", name: "性能:提升性能" },
{ value: "test", name: "测试:新增测试" },
{ value: "chore", name: "工具:更改构建流程或辅助工具" },
{ value: "revert", name: "回滚:代码回滚" },
{ value: "style", name: "样式:格式调整(不影响代码运行)" }
],
scopes: ["root", "backend", "frontend", "components", "utils"],
allowCustomScopes: true,
skipQuestions: ["body", "footerPrefix", "footer", "breaking"],
messages: {
type: "选择提交类型:",
scope: "选择提交范围(可选):",
subject: "请简要描述更改:",
body: "详细描述(可选):",
footer: "关联 ISSUE 或 BREAKING CHANGE(可选):",
confirmCommit: "✅ 确认提交?"
}
}
};


husky

连接 git hook 自定义 提交前后事件

1
2
pnpm -Dw add husky
pnpx husky init

配置

1
2
3
4
// .husky/pre-commit

#!/usr/bin/env sh
pnpm lint:prettier && pnpm lint:eslint && pnpm lint:cspell

lint-staged

检查暂存区文件

1
pnpm -Dw add lint-staged

配置命令

1
"precommit":"lint-staged"
1
2
3
4
5
// .lintstagedrc.js
export default {
"*.{js,ts,mjs,json,tsx,css,less,scss,vue,html,md}":["cspell lint"],
"*.{js,ts,vue,md}":["prettier --write","eslint"]
}

公共库打包

安装rollup

1
pnpm -Dw add rollup @rollup/plugin-node-resolve @rollup/plugin-commonjs rollup-plugin-typescript2 @rollup/plugin-terser @vitejs/plugin-vue rollup-plugin-postcss
  • @rollup/plugin-node-resolve: 解析 node_modules 中的依赖
  • @rollup/plugin-commonjs: 将 CommonJS 模块转为 ESM
  • rollup-plugin-typescript2: 让 Rollup 支持 TS 编译
  • @rollup/plugin-terser: 压缩和混淆
  • @vitejs/plugin-vue: 支持SFC编译
  • rollup-plugin-postcss: 处理css代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    // scropts/buildBase.js

    import path from "node:path";
    import URL from "node:url";
    import fs from "node:fs";
    import { nodeResolve } from "@rollup/plugin-node-resolve";
    import commonjs from "@rollup/plugin-commonjs";
    import typescript from "rollup-plugin-typescript2";
    import vue from "@vitejs/plugin-vue";
    import postcss from "rollup-plugin-postcss";

    const __filename = URL.fileURLToPath(import.meta.url);
    const __dirname = path.dirname(__filename);

    const packages = ["utils", "components"];

    function getPackageRoots() {
    return packages.map(pkg => path.resolve(__dirname, "../packages", pkg));
    }

    async function packageJson(root) {
    const jsonPath = path.resolve(root, "package.json");
    const content = await fs.promises.readFile(jsonPath, "utf-8");
    return JSON.parse(content);
    }

    async function getRollupConfig(root) {
    const config = await packageJson(root);
    const tsconfig = path.resolve(root, "tsconfig.json");
    const { name, formats } = config.buildOptions || {};
    const dist = path.resolve(root, "./dist");
    const entry = path.resolve(root, "./src/index.ts");
    const rollupOptions = {
    input: entry,
    sourcemap: true,
    external: ["vue"],
    plugins: [
    nodeResolve(),
    commonjs(),
    typescript({
    tsconfig,
    compilerOptions: {
    outDir: dist
    }
    }),
    vue({
    template: {
    compilerOptions: {
    // 自定义转换函数,在生成 AST 时移除特定属性
    nodeTransforms: [
    node => {
    if (node.type === 1 /* NodeTypes.ELEMENT */) {
    // 过滤掉所有 data-testid 属性
    node.props = node.props.filter(prop => {
    if (prop.type === 6 /* NodeTypes.ATTRIBUTE */) {
    return prop.name !== "data-testid";
    }
    return true;
    });
    }
    }
    ]
    }
    }
    }),
    postcss()
    ],
    dir: dist
    };
    const output = [];
    for (const format of formats) {
    const outputItem = {
    format,
    file: path.resolve(dist, `index.${format}.js`),
    sourcemap: true,
    globals: {
    vue: "Vue"
    }
    };
    if (format === "iife") {
    outputItem.name = name;
    }
    output.push(outputItem);
    }
    rollupOptions.output = output;
    // watch options
    rollupOptions.watch = {
    include: path.resolve(root, "src/**"),
    exclude: path.resolve(root, "node_modules/**"),
    clearScreen: false
    };
    return rollupOptions;
    }

    export async function getRollupConfigs() {
    const roots = getPackageRoots();
    const configs = await Promise.all(roots.map(getRollupConfig));
    const result = {};
    for (let i = 0; i < packages.length; i++) {
    result[packages[i]] = configs[i];
    }
    return result;
    }

    export function clearDist(name) {
    const dist = path.resolve(__dirname, "../packages", name, "dist");
    if (fs.existsSync(dist)) {
    fs.rmSync(dist, { recursive: true, force: true });
    }
    }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// scripts/build.js
import { getRollupConfigs, clearDist } from "./buildBase.js";
import { rollup } from "rollup";
import terser from "@rollup/plugin-terser";

async function build() {
const configs = await getRollupConfigs();
for (const name in configs) {
clearDist(name);
const config = configs[name];
console.log(`📦 正在打包: ${name}`);
const bundle = await rollup({
input: config.input,
plugins: [...config.plugins, terser()],
external: config.external
});
const tasks = [];
for (const output of config.output) {
tasks.push(bundle.write(output));
}
await Promise.all(tasks);
console.log(`✅ ${name} 打包完成`);
}
}

build();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// scripts/dev.js
import { getRollupConfigs } from "./buildBase.js";
import { watch } from "rollup";

async function dev() {
const configs = await getRollupConfigs();
for (const name in configs) {
const config = configs[name];
const watcher = watch(
config.output.map(o => ({
input: config.input,
plugins: config.plugins,
external: config.external,
output: o,
watch: config.watch
}))
);
watcher.on("event", event => {
if (event.code === "START") {
console.log(`👁️ 开始监听: ${name}`);
} else if (event.code === "ERROR") {
console.error(`❌ ${name}打包失败:`, event.error);
} else if (event.code === "BUNDLE_START") {
console.log(`📦 正在打包: ${name}`);
} else if (event.code === "BUNDLE_END") {
console.log(`✅ ${name} 打包完成`);
}
});
}
}

dev();

统一测试

1
pnpm -Dw add vitest @vitest/browser vitest-browser-vue vue

Monorepo
https://jhyjhy.cn/posts/前端/Monorepo工程管理方案/41960/
作者
Hongyu
发布于
2025年11月19日
许可协议