引言:本文介紹了目前流行的pnpm workspace+changesets+turborepo構建npm包項目的方案,這套方案也適用於其他大型Monorepo項目。此外,還補充了前端工程下package.json、tsconfig和husky等配置知識以及CI/CD相關常識。
一、認識package.json
一個package.json代表了一個項目,可以通過npm/yarn/pnpm命令初始化一個package.json:
pnpm init
然後自行完善package.json。下面我選舉了幾個著名npm包的package.json作為學習參考,並且重點介紹一些字段含義來幫我們理解項目。
1.比如 vueusenpm包的 packages/shared/package.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.比如ahooks的packages/hooks/package.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
{
"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-ui的 package.json (naive-ui項目不是monorepo)
{
"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+引入的現代包入口點定義方式,提供了更精細的控制。
在您的項目中,可以這樣配置
"exports": {
".": {
"types": "./dist/index.d.ts", // TypeScript類型定義
"import": "./dist/index.js" // ES模塊導入入口
}
"./*": "./dist/*"
}
如果你同時提供了ESM和CommonJS產物導出,你可以這樣配置
"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。
發佈配置
"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版本。當有人如下安裝時:
npm i your-package@latest
6.不會安裝最近發佈的beta版,而是安裝最近最新發布的latest版(發行版)。
如果需要安裝beta版,需要手動指定版本:
npm i your-package@1.1.1-beta.0
依賴配置
1."dependencies"
dependencies:聲明的是項目的生產環境中所必須的依賴包。打包時會把這些依賴的代碼打包進去(這些包往往是運行時起作用)。
"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:聲明的是項目的開發環境、項目運行起來所必須的依賴包。打包時不會把這些依賴的代碼打包進去(這些包往往是編譯時起作用)。像typescript,vite還有一些vite plugin和type包。
"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:
"name": "my-package",
"peerDependencies": {
"vue": "^3.4.0"
}
a.當別人項目的vue的大版本 和 我的npm包相同
"dependencies": {
"vue": "^3.4.1"
}
此時別人項目npm i my-package時可以安裝成功的。my-package和別人項目一起依賴vue@3.4.1。
b.但是當別人項目的vue的大版本 和 我的npm包不同
"dependencies": {
"vue": "^2.4.1"
}
此時別人項目npm i my-package時無法成功,會提示別人(報錯):
npm ERR! Could not resolve dependency: npm ERR! peer vue@"^3.4.0 from my-package@1.1.1
此時有一些比較安全可以繞過的方式:
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
常見配置:
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@caikengren/uni-hooks-shared": ["packages/uni-hooks-shared/index.ts"],
"@caikengren/uni-hooks": ["packages/uni-hooks/index.ts"]
}
}
}
運行時配合(示例以 Vite 為例):
// 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'),
},
},
})
在庫項目中,更推薦通過“包名”引用,而不是源碼別名:
// packages/hooks/package.json
{
"name": "@caikengren/uni-hooks",
"dependencies": {
"@caikengren/uni-hooks-shared": "workspace:^"
}
}
這樣在代碼裏直接:
import { foo } from '@caikengren/uni-hooks-shared'
避免了別名與打包器的雙重維護。
3. include 和 exclude
include:指定要編譯的文件或目錄(如["src"])。exclude:指定不編譯的文件或目錄(如["node_modules", "dist"])。
4.extends
用於“繼承”一份基礎配置,統一 Monorepo 多包的 TypeScript 行為。
推薦在根目錄放置一份基準:
// tsconfig.json(根)
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ESNext", "DOM"],
"strict": true,
"declaration": true,
"skipLibCheck": true
}
}
子包繼承並按需覆蓋:
// 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 (扁平化配置) 系統。
核心亮點:
- 一站式解決方案:它不僅僅是一個 ESLint 配置,它集成了 Prettier 的功能(通過 ESLint Stylistic),你不需要再安裝 Prettier。
- 自動檢測:它會自動檢測你的項目中是否使用了 Vue、React、TypeScript、UnoCSS 等,並自動啓用相關規則。
-
代碼風格:
- 無分號 (No Semicolons)
- 單引號 (Single Quotes)
- 尾隨逗號 (Trailing Commas)
- 多文件支持:除了 JS/TS,還支持 JSON、YAML、Markdown、HTML 等文件的 lint 和格式化。
- 導入排序:內置了 eslint-plugin-simple-import-sort,自動把 import 整理得乾乾淨淨。
安裝&配置
pnpm i -wD eslint @antfu/eslint-config
雖然這個包主打“零配置”,但實際開發中,我們通常需要根據團隊習慣微調一些規則。
@antfu/eslint-config 的默認導出是一個工廠函數,它接受任意數量的參數。
// 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
{
"sciprts": {
"lint": "eslint --cache .",
"lint:fix": "eslint --cache --fix .",
}
}
--cache啓用緩存,緩存會把上次已通過的文件記住,下一次只對變更或受影響的文件重新檢查,提升速度。檢查的目標路徑是當前目錄.,具體包含哪些文件由 ESLint 配置決定。- 加上
--fix自動修復可自動修復的 ESLint 違規(如縮進、引號、分號等)。不能自動修復的規則會保留為錯誤或警告,需要手動處理。
pnpm-lock文件
1.pnpm-lock.yaml 用於鎖定整個 Workspace 的依賴樹,保證“可重複安裝”(同樣的依賴版本與拓撲)。
2.如果沒有這個lock文件,會發生什麼呢?
比如我的依賴:
packageX: ^1.0.0
當第三方包 packageX發佈了新版:1.1.0,那麼再次安裝(pnpm/yarn install)時都會安裝1.1.0新版本的庫,有時候可能會帶來問題(尤其生產環境不推薦這樣)。
3.關鍵點:
- 單倉 Monorepo 只有“根鎖文件”,子包不生成獨立鎖;所有安裝動作最終寫回根鎖。
- 鎖文件應提交到倉庫,CI/同事機器據此實現一致的安裝結果。
- 發佈到 npm 不會攜帶鎖文件;庫消費者不受你的鎖文件影響。
4.常用命令:
# 嚴格模式(鎖與 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項目目錄結構:
|-- 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 配置文件:
packages:
- 'packages/*'
這樣一來,/packages下的所有項目都會被 pnpm 識別為 workspace 的成員包
4.下面我們來看JS/TS項目領域,monorepo項目如何通過pnpm/changeset/turbo等工具解決下面三個問題:
- 共享代碼/依賴,依賴管理
- 版本更新、自動tag和發佈
- 並行/串行構建所有項目,處理構建順序
5.後續我們用這個monorepo項目結構進行講解。(記住這個結構,下面會多次用到)
|-- 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:共享代碼/依賴,依賴管理
這個問題可以拆成兩個子問題來看:
- 多個子包依賴外部第三方npm包,比如依賴
vue/react,那麼這些這些包是不是可以共享(而不是每個子包項目都安裝一遍)? - 子包之間存在依賴,比如子包A依賴本地的子包B、C,但B、C都還沒發佈怎麼依賴?能不能本地B、C鏈接到A的node_modules下呢?
1.pnpm i 安裝依賴
針對上面的第一個問題,通過把依賴安裝在根目錄,那麼子項目尋找依賴就會採用根目錄的依賴,從而實現共享依賴
--workspace參數
根目錄下安裝vue:
pnpm i -w vue
-w相當於--workspace,表示在根目錄下安裝依賴。
比如我要安裝typescript:
pnpm i -wD typescript
-D表示安裝devDependencies,-w和-D可以合併為-wD。
安裝完成後,只有根目錄package.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這個子包中安裝另外兩個依賴。
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會出現:
"dependencies": {
"@caikengren/uni-hooks-shared": "workspace:^",
"@caikengren/uni-use-request": "workspace:^"
}
@workspace:^
@workspace:^ 是一種特殊的協議前綴,用於在依賴聲明中引用當前 workspace 中的本地包,並自動使用該包的版本號加上 ^ 語義化版本範圍。
也就是説,pnpm 會:
- 在當前 workspace 中查找名為
my-local-pkg的本地包,不去遠程下載了; - 獲取它在
package.json中聲明的版本號(比如1.2.3); - 將依賴解析為
^1.2.3,即允許安裝兼容的次要版本更新(遵循 semver 規範); - 但前提是這個包必須存在於 workspace 中。如果不存在,構建會失敗。
2.pnpm自動link
前面,我們利用@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 中該包的精確版本。
{
"dependencies": {
"@caikengren/packageA": "1.0.0"
}
}
2.@caikengren/packageA@workspace:^ 打包後 (產物/發佈到 npm):
pnpm 會將其替換為以當前版本為基準的 (^) 範圍。
{
"dependencies": {
"@caikengren/packageA": "^1.0.0"
}
}
changeset 發佈
問題2:版本更新、changelog、自動tag和發佈
changesets 主要關心 monorepo 項目下子項目版本的更新、changelog 文件生成、包的發佈。一個 changeset 是個包含了在某個分支或者 commit 上改動信息的 md 文件,它會包含這樣一些信息:
- 需要發佈的包
- 包版本的更新層級(遵循 semver 規範)
- CHANGELOG 信息
1.項目準備
安裝工具
pnpm i -wD @changesets/cli
安裝完成後,會在node_modules/.bin目錄(依賴包提供的命令會出現在這)看到changeset,意味着可以用npx來運行這個命令。
初始化
npx changeset init
會多出一個changeset的文件,如下
2.本地認證準備
假設你已經註冊了一個npm賬號(或私有倉庫賬號),然後通過terminal登錄,在終端輸入
npm adduser
# 或者(效果一樣)
npm login
3.發佈流程
假設你已經改過代碼並且提交了,準備發佈新版本。
1.創建一個新的 changeset 文件。
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倉庫
npx changeset publish
-
説明:
- 它會執行
npm publish對每個需要發佈的包。 - 默認只發布那些版本號發生變化的包。
- 需要你已經登錄到 npm(
npm whoami)。 - 此外,會給每個版本變動的包打tag.
- 它會執行
先提交和push代碼
git add .
git commit -m 'chore(release): v1.1.0'
git push
再發布(企業級發佈,這一步通常屬於CI,在雲端流水線完成)
npx changeset publish
然後在npm倉庫 可以看到發佈的三個包
turborepo 構建
問題3:並行/串行構建所有項目,處理構建順序
1.介紹
為什麼需要 Turborepo
- Monorepo 常見痛點:構建串行、慢;跨包依賴編排複雜;重複構建浪費時間。
yarn workspace通常串行構建,整體耗時長且不可控。pnpm workspace能並行,但對複雜依賴拓撲的順序編排有限,且缺少內建的強緩存機制。- Turborepo面向這些問題,提供有序並行、增量與緩存的組合解法。
它解決了什麼問題
- 構建編排:用任務依賴圖(DAG)明確“誰先誰後”,避免手工腳本膠水。
- 並行執行:獨立包並行跑,依賴包按順序跑,縮短全量構建時間。
- 增量構建:對輸入做哈希,只重建被改動影響的任務,減少無效工作。
- 本地與遠程緩存:同樣輸入直接複用產物;接入遠程緩存後,CI 與開發者之間共享結果。
2.並行和構建編排
使用turbo初始化一個 Monorepo 的項目,有以下幾個 package:
apps/web,依賴 sharedapps/docs,依賴 sharedpackages/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構建完成。
配置如下:
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"test": {
"dependsOn": ["^build"]
},
"lint": {},
}
}
tasks:聲明任務編排與產物輸出,key是turbo run Xxx的Xxx。dependsOn:用^task表示“先跑上游依賴的同名任務”,自動根據依賴圖排序。outputs:聲明任務產物(如dist/**),讓 Turborepo準確緩存和複用構建結果。
關於^task的理解是,不同包的任務編排:
當前 package 執行 build 任務之前,需要 先運行它依賴的包(dependencies / devDependencies / peerDependencies) 的 build 任務。
當你配置了"dependsOn": ["^build"],那麼「web和docs 的構建依賴share構建完成後,再執行」,否則不會。
關於dependsOn另外一種情況,沒有^,表示同一個包中的任務編排。比如下面就表達了所有的test任務執行前,要先完成build命令。
{
"tasks": {
"test": { "dependsOn": ["build"] }
}
}
進一步,你還可以限定到某個包的 任務編排。比如下面表達了 所有lint任務執行前,要先完成utils包的build任務。
{
"tasks": {
"lint": {
"dependsOn": ["utils#build"]
}
}
}
3.緩存和增量構建
第一次trubo run build後,會生成緩存存放在 node_modules/.cache/turbo/目錄下
第一次構建:還沒產物(沒有dist目錄),記錄子包項目文件的hash值。
第二次構建:子包文件的hash標識和之前的比較,如果沒變化,則跳過構建。
配置如下:
//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 等任務關閉緩存,保證開發時的即時性。
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"dev": {
"cache": false
},
}
}
--filter:按包名或路徑過濾執行範圍。
turbo run <command> --filter=<package name>
構建(打包)工具
tsdown的主要功能
相關功能請查閲tsdown中文文檔,目前可以看到vueuse項目已經採用了tsdown來打包庫項目。
下面簡單介紹下tsdown功能:
1.零配置TypeScript支持
tsdown內置了對TypeScript的完整支持,無需額外的配置:
{
"scripts": {
"build": "tsdown"
}
}
2.多格式輸出
支持同時生成多種模塊格式:
{
"tsdown": {
"format": ["esm", "cjs", "umd"],
"entry": "src/index.ts",
"outDir": "dist"
}
}
並且自動為每個輸出格式生成對應的.d.ts聲明文件:
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,自動移除未使用的代碼:
// src/utils.ts
export function used() { return 'used'; }
export function unused() { return 'unused'; }
// src/index.ts
export { used } from './utils';
// unused函數會被自動移除
4.外部依賴處理
配置外部依賴,避免將依賴打包進輸出文件:
{
"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 lint、npm 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
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): xxx, feat(components): xxx。下面就介紹下常用的一種規範結構。
Commit Message 結構:
-
Header(頭部): 必須包含,是Commit Message的核心部分。
- 類型 (type): 標記Commit的類別,例如
feat(新功能),fix(修復bug),docs(文檔),style(代碼風格) 等。 - 範圍 (scope): 可選,用於説明Commit影響的範圍,如文件、組件或模塊。
- 主題 (subject): 簡短地描述本次提交的內容,通常不超過50個字符,首字母小寫。
- 格式:
type(scope): subject。
- 類型 (type): 標記Commit的類別,例如
-
Body(描述): 可選,對Commit進行詳細描述,可以分成多行。
- 解釋提交的動機、解決的問題等。
- 每行建議不超過72個字符,以避免自動換行影響可讀性。
-
Footer(尾部): 可選,用於關聯Issue編號或標記重大性變更 (Breaking Changes)。
- 關聯Issue: 使用如
Closes #123或Fixes #123。
- 關聯Issue: 使用如
@commitlint/cli 用來校驗 Git 提交信息是否符合約定的格式。配合 @commitlint/config-conventional 使用,可強制統一提交類型(如 feat、fix、docs、refactor、chore 等)與消息結構,提升版本管理與自動化發佈的可靠性。
4.commitizen和cz-git 交互式提交
commitizen提供了cli(命令交互式)的方式來完成Commit Message填寫。
cz-git 是 Commitizen 的一個適配器(adapter),通常配合 commitizen 使用。基於cz-git可以更靈活的控制你的提交Message規範。
配置工具
1.初始化husky目錄
npx husky init
pre-commit是 Git 眾多鈎子中的一個,顧名思義,它在 Commit 之前 執行。也就是説這個文件裏的腳本,會在git commit真正執行前執行,避免不規範的提交。
2.commitizen 配置
- Scripts: 添加 commit 命令來啓動交互式提交。
- Config: 指定 commitizen 使用 cz-git 適配器。
修改package.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:
{
"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(代碼格式化/檢查)。
echo "npx lint-staged" > .husky/pre-commit
3. Commitlint & Cz-git配置
cz-git 的強大之處在於它可以直接讀取 commitlint 的配置,從而實現“一份配置,兩處生效”。
在根目錄新建文件 commitlint.config.js (如果是 type: module 項目則為 .mjs 或 .cjs):
// 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 格式。
echo "npx --no-install commitlint --edit \$1" > .husky/commit-msg
結果演示
現在有6個修改
#添加到暫存區
git add .
# 執行commit script(替代git commit)
pnpm commit
六、實戰monorepo形式的npm項目
關於這一塊,可以參考我的一個項目 @caikengren/uni-hooks,後續我會根據這個項目展開寫幾篇博客和大家一起交流學習。