【前端工程化】一文看懂现代Monorepo(npm)工程

引言:本文介绍了目前流行的 pnpm workspace + changesets + turborepo 构建npm包项目的方案,这套方案也适用于其他大型Monorepo项目。此外,还补充了前端工程下package.jsontsconfighusky等配置知识以及CI/CD相关常识。

一、认识package.json

一个package.json代表了一个项目,可以通过npm/yarn/pnpm命令初始化一个package.json:

bash 复制代码
pnpm init

然后自行完善package.json。下面我选举了几个著名npm包的package.json作为学习参考,并且重点介绍一些字段含义来帮我们理解项目。

1.比如 vueusenpm包的 packages/shared/package.json

json 复制代码
{
  "name": "@vueuse/shared",
  "type": "module",
  "version": "14.0.0",
  "author": "Anthony Fu <https://github.com/antfu>",
  "license": "MIT",
  "funding": "https://github.com/sponsors/antfu",
  "homepage": "https://github.com/vueuse/vueuse/tree/main/packages/shared#readme",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/vueuse/vueuse.git",
    "directory": "packages/shared"
  },
  "bugs": {
    "url": "https://github.com/vueuse/vueuse/issues"
  },
  "keywords": [
    "vue",
    "vue-use",
  ],
  "sideEffects": false,
  "exports": {
    ".": "./dist/index.js",
    "./*": "./dist/*"
  },
  "main": "./dist/index.js",
  "module": "./dist/index.js",
  "unpkg": "./dist/index.iife.min.js",
  "jsdelivr": "./dist/index.iife.min.js",
  "types": "./dist/index.d.ts",
  "files": [
    "dist"
  ],
  "scripts": {
    "build": "tsdown",
    "prepack": "pnpm run build",
    "test:attw": "attw --pack --config-path ../../.attw.json ."
  },
  "peerDependencies": {
    "vue": "^3.5.0"
  }
}

2.比如ahookspackages/hooks/package.json

json 复制代码
{
  "name": "ahooks",
  "version": "3.9.6",
  "description": "react hooks library",
  "keywords": [
    "ahooks",
    "umi hooks",
    "react hooks"
  ],
  "main": "./lib/index.js",
  "module": "./es/index.js",
  "types": "./lib/index.d.ts",
  "unpkg": "dist/ahooks.js",
  "sideEffects": false,
  "authors": {
    "name": "brickspert",
    "email": "brickspert.fjl@alipay.com"
  },
  "publishConfig": {
    "registry": "https://registry.npmjs.org/"
  },
  "repository": "https://github.com/alibaba/hooks",
  "homepage": "https://github.com/alibaba/hooks",
  "scripts": {
    "build": "gulp && webpack-cli",
    "test": "vitest run --color",
    "test:cov": "vitest run --color --coverage",
    "tsc": "tsc --noEmit"
  },
  "files": [
    "dist",
    "lib",
    "es",
    "metadata.json",
    "package.json",
    "README.md"
  ],
  "dependencies": {
  },
  "peerDependencies": {
    "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
    "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
  },
  "license": "MIT",
  "gitHead": "11f6ad571bd365c95ecb9409ca3050cbbfc9b34a"
}

3.element-plus的packages/element-plus/package.json

json 复制代码
{
  "name": "element-plus",
  "version": "0.0.0-dev.1",
  "description": "A Component Library for Vue 3",
  "keywords": [
    "element-plus",
  ],
  "homepage": "https://element-plus.org/",
  "bugs": {
    "url": "https://github.com/element-plus/element-plus/issues"
  },
  "license": "MIT",
  "main": "lib/index.js",
  "module": "es/index.mjs",
  "types": "es/index.d.ts",
  "exports": {
    ".": {
      "types": "./es/index.d.ts",
      "import": "./es/index.mjs",
      "require": "./lib/index.js"
    },
    "./global": {
      "types": "./global.d.ts"
    },
    "./es": {
      "types": "./es/index.d.ts",
      "import": "./es/index.mjs"
    },
    "./lib": {
      "types": "./lib/index.d.ts",
      "require": "./lib/index.js"
    },
    "./es/*.mjs": {
      "types": "./es/*.d.ts",
      "import": "./es/*.mjs"
    },
    "./es/*": {
      "types": [
        "./es/*.d.ts",
        "./es/*/index.d.ts"
      ],
      "import": "./es/*.mjs"
    },
    "./lib/*.js": {
      "types": "./lib/*.d.ts",
      "require": "./lib/*.js"
    },
    "./lib/*": {
      "types": [
        "./lib/*.d.ts",
        "./lib/*/index.d.ts"
      ],
      "require": "./lib/*.js"
    },
    "./*": "./*"
  },
  "unpkg": "dist/index.full.js",
  "jsdelivr": "dist/index.full.js",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/element-plus/element-plus.git"
  },
  "publishConfig": {
    "access": "public"
  },
  "style": "dist/index.css",
  "peerDependencies": {
    "vue": "^3.2.0"
  },
  "dependencies": {
    
  },
  "devDependencies": {
  },
  "web-types": "web-types.json",
  "browserslist": [
    "> 1%",
    "not ie 11",
    "not op_mini all"
  ]
}

4.比如 naive-uipackage.json (naive-ui项目不是monorepo)

json 复制代码
{
  "name": "naive-ui",
  "version": "2.43.1",
  "packageManager": "pnpm@9.5.0",
  "description": "A Vue 3 Component Library. Fairly Complete, Theme Customizable, Uses TypeScript, Fast",
  "author": "07akioni",
  "license": "MIT",
  "homepage": "https://www.naiveui.com",
  "repository": {
    "type": "git",
    "url": "https://github.com/tusen-ai/naive-ui"
  },
  "keywords": [
    "naive-ui",
  ],
  "sideEffects": false,
  "main": "lib/index.js",
  "module": "es/index.mjs",
  "unpkg": "dist/index.js",
  "jsdelivr": "dist/index.js",
  "types": "es/index.d.ts",
  "files": [
    "README.md",
    "dist",
    "es",
    "generic",
    "lib",
    "volar.d.ts",
    "web-types.json"
  ],
  "scripts": {
    "start": "pnpm run dev",
    "dev": "pnpm run clean && pnpm run gen-version && pnpm run gen-volar-dts && NODE_ENV=development vite",
  },
  "web-types": "./web-types.json",
  "peerDependencies": {
    "vue": "^3.0.0"
  },
  "dependencies": {
    
  },
  "devDependencies": {
    
  },

}

文件目录

1."type"字段

可以设置"type": "module" 或者"type": "commonjs"

含义 :这个字段指定了Node.js应该如何处理.js文件的模块系统。

作用

  • 当设置为"module"时,所有.js文件都会被当作ES模块处理
  • 这意味着您可以使用import/export语法而不是require/module.exports
  • 如果没有设置或者设置为"commonjs",则使用CommonJS模块系统

但是你要注意:js/ts文件是"谁"运行的,如果是Node,自然遵循上述原则。但如果是webpack/vite/rollup/tsdown这类构建工具其实"type"这个字段对这类js/ts文件约束不到,因为此时不管是import还是require写的,构建工具都应该能识别并转换到target(target指配置打包cjs还是esm格式)

a. vueuse声明了"type": "module" ,然后采用.js + "import"语法 b. 你还会发现很多项目也不爱写type:"module",然后有两种处理方式:

  • .mjs + "import"的语法,比如element-plus
  • .js + "require"的语法,比如ahooks

vueuse项目部分截图

ahooks项目部分截图

P.S. 项目下vitest.config.ts种都是采用import语法,不管你声没声明"type": "module",这是因为Node v12 模块系统就开始支持require和import两种语法了。

2."module"字段

含义 :当使用import ... from xx导入包时,构建工具(如webpack、rollup或vite等)用来识别ES模块版本的入口文件。

作用

  • 当打包工具支持ES模块时,会优先使用这个字段指定的文件作为入口
  • 有助于实现tree-shaking(摇树优化),因为ES模块是静态分析的

在您的项目中"module": "./dist/index.mjs" 表示打包工具使用ESM导入时,从入口./dist/index.mjs这个文件导入。

3."main"字段

含义 :当使用require()导入包时,Node.js会查找这个字段指定的文件。

作用

  • "main"字段指定了包的CommonJS入口点。
  • 这是传统的包入口点定义方式。

"module"字段的区别

  • "main": CommonJS入口(用于Node.js的require)
  • "module": ES模块入口(用于打包工具的ESM的import/export)

4."exports"字段(现代替代方案)

含义:这是Node.js 12+引入的现代包入口点定义方式,提供了更精细的控制。

在您的项目中,可以这样配置

bash 复制代码
"exports": {
    ".": {
	    "types": "./dist/index.d.ts",    // TypeScript类型定义
	    "import": "./dist/index.js"     // ES模块导入入口
	}
    "./*": "./dist/*"
  }

如果你同时提供了ESM和CommonJS产物导出,你可以这样配置

json 复制代码
"exports": {
    ".": {
      "types": "./es/index.d.ts",
      "import": "./es/index.mjs",
      "require": "./lib/index.js"
    }
}

解释

  • ".": "./dist/index.js" - 当其他模块通过 import ... from '@caikengren/uni-hooks'导入您的包时,Node.js将使用 index.js 文件作为入口点。
  • "./*": "./dist/*" - 这允许导入包的子路径,例如 import useXXX from '@caikengren/uni-hooks/useXXX' 。这种模式允许访问包内的特定文件或子模块。
  • "types": "./dist/index.d.ts" - 告诉 TypeScript 编译器在哪里找到该包的类型声明文件。

补充说明

  • exports应该比这比传统的 main/module 字段提供了更强大的控制能力。 exports比较新,为了兼容性,一般也需要对应配置好main/module。
  • 控制包的访问边界。使用 exports 字段的一个重要好处是它可以保护包的内部文件。在没有 exports 字段的情况下,包的所有文件都可以被导入。而使用 exports 字段后,只有明确定义的路径才能被外部访问,这为包作者提供了更好的封装性。

5."files"字段

定义了npm上传到仓库时,需要上传哪些文件(目录),通常包含你打包的代码文件、package.json、README还有其他运行时要用到的文件 。

例如:类似ahooks,它针对Node CJS、浏览器ESM和浏览器unpkg的形式打包了三份代码,分别放在三个不同文件夹,这些文件夹都是要上传的。所以files字段定义了dist、lib和es。

发布配置

ts 复制代码
"private": true,
"publishConfig": {
  "tag": "1.1.0",
  "registry": "https://registry.npmjs.org/",
  "access": "public"
}

1."private"字段

1.private字段可以防止我们意外地将私有库发布到npm服务器。即当我们使用npm publish发布时不会被当做一个"npm包"被发布。 比如monorepo项目的根package.json文件(仅管理项目结构用,不是一个单独的npm包)。

2.但即使"private": true,当我配置了publishConfig,那么也是可以继续发布的。明确配置了publishConfig可以保证这个包是可以安全发布的。

3.默认 "private"字段为 true

2."publishConfig"字段

1.当发布包时这个配置会起作用,在这里配置tag或仓库地址。

2."registry":你团队/公司的的npm仓库地址。

3."tag":默认就是 latest(表示发布的就是正式版)。有时候需要发布beta版本(公开测试),那么这个tag就起作用了。

4.假设我们的version: 1.1.1-beta.0,然后通过npm publish --tag beta发布(注意:此时发布命令多了一个--tag参数),如果不显示指定--tag,那么就会用到我们的publishConfig.tag作为这个tag值。

5.tag值的作用就是告诉仓库这是个beta版本。当有人如下安装时:

bash 复制代码
npm i your-package@latest

6.不会安装最近发布的beta版,而是安装最近最新发布的latest版(发行版)。 如果需要安装beta版,需要手动指定版本:

bash 复制代码
npm i your-package@1.1.1-beta.0

依赖配置

1."dependencies"

dependencies:声明的是项目的生产环境中所必须的依赖包。打包时会把这些依赖的代码打包进去(这些包往往是运行时起作用)。

json 复制代码
"dependencies": {
   "react": "^18.0.2",
   "react-dom": "~18.0.2",
   "lodash": "4.17.21" 
}

这里每一项配置都是一个键值对(key-value), key表示模块名称,value表示模块的版本号。版本号遵循主版本号.次版本号.修订号的格式规定:

  • 固定版本: "lodash": "4.17.21"表示安装时只安装这个指定的版本;
  • 波浪号: "react-dom": "~18.0.2"表示安装18.0.x的最新版本(不低于18.0.2),也就是说安装时不会改变主版本号和次版本号,修订号会安装最新的;
  • 插入号: "react": "^18.0.2"表示安装18.x.x的最新版本(不低于18.0.2),也就是说安装时不会改变主版本号,次版本号和修订号会安装最新的;

通常推荐"^18.0.2"这种,保证大版本的最新。

2."devDependencies"

devDependencies:声明的是项目的开发环境、项目运行起来所必须的依赖包。打包时不会 把这些依赖的代码打包进去(这些包往往是编译时起作用)。像typescriptvite还有一些vite plugin和type包。

json 复制代码
"devDependencies": {
    "@types/node": "^20.10.0",
    "@vitejs/plugin-vue": "^6.0.1",
    "tsdown": "^0.15.12",
    "typescript": "^5.4.0",
    "vite": "^5.0.0",
  }

3."peerDependencies"

peerDependencies:这个专门用于npm包项目。我的npm包项目和安装这个npm的项目可能依赖同一个模块(另外一个npm包),那么我的npm包打包时就不需要把这个模块打包进去。比如: 我的npm包是依赖vue框架,所有安装我这个npm包的项目都应该安装了vue依赖,那么就有 我的npm包package.json:

json 复制代码
"name": "my-package",
"peerDependencies": {
    "vue": "^3.4.0"
}

a.当别人项目的vue的大版本 和 我的npm包相同

json 复制代码
"dependencies": {
    "vue": "^3.4.1"
}

此时别人项目npm i my-package时可以安装成功的。my-package和别人项目一起依赖vue@3.4.1

b.但是当别人项目的vue的大版本 和 我的npm包不同

json 复制代码
"dependencies": {
    "vue": "^2.4.1"
}

此时别人项目npm i my-package时无法成功,会提示别人(报错):

perl 复制代码
npm ERR! Could not resolve dependency: npm ERR! peer vue@"^3.4.0 from my-package@1.1.1

此时有一些比较安全可以绕过的方式:

ts 复制代码
npm i my-package --legacy-peer-deps

👉 适用于你明确知道自己在干什么,比如"这个库虽然没声明支持 vue 2,但其实可以工作"。(这是有一定风险的)

参考

关于前端大管家 package.json,你知道多少?
PACKAGE.JSON
在NPM上发布beta或alpha版

二、认识tsconfig配置和eslint config

tsconfig

tsconfig.json 是 TypeScript 项目的核心配置文件,用于控制 TypeScript 编译器(tsc)的行为。它决定了哪些文件会被编译、如何编译、以及输出结果的格式和位置。下面我将结合你的项目实际情况,详细解释常见的配置项及其作用:

1. compilerOptions

这是最重要的配置块,决定了编译器的具体行为。常见字段如下:

  • target :指定编译后的 JavaScript 版本,比如 "ESNext""ES2020"。影响新语法的支持。
  • module :指定模块系统,如 "ESNext""CommonJS"。你的项目采用 ESM,通常设置为 "ESNext"
  • lib :指定要包含在编译中的库,比如 ["DOM", "ESNext"],决定可用的全局类型。
  • declaration :是否生成类型声明文件(.d.ts),通常库项目会开启。
  • outDir :编译输出目录,比如 "dist"
  • baseUrl: 通常设置为根目录".",它是ts中识别路径的"起点"。比如import ... from 'src/...',表示从.路径下找src目录。
  • rootDir:源码根目录,决定哪些文件被编译。
  • strict:开启所有严格类型检查选项,建议库项目开启。
  • esModuleInterop:允许默认导入 CommonJS 模块,提升兼容性。
  • skipLibCheck:跳过库文件的类型检查,加快编译速度。
2.paths 别名

TypeScript 仅在"编译期"识别路径别名,它不会影响运行时。要让别名在运行时也可用,需要让打包器(Vite/Rollup/tsdown)或 Node 的解析与之保持一致。必须先确保设置了baseUrl

常见配置:

json 复制代码
// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@caikengren/uni-hooks-shared": ["packages/uni-hooks-shared/index.ts"],
      "@caikengren/uni-hooks": ["packages/uni-hooks/index.ts"]
    }
  }
}

运行时配合(示例以 Vite 为例):

ts 复制代码
// vite.config.ts
import { defineConfig } from 'vite'
import path from 'node:path'

export default defineConfig({
  resolve: {
    alias: {
      '@caikengren/uni-hooks-shared': path.resolve(__dirname, 'packages/uni-hooks-shared/index.ts'),
      '@caikengren/uni-hooks': path.resolve(__dirname, 'packages/uni-hooks/index.ts'),
    },
  },
})

在库项目中,更推荐通过"包名"引用,而不是源码别名

json 复制代码
// packages/hooks/package.json
{
  "name": "@caikengren/uni-hooks",
  "dependencies": {
    "@caikengren/uni-hooks-shared": "workspace:^"
  }
}

这样在代码里直接:

ts 复制代码
import { foo } from '@caikengren/uni-hooks-shared'

避免了别名与打包器的双重维护。

3. includeexclude
  • include :指定要编译的文件或目录(如 ["src"])。
  • exclude :指定不编译的文件或目录(如 ["node_modules", "dist"])。
4.extends

用于"继承"一份基础配置,统一 Monorepo 多包的 TypeScript 行为。

推荐在根目录放置一份基准:

json 复制代码
// tsconfig.json(根)
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "lib": ["ESNext", "DOM"],
    "strict": true,
    "declaration": true,
    "skipLibCheck": true
  }
}

子包继承并按需覆盖:

json 复制代码
// packages/uni-hooks-shared/tsconfig.json
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "rootDir": "src",
    "outDir": "dist"
  },
  "include": ["src"]
}

可以多层继承,但保持链路清晰,避免在各包内重复配置相同选项。

P.S. 如果只有根目录有tsconfig.json,子包没有tsconfig.json,子包会受到根目录tsconfig.json的约束。

eslint config

关于js格式和规范上,传统都是eslint + prettier,但配置时比较繁琐,还要处理两者的冲突。参考antfu大佬的译文,可以使用@antfu/eslint-config(vueuse项目就使用了这个)来做eslint检查。

@antfu/eslint-config是由 Vue 核心团队成员、开源界大神 Anthony Fu 创建并维护的一套 ESLint 配置集合 。它是目前社区中最流行的 ESLint 配置之一(GitHub Star 数非常高),其核心特点是"固执己见" (Opinionated) 且"开箱即用"。它采用了 ESLint 最新的 Flat Config (扁平化配置) 系统。

核心亮点:
  1. 一站式解决方案 :它不仅仅是一个 ESLint 配置,它集成了 Prettier 的功能(通过 ESLint Stylistic),你不需要再安装 Prettier。
  2. 自动检测:它会自动检测你的项目中是否使用了 Vue、React、TypeScript、UnoCSS 等,并自动启用相关规则。
  3. 代码风格
    • 无分号 (No Semicolons)
    • 单引号 (Single Quotes)
    • 尾随逗号 (Trailing Commas)
  4. 多文件支持:除了 JS/TS,还支持 JSON、YAML、Markdown、HTML 等文件的 lint 和格式化。
  5. 导入排序:内置了 eslint-plugin-simple-import-sort,自动把 import 整理得干干净净。
安装&配置
bash 复制代码
pnpm i -wD eslint @antfu/eslint-config

虽然这个包主打"零配置",但实际开发中,我们通常需要根据团队习惯微调一些规则。

@antfu/eslint-config 的默认导出是一个工厂函数,它接受任意数量的参数。

js 复制代码
// eslint.config.js
import antfu from '@antfu/eslint-config'

export default antfu(
  {
    // ============================================================
    // 1. 全局功能配置 (Options)
    // ============================================================

    // 显式启用/禁用特定框架支持(默认会自动检测,但显式写出更清晰)
    vue: true,
    typescript: true,

    // 格式化风格配置 (替代 Prettier)
    stylistic: {
      indent: 2, // 缩进 2 空格
      quotes: 'single', // 单引号
      jsx: true, // 支持 JSX
    },

    // 忽略文件 (相当于 .eslintignore)
    ignores: [
      'patches',
      'playground',
      'playgrounds',
      'docs',
      '**/types',
      '**/cache',
      '**/*.svg',
      '.cursor',
      '.trae',
      'scripts',
    ],
  },

  // ============================================================
  // 2. 具体规则覆盖 (Overrides)
  // ============================================================

  // 一般性规则覆盖
  {
    rules: {
      // 允许使用 console.log (默认是 warn 或 error)
      'no-console': 'off',
      // 允许使用未使用的变量 (通常用于解构时忽略某些属性)
      'unused-imports/no-unused-vars': 'warn',
      // 允许使用 @ts-ignore(覆盖 antfu 默认的禁用策略)
      'ts/ban-ts-comment': ['error', { 'ts-ignore': false }],
      // 如果你真的想要分号 (虽然 antfu 默认是无分号的)
      // 'style/semi': ['error', 'always'],
      'style/max-statements-per-line': 'off',

      'ts/ban-types': 'off',
      'node/no-callback-literal': 'off',
      'import/namespace': 'off',
      'import/default': 'off',
      'import/no-named-as-default': 'off',
      'import/no-named-as-default-member': 'off',
      'node/prefer-global/process': 'off',
      'ts/unified-signatures': 'off',
      'ts/no-unsafe-function-type': 'off',
      'ts/no-dynamic-delete': 'off',
    },
  },

  // 针对特定文件的规则覆盖
  {
    files: ['**/*.vue'],
    rules: {
      // Vue 组件名必须由多个单词组成?关掉它
      'vue/multi-word-component-names': 'off',
    },
  },

  {
    files: ['**/*.json'],
    rules: {
      // 允许 JSON 文件末尾有逗号 (某些工具不支持,可能需要关掉)
      'jsonc/comma-dangle': 'off',
    },
  },
)
配置lint script
json 复制代码
{
	"sciprts": {
		"lint": "eslint --cache .",
		"lint:fix": "eslint --cache --fix .",
	}
}
  • --cache启用缓存,缓存会把上次已通过的文件记住,下一次只对变更或受影响的文件重新检查,提升速度。检查的目标路径是当前目录.,具体包含哪些文件由 ESLint 配置决定。
  • 加上--fix 自动修复可自动修复的 ESLint 违规(如缩进、引号、分号等)。不能自动修复的规则会保留为错误或警告,需要手动处理。

pnpm-lock文件

1.pnpm-lock.yaml 用于锁定整个 Workspace 的依赖树,保证"可重复安装"(同样的依赖版本与拓扑)。 2.如果没有这个lock文件,会发生什么呢? 比如我的依赖:

makefile 复制代码
packageX: ^1.0.0

当第三方包 packageX发布了新版:1.1.0,那么再次安装(pnpm/yarn install)时都会安装1.1.0新版本的库,有时候可能会带来问题(尤其生产环境不推荐这样)。

3.关键点:

  • 单仓 Monorepo 只有"根锁文件",子包不生成独立锁;所有安装动作最终写回根锁。
  • 锁文件应提交到仓库,CI/同事机器据此实现一致的安装结果。
  • 发布到 npm 不会携带锁文件;库消费者不受你的锁文件影响。

4.常用命令:

bash 复制代码
# 严格模式(锁与 package.json 不一致时失败)
pnpm install --frozen-lockfile

# 仅更新锁文件(不下载依赖)
pnpm install --lockfile-only

5.与 npm/yarn 的差异:

  • pnpm 采用"内容寻址存储(Content-Addressable Store)",远程包先存入全局 .pnpm-store,再以符号链接挂到 node_modules,磁盘占用更小。
  • Workspace 下通过 workspace: 协议把本地包也以符号链接方式关联,升级内部版本时,由根锁统一反映变更。

6.多人协作建议:

  • 不手改锁文件;依赖升级统一用 pnpm up <pkg>@<range> 或在子包用 --filter 精准升级。
  • 合并冲突时,优先保留最新一次"成功安装后"的锁版本。

参考

【译】antfu博客:为什么我不用Prettier

三、认识monorepo和相关工具

monorepo 介绍

1.monorepo 是多个包在同一个项目中管理的方式,比较流行的一种管理方式。 软件项目管理经历了三个阶段:

阶段 名称 管理方式 产生背景 / 特点 优势 劣势
1​. **Monolith (单体巨石应用)**​ 单仓库管理所有项目代码 项目初期,业务复杂度低,所有代码集中在一个仓库中。 结构简单,初期管理方便。 1. 随着业务增长,代码量庞大,复杂度高。 2. 构建效率低下。
2. **MultiRepo (多仓库多模块)**​ 每个业务模块独立一个仓库 为解耦巨石应用,将项目拆分为多个模块,分别管理。 1. 模块解耦,复杂度降低。 2. 各模块可独立开发、测试、发布。 3. 构建效率提升。 1. 跨仓库代码共享困难 。 2. 依赖管理复杂 (底层模块升级,依赖方需手动更新)。 3. 仓库数量增多后,工程管理难度增加 。 4. 构建耗时增加(需按顺序构建多个仓库)。
3.​ **MonoRepo (单仓库多模块)**​ 单一仓库中管理多个项目或模块 为解决MultiRepo在模块数量增多后产生的管理问题。 1. 代码共享便捷 。 2. 共享依赖,减少包安装和包一致性 。 3. 共享工程配置 ,保证代码风格和质量一致。 4. 便于构建工具优化(如增量构建、并行构建)。 1. 仓库体积较大。 2. 需要工具支持来管理权限和优化构建性能。

2.monorepo项目目录结构:

lua 复制代码
|-- node_modules
|-- packages
|   |-- packageA
|   |   |-- package.json
|   |-- packageB
|   |   |-- package.json
|   |-- packageC
|   |   |-- package.json
|-- eslint.config.js
|-- package.json

1个package.json代表一个项目,最外面的package.json代表根项目,里面的package.json代表子包项目A、B和C等。 根项目存在的意义是:管理这些子包,比如共享根项目的依赖(npm包)子包间依赖自动link递归执行子包script命令等。

3.monorepo是一种管理规范,那么pnpm workspace协议就是用来支撑起这种规范的技术协议。具体做法:

根目录添加pnpm-workspace.yaml 配置文件:

yaml 复制代码
packages:
  - 'packages/*'

这样一来,/packages下的所有项目都会被 pnpm 识别为 workspace 的成员包

4.下面我们来看JS/TS项目领域,monorepo项目如何通过pnpm/changeset/turbo等工具解决下面三个问题:

  1. 共享代码/依赖,依赖管理
  2. 版本更新、自动tag和发布
  3. 并行/串行构建所有项目,处理构建顺序

5.后续我们用这个monorepo项目结构进行讲解。(记住这个结构,下面会多次用到

go 复制代码
|-- node_modules
|-- packages
|   |-- hooks
|   |   |-- package.json ("name":"@caikengren/uni-hooks")
|   |-- shared
|   |   |-- package.json ("name":"@caikengren/uni-hooks-shared")
|   |-- use-request
|   |   |-- package.json  ("name":"@caikengren/uni-use-request")
|-- eslint.config.js
|-- package.json

pnpm workspace 管理依赖

问题1:共享代码/依赖,依赖管理

这个问题可以拆成两个子问题来看:

  1. 多个子包依赖外部第三方npm包,比如依赖vue/react,那么这些这些包是不是可以共享(而不是每个子包项目都安装一遍)?
  2. 子包之间存在依赖,比如子包A依赖本地的子包B、C,但B、C都还没发布怎么依赖?能不能本地B、C链接到A的node_modules下呢?

1.pnpm i 安装依赖

针对上面的第一个问题,通过把依赖安装在根目录,那么子项目寻找依赖就会采用根目录的依赖,从而实现共享依赖 --workspace参数 根目录下安装vue

bash 复制代码
pnpm i -w vue

-w相当于--workspace,表示在根目录下安装依赖。

比如我要安装typescript

bash 复制代码
pnpm i -wD typescript

-D表示安装devDependencies,-w-D可以合并为-wD

安装完成后,只有根目录package.json会有依赖声明,如下:

json 复制代码
"dependencies": {
    "vue": "^3.4.0"
  },
  "devDependencies": {
    "typescript": "^5.4.0",
  }

--filter参数 比如我有一个@caikengren/uni-hooks子包,依赖了另外两个子包

  • @caikengren/uni-use-request
  • @caikengren/uni-hooks-shared 那么可以通过下面的方式,表示在@caikengren/uni-hooks这个子包中安装另外两个依赖。
shell 复制代码
pnpm i @caikengren/uni-use-request@workspace:^ --filter @caikengren/uni-hooks

pnpm i @caikengren/uni-hooks-shared@workspace:^ --filter @caikengren/uni-hooks

--filter xxx表示在xxx子包下安装依赖,其中依赖的版本使用@workspace:^占位。

安装完成后 packages/uni-hooks目录的package.json会出现:

json 复制代码
"dependencies": {
    "@caikengren/uni-hooks-shared": "workspace:^",
    "@caikengren/uni-use-request": "workspace:^"
}

@workspace:^ @workspace:^ 是一种特殊的协议前缀,用于在依赖声明中引用当前 workspace 中的本地包,并自动使用该包的版本号加上 ^ 语义化版本范围。

也就是说,pnpm 会:

  1. 在当前 workspace 中查找名为 my-local-pkg 的本地包,不去远程下载了;
  2. 获取它在 package.json 中声明的版本号(比如 1.2.3);
  3. 将依赖解析为 ^1.2.3,即允许安装兼容的次要版本更新(遵循 semver 规范);
  4. 但前提是这个包必须存在于 workspace 中。如果不存在,构建会失败。

前面,我们利用@workspace:^uni-hooks子包中安装了另外两个依赖:@caikengren/uni-hooks-shared@caikengren/uni-use-request,那么这两依赖实际是指向哪里呢?

自动link原理

1.pnpm i -w B@workspace:^ C@workspace:^ --filter A B,C被软链接到A的node_modules

2.pnpm i -w Other@5.0.0 Other实际上是安装到.pnpm store,.pnpm目录相当于.pnpm store的映射(内容同步的),.pnpm目录下的Other会被软链接到根目录的node_modules

拓展补充:ln是shell命令(ln -s相当于windows的快捷方式)。 ln -s(软链接)的文件可读写(需要原文件允许软链接读写),本质不是同一文件,而是创建了一个链接符号。 ln (硬链接)的文件可以读写,和原文件本质上是同一个文件,仅在不同地方展示了。

区分:

  • pnpm 的特性:安装的远程依赖会被放到.pnpm目录,然后软链接到node_modules,从而依赖共享,节省磁盘空间。
  • pnpm workspace的特性: 自动link。通过@workspace:^安装的本地依赖,直接软链接到node_modules,即自动link。

我们的项目中:
uni-hooks子包依赖本地@caikengren/uni-hooks-shared@caikengren/uni-use-request ,安装后你会发现node_modules下的依赖有箭头符号,说明是"符号链接"(软链接)

疑问: workspace:*workspace:^的区别?

1.@caikengren/packageA@workspace:* 打包后 (产物/发布到 npm):

pnpm 会将其替换为当前 workspace 中该包的精确版本

json 复制代码
{
  "dependencies": {
    "@caikengren/packageA": "1.0.0" 
  }
}

2.@caikengren/packageA@workspace:^ 打包后 (产物/发布到 npm):

pnpm 会将其替换为以当前版本为基准的 (^) 范围

json 复制代码
{
  "dependencies": {
    "@caikengren/packageA": "^1.0.0"
  }
}

changeset 发布

问题2:版本更新、changelog、自动tag和发布

changesets 主要关心 monorepo 项目下子项目版本的更新、changelog 文件生成、包的发布。一个 changeset 是个包含了在某个分支或者 commit 上改动信息的 md 文件,它会包含这样一些信息:

  • 需要发布的包
  • 包版本的更新层级(遵循 semver 规范)
  • CHANGELOG 信息

1.项目准备

安装工具

bash 复制代码
pnpm i -wD @changesets/cli

安装完成后,会在node_modules/.bin目录(依赖包提供的命令会出现在这)看到changeset,意味着可以用npx来运行这个命令。

初始化

bash 复制代码
npx changeset init

会多出一个changeset的文件,如下

2.本地认证准备

假设你已经注册了一个npm账号(或私有仓库账号),然后通过terminal登录,在终端输入

bash 复制代码
npm adduser
# 或者(效果一样)
npm login

3.发布流程

假设你已经改过代码并且提交了,准备发布新版本。

1.创建一个新的 changeset 文件

bash 复制代码
npx changeset add

说明

  • 这个命令会交互式地让你选择要升级的包(在 monorepo 中)、版本类型(patch / minor / major)以及填写变更描述。
  • 生成的文件会保存在 .changeset/ 目录下,格式如 clever-horses-fix.md
  • 这些文件后续会被用来决定如何 bump 版本和生成 changelog。

第一个问题:选择要改版的子包项目。「上下」键选择子包,「空格」键表示选择,「enter」确定最终选择。 changeset add是要依据commit提交信息

进入第二个问题:选择要递增的版本级别(依次是major/minor/patch )。按「enter」进入待办项目的下一个版本级别。(比如现在是major, 「enter」后进入minor)

进入第三个问题:总结

进入第四个问题:是否确认

最后,会多出一个changeset文件(md文件),描述了版本变更、changelog内容

2.根据 .changeset/ 中的所有 changeset 文件,自动 bump 相关包的版本,并更新依赖关系。

复制代码
npx changeset version

说明

  • 它会读取所有未处理的 changeset,计算每个包的新版本号。
  • 自动修改 package.json 中的版本字段。
  • 如果是 monorepo,还会更新内部依赖的版本(比如 pkg-a 依赖 pkg-b,且 pkg-b 升级了,这里会自动更新引用)。
  • 同时会删除已处理的 changeset 文件(或将其归档)。

可以看出子包下面都多了/修改了CHANGELOG.md文件,并且package.json的版本号发生了变化(1.0.0->1.1.0

3.将新版本的包发布到npm仓库

bash 复制代码
npx changeset publish
  • 说明
    • 它会执行 npm publish 对每个需要发布的包。
    • 默认只发布那些版本号发生变化的包。
    • 需要你已经登录到 npm(npm whoami)。
    • 此外,会给每个版本变动的包打tag.

先提交和push代码

bash 复制代码
git add .
git commit -m 'chore(release): v1.1.0'
git push

再发布(企业级发布,这一步通常属于CI,在云端流水线完成)

bash 复制代码
npx changeset publish

然后在npm仓库 可以看到发布的三个包

turborepo 构建

问题3:并行/串行构建所有项目,处理构建顺序

1.介绍

为什么需要 Turborepo

  • Monorepo 常见痛点:构建串行、慢;跨包依赖编排复杂;重复构建浪费时间。
  • yarn workspace 通常串行构建,整体耗时长且不可控。
  • pnpm workspace 能并行,但对复杂依赖拓扑的顺序编排有限,且缺少内建的强缓存机制。
  • Turborepo面向这些问题,提供有序并行、增量与缓存的组合解法。

它解决了什么问题

  • 构建编排:用任务依赖图(DAG)明确"谁先谁后",避免手工脚本胶水。
  • 并行执行:独立包并行跑,依赖包按顺序跑,缩短全量构建时间。
  • 增量构建:对输入做哈希,只重建被改动影响的任务,减少无效工作。
  • 本地与远程缓存:同样输入直接复用产物;接入远程缓存后,CI 与开发者之间共享结果。

2.并行和构建编排

使用turbo初始化一个 Monorepo 的项目,有以下几个 package:

  • apps/web,依赖 shared
  • apps/docs,依赖 shared
  • packages/shared,被 web 和 docs 依赖

目录如下:

yarn 命令 只能串行运行任务: yarn workspaces run lint && yarn workspaces run build && yarn workspaces run test

但是,要使用 Turborepo 可以更快地 完成相同的工作,您可以使用 turbo run lint build test

你会发现有些任务是可以并行执行的------lint和test任务可以并行。还有些是执行顺序依赖的------web和docs 的构建依赖share构建完成。

配置如下:

json:turbo.json 复制代码
{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "test": {
      "dependsOn": ["^build"]
    },
    "lint": {},
  }
}
  • tasks:声明任务编排与产物输出,key是turbo run XxxXxx
  • dependsOn:用 ^task 表示"先跑上游依赖的同名任务",自动根据依赖图排序。
  • outputs:声明任务产物(如 dist/**),让 Turborepo准确缓存和复用构建结果。

关于^task的理解是,不同包的任务编排: 当前 package 执行 build 任务之前,需要 先运行它依赖的包(dependencies / devDependencies / peerDependencies)build 任务。 当你配置了"dependsOn": ["^build"],那么「web和docs 的构建依赖share构建完成后,再执行」,否则不会。

关于dependsOn另外一种情况,没有^,表示同一个包中的任务编排。比如下面就表达了所有的test任务执行前,要先完成build命令。

json 复制代码
{ 
	"tasks": { 
		"test": { "dependsOn": ["build"] } 
	}
}

进一步,你还可以限定到某个包的 任务编排。比如下面表达了

json 复制代码
"outputs": ["dist/**"]
    },
    "test": {
      "dependsOn": ["^build"]
    },
    "lint": {},
  }
}

3.缓存和增量构建

第一次trubo run build后,会生成缓存存放在 node_modules/.cache/turbo/目录下

第一次构建:还没产物(没有dist目录),记录子包项目文件的hash值。

第二次构建:子包文件的hash标识和之前的比较,如果没变化,则跳过构建。

配置如下:

json 复制代码
//turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "test": {
      "dependsOn": ["^build"]
    },
    "lint": {},
  }
}

P.S. tasks是新字段名,对应旧pipeline,含义一样。

此外,还有cache配置和--filter命令参数:

cache:默认开启;可对 dev 等任务关闭缓存,保证开发时的即时性。

json:turbo.json 复制代码
{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "dev": {
      "cache": false
    },
  }
}

--filter:按包名或路径过滤执行范围。

bash 复制代码
turbo run <command> --filter=<package name>

构建(打包)工具

tsdown的主要功能

相关功能请查阅tsdown中文文档,目前可以看到vueuse项目已经采用了tsdown来打包库项目。

下面简单介绍下tsdown功能:

1.零配置TypeScript支持 tsdown内置了对TypeScript的完整支持,无需额外的配置:

json 复制代码
{
  "scripts": {
    "build": "tsdown"
  }
}

2.多格式输出 支持同时生成多种模块格式:

json 复制代码
{
  "tsdown": {
    "format": ["esm", "cjs", "umd"],
    "entry": "src/index.ts",
    "outDir": "dist"
  }
}

并且自动为每个输出格式生成对应的.d.ts声明文件:

bash 复制代码
dist/
├── index.esm.js
├── index.esm.d.ts
├── index.cjs.js
├── index.cjs.d.ts
├── index.umd.js
└── index.umd.d.ts

3.Tree-shaking 极速Tree-shaking,自动移除未使用的代码:

typescript 复制代码
// src/utils.ts
export function used() { return 'used'; }
export function unused() { return 'unused'; }

// src/index.ts
export { used } from './utils';
// unused函数会被自动移除

4.外部依赖处理 配置外部依赖,避免将依赖打包进输出文件:

json 复制代码
{
  "tsdown": {
    "external": ["react", "vue"],
    "globals": {
      "react": "React",
      "vue": "Vue"
    }
  }
}

Rollup vs Vite vs tsdown

Rollup - 经典选择

Rollup是最早专注于ESM打包的工具之一,以其优秀的Tree-shaking能力而闻名。

优势:

  • 出色的Tree-shaking优化
  • 生成简洁的ESM输出
  • 丰富的插件生态系统
  • 适合库开发

劣势:

  • 配置相对复杂
  • 开发模式下的HMR支持有限
  • 构建速度相对较慢
Vite - 现代开发体验

Vite基于原生ESM,提供了极快的开发服务器和优化的构建流程。

优势:

  • 闪电般的冷启动速度
  • 即时的热模块替换(HMR)
  • 开箱即用的TypeScript支持
  • 现代化的开发体验

劣势:

  • 主要用于应用开发
  • 对于纯库打包可能过于复杂
  • 依赖Node.js环境
tsdown - 专为ts库项目

tsdown是一个新兴的基于rolldown(基于rust)的打包工具,专门为TypeScript项目设计,结合了现代打包工具的优点。

性能对比

在实际项目中,三种工具的构建性能对比如下:

工具 冷启动时间 HMR速度 输出大小 配置复杂度
Rollup 中等
Vite 极快 中等
tsdown 极快 极低
选择建议
  • 库开发:推荐使用tsdown,零配置、多格式输出、类型声明自动生成
  • 应用开发:推荐使用Vite,优秀的开发体验和构建性能
  • 传统项目:如果已有Rollup配置且运行良好,可以继续使用

参考

带你了解更全面的 Monorepo - 优劣、踩坑、选型
从零到一使用 turborepo + pnpm 搭建企业级 Monorepo 项目
Changesets: 流行的 monorepo 场景发包工具

五、CI/CD

解释:

  • CI (Continuous Integration 持续整合)
  • CD (Continuous Delivery/Deployment 持续交付/部署)

CI

CI的目标:

  • 自动运行测试 :自动执行单元测试、集成测试等。例如:npm test
  • 代码质量检查 :包括 ESLint、Prettier、TypeScript 类型检查等。例如:npm run lintnpm run type-check
  • 构建验证:确保包可以正确构建(如编译 TypeScript 、打包等)。例如`npm run build

其中自动化测试代码检查构建验证一般在什么时候触发?

  • push到主分支 (main, master)之前。必须的
  • push到特征分支之前。可选
  • 提PR之前。可选

常用的CI平台 :

平台 特点
GitHub Actions 免费、与 GitHub 深度集成,YAML 配置,社区生态丰富
GitLab CI 内置于 GitLab,适合私有部署
Travis CI 曾经流行,现逐渐被 GitHub Actions 取代

CD

1.产物管理 (Artifact Management) 下载构建产物: 从 CI 流程(如 Jenkins workspace、GitHub Actions artifacts)中拉取打包好的 dist 或 build 目录。给当前的发布包打上 Tag 或版本号。(如果是docker,则会构建镜像和推送到镜像仓库)

2.环境配置与注入 (Configuration Injection)

  • 构建时注入:如果是静态站点,通常在 CI 阶段就通过 DefinePlugin 或 import.meta.env 写入了。
  • 运行时注入(CD 阶段) 如果是 Docker 容器化部署,通常在 CD 阶段通过 K8s ConfigMap 或环境变量将配置注入到容器中。

3.部署执行 (Deployment Execution)

a. 静态资源部署 (SPA - Vue/React)

  • 上传对象存储: 将 HTML/CSS/JS 上传到云厂商的对象存储(AWS S3, Aliyun OSS, Tencent COS)。
  • 更新CDN:为了防止用户在发布过程中访问到 404 文件,通常会先上传带 Hash 的静态资源(JS/CSS),最后上传入口文件(index.html)。

b.服务端应用部署 (SSR - Next.js/Nuxt/Node BFF)

  • 容器更新: 更新 Kubernetes (K8s) 的 Deployment 镜像版本,执行滚动更新(Rolling Update)。
  • 服务重启: 传统服务器上使用 PM2 reload。

c.npm包这类工程产物

  • 上传到npm仓库。使用npx changeset publish或者npm publish完成上传。

Git规范配置 - husky

git规范方案

该方案需要安装以下依赖

  • husky
  • lint-staged
  • commitizen
  • cz-git
  • @commitlint/cli
  • @commitlint/config-conventional
bash 复制代码
pnpm i -wD husky lint-staged commitizen cz-git @commitlint/cli @commitlint/config-conventional

1.husky拦截Git hooks

Git Hooks:Git 原生就自带一套"钩子"机制。在 .git/hooks/ 目录下,有一堆脚本(如 pre-commit.sample)。

  • 机制:当你执行特定的 Git 命令(如 commit、push、merge)时,Git 会自动去检查这个目录下有没有对应的脚本文件。如果有,就执行它。
  • 痛点:.git 目录是不会被提交到代码仓库的(它被 .gitignore 忽略)。这意味着,你在本地配置的钩子脚本,你的同事拉取代码后是看不到的,无法同步团队规范。

Husky 的核心作用就是"篡改"Git 查找钩子的路径,并将其指向项目代码中的位置(通常是根目录下的 .husky/ 文件夹),这个文件夹是可以提交到 Git 仓库共享给所有人的。

2.lint-staged 检查代码

每次提交代码前,我们希望"提交的代码"能通过eslint检查。这里有两个关键词:eslint检查提交的代码lint-staged就是用来做提交的代码eslint检查(而不是全部代码检查)。

3.commitlint校验commit信息

所有的git项目,都应该去遵循一个git规范,这个规范之一就是git commit的message。你肯定见过很多这样的message:chore(ci): xxxfeat(components): xxx。下面就介绍下常用的一种规范结构。

Commit Message 结构:

  • Header(头部) : 必须包含,是Commit Message的核心部分。
    • 类型 (type) : 标记Commit的类别,例如 feat (新功能), fix (修复bug), docs (文档), style (代码风格) 等。
    • 范围 (scope): 可选,用于说明Commit影响的范围,如文件、组件或模块。
    • 主题 (subject): 简短地描述本次提交的内容,通常不超过50个字符,首字母小写。
    • 格式 : type(scope): subject
  • Body(描述) : 可选,对Commit进行详细描述,可以分成多行。
    • 解释提交的动机、解决的问题等。
    • 每行建议不超过72个字符,以避免自动换行影响可读性。
  • Footer(尾部) : 可选,用于关联Issue编号或标记重大性变更 (Breaking Changes)。
    • 关联Issue : 使用如 Closes #123Fixes #123

@commitlint/cli 用来校验 Git 提交信息是否符合约定的格式。配合 @commitlint/config-conventional 使用,可强制统一提交类型(如 featfixdocsrefactorchore 等)与消息结构,提升版本管理与自动化发布的可靠性。

4.commitizen和cz-git 交互式提交

commitizen提供了cli(命令交互式)的方式来完成Commit Message填写。 cz-git 是 Commitizen 的一个适配器(adapter),通常配合 commitizen 使用。基于cz-git可以更灵活的控制你的提交Message规范。

配置工具

1.初始化husky目录

csharp 复制代码
npx husky init

pre-commit是 Git 众多钩子中的一个,顾名思义,它在 Commit 之前 执行。也就是说这个文件里的脚本,会在git commit真正执行前执行,避免不规范的提交。

2.commitizen 配置

  • Scripts: 添加 commit 命令来启动交互式提交。
  • Config: 指定 commitizen 使用 cz-git 适配器。 修改package.json:
json 复制代码
{
  "scripts": {
    "prepare": "husky", //?
    "commit": "git-cz" //使用git-cz 来做提交(多个git命令的组合)
  },
  "config": { //commitizen工具的配置
    "commitizen": {
      "path": "node_modules/cz-git"
    }
  }
}

请根据你项目中实际安装的 eslint/prettier 情况调整 lint-staged 里的命令)

2.lint-staged配置

  • Lint-staged: 配置暂存区文件的校验规则。 修改package.json:
json 复制代码
{
  "scripts": {
    "prepare": "husky", 
    "commit": "git-cz" 
  },
  "lint-staged": { // lint-staged工具的配置
    "*.{js,ts,vue,jsx,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{json,md,css,scss}": [
      "prettier --write"
    ]
  }
}

添加husky钩子,pre-commit: 提交前执行 lint-staged(代码格式化/检查)。

bash 复制代码
echo "npx lint-staged" > .husky/pre-commit

3. Commitlint & Cz-git配置

cz-git 的强大之处在于它可以直接读取 commitlint 的配置,从而实现"一份配置,两处生效"。

在根目录新建文件 commitlint.config.js (如果是 type: module 项目则为 .mjs 或 .cjs):

js 复制代码
// commitlint.config.js
/** @type {import('cz-git').UserConfig} */
export default {
  // 继承的规则
  extends: ['@commitlint/config-conventional'],
  // 自定义规则
  rules: {
    // type 类型定义,表示 git 提交的 type 必须在以下类型范围内
    'type-enum': [
      2,
      'always',
      [
        'feat', // 新功能 feature
        'fix', // 修复 bug
        'docs', // 文档注释
        'style', // 代码格式(不影响代码运行的变动)
        'refactor', // 重构(既不增加新功能,也不是修复bug)
        'perf', // 性能优化
        'test', // 增加测试
        'chore', // 构建过程或辅助工具的变动
        'revert', // 回退
        'build' // 打包
      ]
    ],
    // subject 大小写不做校验
    'subject-case': [0]
  },
  // cz-git 的交互配置(可选,用于定制交互界面)
  prompt: {
    useEmoji: true, // 是否使用 emoji
    // 可以在这里自定义提示语,例如中文化
    messages: {
      type: '选择你要提交的类型 :',
      scope: '选择一个提交范围(可选):',
      customScope: '请输入自定义的提交范围 :',
      subject: '填写简短精炼的变更描述 :\n',
      body: '填写更加详细的变更描述(可选)。使用 "|" 换行 :\n',
      breaking: '列举非兼容性重大的变更(可选)。使用 "|" 换行 :\n',
      footerPrefixsSelect: '选择关联issue前缀(可选):',
      customFooterPrefix: '输入自定义issue前缀 :',
      footer: '列举关联issue (可选) 例如: #31, #I3244 :\n',
      confirmCommit: '是否提交或修改commit ?'
    },
    // 设置 scope 范围(根据项目模块调整)
    scopes: ['components', 'hooks', 'utils', 'styles', 'deps']
  }
}

添加 Husky钩子,提交时校验 commit message 格式。

bash 复制代码
echo "npx --no-install commitlint --edit \$1" > .husky/commit-msg

结果演示

现在有6个修改

bash 复制代码
#添加到暂存区
git add .
# 执行commit script(替代git commit)
pnpm commit

六、实战monorepo形式的npm项目

关于这一块,可以参考我的一个项目 @caikengren/uni-hooks,后续我会根据这个项目展开写几篇博客和大家一起交流学习。

相关推荐
JarvanMo16 分钟前
Flutter:如何更改默认字体
前端
默海笑17 分钟前
VUE后台管理系统:定制化、高可用前台样式处理方案
前端·javascript·vue.js
YaeZed24 分钟前
Vue3-toRef、toRefs、toRaw
前端·vue.js
用户66006766853925 分钟前
CSS定位全解析:从static到sticky,彻底搞懂布局核心
前端·css
听风说图25 分钟前
Figma Vector Networks: 形状、填充及描边
前端
hanliu200329 分钟前
实训11 ,百度评分
前端
Y***K43434 分钟前
TypeScript模块解析
前端·javascript·typescript
JarvanMo37 分钟前
Xcode 没人想解决的问题:为什么苹果对平庸感到满意
前端
合作小小程序员小小店1 小时前
web网页开发,在线%餐饮点餐%系统,基于Idea,html,css,jQuery,java,ssm,mysql。
java·前端·数据库·html·intellij-idea·springboot