3. 从零搭建企业级 Monorepo 工程化模板:集成 Husky 9 + lint-staged

导读

在前两章中,我们已经完成了 ESLint 与 Prettier 的配置,为 Monorepo 项目建立了统一的代码质量和格式规范。但规范真正落地的关键,不只是"有配置",而是要在代码进入仓库之前自动执行检查,避免开发者忘记运行命令或忽略警告。这正是本章要解决的问题。本章我们将集成 Husky 与 lint-staged:Husky 负责在 Git 提交时触发钩子,lint-staged 则只对暂存区的文件执行 ESLint 与 Prettier,确保每一次提交都经过校验。学完本章,你的项目将拥有一个可靠的提交前自动检查机制,为后续持续集成和团队协作打下坚实基础。

第一步:理解 Husky 9 与 lint-staged 的职责

在开始配置之前,先理解两个核心工具各自解决什么问题,以及它们如何配合,这样后续操作会更清晰。

1.1 Git Hooks 与 Husky 9

Git 本身提供了一系列的钩子(hooks),允许你在特定的 Git 事件(如 commitpushmerge)发生前后自动执行自定义脚本。例如,你可以在每次提交前运行测试、检查代码风格,或者校验提交信息。

但原生的 Git hooks 存在一个痛点:它们存放在 .git/hooks 目录下,而该目录不会被 Git 跟踪,导致团队协作时无法共享同一套钩子,每个人都需要手动配置,而且容易遗漏。

Husky 就是为了解决这个问题而生的。它将 Git hooks 的配置纳入项目仓库中(通常放在 .husky/ 目录),这样所有开发者克隆项目后,只需执行一次初始化(或安装依赖时自动完成),就能获得完全一致的钩子行为。Husky 让我们可以用熟悉的 JavaScript/Shell 来编写钩子,并且配置可版本化、可共享。

1.2 lint-staged:只检查暂存文件

虽然我们可以直接让 Husky 的 pre-commit 钩子执行 eslint .prettier --write .,但这会检查整个项目,随着项目变大,运行速度会明显下降,而且可能会因为历史遗留问题(例如某个旧文件不符合规范)导致提交失败。

lint-staged 专门解决这个问题。它接收 Git 暂存区(staged)的文件列表,然后只对这些文件执行指定的命令。这样有几个好处:

  • 速度快:只处理本次提交涉及的文件,而不是全仓库扫描。
  • 精准:只关心你要提交的内容,不会因为无关文件的旧问题阻塞提交。
  • 可组合 :可以对不同类型的文件指定不同的命令(例如对 .ts 文件执行 eslint --fix,对 .json 文件执行 prettier --write)。

1.3 两者如何配合

整个提交流程可以概括为:

sql 复制代码
git commit  
→ Husky 触发 pre-commit 钩子  
→ lint-staged 获取暂存文件列表  
→ 根据配置对暂存文件执行 ESLint / Prettier  
→ 检查通过 → 继续执行 commit-msg 钩子(如果有)  
→ 检查不通过 → 阻止提交并输出错误信息

下面用一张流程图更直观地展示这个过程:

graph TD A[git commit] --> B{Husky pre-commit} B --> C[lint-staged 获取暂存文件] C --> D{文件类型匹配} D -->|JS/TS/Vue| E[ESLint --fix + Prettier --write] D -->|JSON/MD/CSS| F[Prettier --write] E --> G{检查是否通过?} F --> G G -->|通过| H[继续 commit-msg 钩子] G -->|不通过| I[阻止提交并输出错误] H --> J[提交完成]

注:如果配置了 commitlint,在 commit-msg 阶段还会进一步校验提交信息格式,这部分会在后续步骤中介绍。

第二步:安装依赖

2.1 安装 Husky 9 与 lint-staged

  • 操作 :在项目根目录执行以下命令,将 huskylint-staged 安装到根目录 devDependencies 中。

    bash 复制代码
    pnpm add -D -w husky lint-staged
  • 作用

    • husky:用于管理 Git hooks,让团队可以共享统一的 Git 钩子配置。
    • lint-staged:只对 Git 暂存区的文件执行校验和格式化命令,避免全仓库扫描,提升提交前检查的效率。
  • 为什么安装在根目录

    • Git hooks 作用于整个仓库,所有子包共用一套提交流程即可,无需每个子包单独安装。
    • 根目录统一管理 lint-staged 配置,避免在多包项目中重复维护,符合 Monorepo 的集中治理原则。
  • 注意

    • -w 参数表示将依赖安装到 workspace 根目录,而不是当前子包。如果省略 -w 并在子包目录中执行,pnpm 会默认安装到子包中,这是我们不希望的。
    • 安装完成后,可以执行 pnpm list -D husky lint-staged --depth 0 确认是否安装成功。

第三步:初始化 Husky 9 并生成 pre-commit 钩子

3.1 执行初始化命令

  • 操作:在项目根目录执行以下命令,生成 Husky 所需的目录和基础文件。

    bash 复制代码
    pnpm exec husky init
  • 作用

    • 自动创建 .husky/ 目录,用于存放 Git hooks 的配置脚本。

    • .husky/ 下生成一个示例文件 pre-commit,默认内容通常是 npm test 或类似占位命令。

    • 如果根目录 package.json 中尚未定义 prepare 脚本,该命令会自动添加:

      json 复制代码
      "scripts": {
        "prepare": "husky"
      }
    • prepare 脚本是 npm/pnpm 的生命周期脚本,会在执行 pnpm install 时自动运行,从而在团队协作中确保每个成员拉取代码后都能正确初始化 Husky 钩子,无需手动操作。

  • 注意

    • 初始化后,.husky/pre-commit 文件的内容需要手动修改,我们会在第五步完成。
    • 如果 pnpm exec husky init 命令在你的环境中没有自动添加 prepare 脚本,请手动在根目录 package.jsonscripts 中加上 "prepare": "husky"

第四步:配置 lint-staged

4.1 创建 lint-staged 配置文件

  • 操作 :在项目根目录创建 lint-staged.config.js 文件。

  • 作用:告诉 lint-staged 如何匹配暂存文件,并对不同文件类型执行对应的校验和格式化命令。集中管理全仓库的提交前校验规则。

  • 代码块

    javascript 复制代码
    // 根目录 lint-staged.config.js
    export default {
      // 匹配所有前端代码文件,先执行 ESLint 修复,再执行 Prettier 格式化
      "**/*.{js,mjs,cjs,ts,mts,cts,vue}": [
        "eslint --fix",
        "prettier --write"
      ],
      // 对 JSON、Markdown、样式等文件只执行 Prettier 格式化
      "**/*.{json,md,css,scss,html}": [
        "prettier --write"
      ]
    };
  • 字段解析

    • 第一组 "**/*.{js,mjs,cjs,ts,mts,cts,vue}":使用 glob 通配符递归匹配 Monorepo 中所有子包的相关文件,包括 JavaScript、TypeScript 和 Vue 单文件组件。

    • 数组 ["eslint --fix", "prettier --write"] 表示对这些文件依次执行命令:

      • eslint --fix:自动修复可修复的 ESLint 问题(如缩进、未使用变量等,取决于规则配置)。
      • prettier --write:对修复后的文件进行格式化,确保代码风格统一。
    • 第二组 "**/*.{json,md,css,scss,html}":匹配配置类、文档类和样式类文件,只执行 prettier --write,避免 ESLint 对非 JS 文件误报。

    • 注意 :命令中直接使用 eslintprettier,因为这两个可执行文件已安装在根目录的 node_modules/.bin 中,lint-staged 会自动从该目录查找命令,无需加 pnpm 前缀。

  • 补充说明

    • 如果希望某些文件只检查不自动修复,可以将命令改为 eslint(不带 --fix)。
    • lint-staged 支持传入函数动态生成命令,但当前静态配置已满足大部分需求。

第五步:配置 pre-commit 钩子

5.1 为什么 lint-staged 能只处理暂存文件?

lint-staged 的核心原理是:它通过 Git 命令动态获取当前暂存区(staged)的文件列表,然后只对这些文件执行你配置的命令

具体来说,当你运行 lint-staged 时,它内部大致做了这几件事:

  1. 执行类似 git diff --cached --name-only --diff-filter=ACMR 的命令,得到所有已暂存的文件路径。
  2. 根据你在配置文件中定义的 glob 规则(比如 **/*.{ts,vue})对这些文件进行过滤。
  3. 对每一个匹配的文件,按顺序执行配置的命令(如 eslint --fixprettier --write)。
  4. 如果所有命令都成功(退出码为 0),则 lint-staged 返回成功;如果有任何一个命令失败,则返回失败并输出错误信息,阻止提交。

为什么不用 eslint .

eslint . 会对整个项目目录进行全量扫描,随着文件数量增加,运行时间会越来越长。而且如果仓库中存在历史遗留问题(例如某个旧文件不符合规范),即使与本次提交无关,也会导致提交失败。

lint-staged 的好处

  • 只检查即将提交的内容,避免无关文件干扰。
  • 大幅提升检查速度,适合频繁提交的开发节奏。
  • 可以按文件类型精细控制执行哪些命令。

与 Husky 的关系

Husky 负责在 Git 提交的生命周期触发钩子(如 pre-commit),而 lint-staged 负责在钩子内高效地执行具体校验任务。两者配合,就形成了"提交前只检查暂存文件"的闭环。

5.2 修改 .husky/pre-commit 文件

  • 操作 :打开项目根目录下的 .husky/pre-commit 文件,将默认内容替换为以下命令。

    sh 复制代码
    pnpm lint-staged

    如果文件不存在,可以手动创建,并确保文件具有可执行权限(Linux/macOS 下执行 chmod +x .husky/pre-commit)。

  • 作用 :当开发者执行 git commit 时,Husky 会触发 pre-commit 钩子,并运行 pnpm lint-staged。此时 lint-staged 会读取根目录的 lint-staged.config.js,对暂存文件执行对应的检查与格式化。只有当所有命令成功执行后,Git 才会继续完成提交;否则提交会被阻止。

  • 为什么使用 pnpm lint-staged 而不是直接 lint-stagednpx lint-staged

    • pnpm lint-staged 会优先使用项目内安装的 lint-staged,确保版本一致,避免全局环境干扰。
    • 在 Monorepo 中,通过根目录的 pnpm 运行,可以正确识别 workspace 依赖和脚本路径。
  • 字段解析

    • pnpm:使用 pnpm 作为包管理器来执行命令。
    • lint-staged:要执行的脚本名称,对应根目录 node_modules/.bin/lint-staged
  • 权限提醒 :如果提交时提示 husky - pre-commit script failedPermission denied,大概率是 .husky/pre-commit 文件没有执行权限,执行 chmod +x .husky/pre-commit 即可修复。

第六步:配置 commitlint

6.1 安装 commitlint 相关依赖

  • 操作:在项目根目录执行:

    bsh 复制代码
    pnpm add -D -w @commitlint/cli @commitlint/config-conventional cz-git commitizen
  • 作用

    • @commitlint/cli:提供 commitlint 命令,用于校验提交信息。
    • @commitlint/config-conventional:提供基于 Angular 提交规范的标准规则集,是社区使用最广泛的约定。
    • cz-git:提供交互式提交体验和动态 scope 支持。
    • commitizen:提供 cz 命令,作为交互式提交的入口。
  • 为什么需要 commitlint?

    统一的提交信息格式能让 Git 历史更清晰,便于生成 changelog、定位问题、代码审查以及自动化发布。常见的规范格式为:

    text 复制代码
    type(scope): subject

    例如:

    text 复制代码
    feat(admin): 新增员工管理页面
    fix(api): 修复登录接口返回错误的问题

6.2 创建 commitlint 配置文件(含 cz-git 交互式提交)

  • 操作 :在项目根目录创建 commitlint.config.js 文件。

  • 代码块

    javascript 复制代码
    // docs:https://cz-git.qbb.sh/zh
    import fs from "node:fs";
    import path from "node:path";
    import { defineConfig } from "cz-git";
    
    /**
     * 动态获取 Monorepo 中的子包目录,作为 commitlint 的 scope 选项。
     * 会遍历 apps/ 和 packages/ 两个目录,并额外增加 root 选项。
     */
    function getScopes() {
      const scopes = [];
      const root = process.cwd();
      const appsDir = path.join(root, "apps");
      const packagesDir = path.join(root, "packages");
    
      // 遍历 apps 目录
      if (fs.existsSync(appsDir)) {
        for (const dir of fs.readdirSync(appsDir)) {
          const fullPath = path.join(appsDir, dir);
          if (fs.statSync(fullPath).isDirectory()) {
            scopes.push({ value: `apps/${dir}`, name: `apps/${dir}` });
          }
        }
      }
    
      // 遍历 packages 目录
      if (fs.existsSync(packagesDir)) {
        for (const dir of fs.readdirSync(packagesDir)) {
          const fullPath = path.join(packagesDir, dir);
          if (fs.statSync(fullPath).isDirectory()) {
            scopes.push({ value: `packages/${dir}`, name: `packages/${dir}` });
          }
        }
      }
    
      // 添加根目录选项
      scopes.push({ value: "root", name: "root: 根目录" });
    
      return scopes;
    }
    
    export default defineConfig({
      ignores: [commit => commit.includes("init")],
      extends: ["@commitlint/config-conventional"],
      rules: {
        "type-enum": [
          2,
          "always",
          ["feat", "fix", "docs", "style", "refactor", "perf", "test", "build", "ci", "revert", "chore"],
        ],
      },
      prompt: {
        scopes: getScopes(),
        messages: {
          type: "选择你要提交的类型: ",
          scope: "选择一个提交范围(可选): ",
          customScope: "请输入自定义的提交范围: ",
          subject: "填写简短精炼的变更描述:\n",
          body: '填写更加详细的变更描述(可选)。使用 "|" 换行:\n',
          breaking: '列举非兼容性重大的变更(可选)。使用 "|" 换行:\n',
          footerPrefixesSelect: "选择关联 Issue 前缀(可选): ",
          customFooterPrefix: "输入自定义 Issue 前缀: ",
          footer: "列举关联 Issue (可选) 例如: #31, #I3244:\n",
          confirmCommit: "是否提交或修改 commit ?",
        },
        types: [
          { value: "feat", name: "feat:     🚀  新增功能 | A new feature", emoji: "🚀" },
          { value: "fix", name: "fix:      🐞  修复缺陷 | A bug fix", emoji: "🐞" },
          { value: "docs", name: "docs:     📚  文档更新 | Documentation only changes", emoji: "📚" },
          {
            value: "style",
            name: "style:    🎨  代码格式 | Changes that do not affect the meaning of the code",
            emoji: "🎨",
          },
          {
            value: "refactor",
            name: "refactor: ♻️   代码重构 | A code change that neither fixes a bug nor adds a feature",
            emoji: "♻️",
          },
          { value: "perf", name: "perf:     ⚡️  性能优化 | A code change that improves performance", emoji: "⚡️" },
          {
            value: "test",
            name: "test:     ✅  测试相关 | Adding missing tests or correcting existing tests",
            emoji: "✅",
          },
          {
            value: "build",
            name: "build:    📦️  构建相关 | Changes that affect the build system or external dependencies",
            emoji: "📦️",
          },
          { value: "ci", name: "ci:       🎡  持续集成 | Changes to our CI configuration files and scripts", emoji: "🎡" },
          { value: "revert", name: "revert:   ⏪️  回退代码 | Revert to a commit", emoji: "⏪️" },
          {
            value: "chore",
            name: "chore:    🔨  其他修改 | Other changes that do not modify src or test files",
            emoji: "🔨",
          },
        ],
        useEmoji: true,
        emojiAlign: "center",
        themeColorCode: "",
        useAI: false,
        aiNumber: 1,
        allowCustomScopes: true,
        allowEmptyScopes: true,
        customScopesAlign: "bottom",
        customScopesAlias: "custom",
        emptyScopesAlias: "empty",
        upperCaseSubject: false,
        markBreakingChangeMode: false,
        allowBreakingChanges: ["feat", "fix"],
        breaklineNumber: 100,
        breaklineChar: "|",
        skipQuestions: [],
        issuePrefixes: [{ value: "closed", name: "closed:   ISSUES has been processed" }],
        customIssuePrefixAlign: "top",
        emptyIssuePrefixAlias: "skip",
        customIssuePrefixAlias: "custom",
        allowCustomIssuePrefix: true,
        allowEmptyIssuePrefix: true,
        confirmColorize: true,
        scopeOverrides: undefined,
        defaultBody: "",
        defaultIssues: "",
        defaultScope: "",
        defaultSubject: "",
      },
    });
  • 字段解析

    • getScopes():动态读取 apps/packages/ 目录,自动生成提交范围选项,新增子包后无需手动维护。
    • ignores:包含 "init" 的提交信息会跳过校验,方便初始化提交。
    • extends:继承官方预设规则集。
    • rules.type-enum:覆盖官方预设,只允许指定的 type 列表。
    • prompt:cz-git 的交互配置,定义了选择提示、type 列表(含 emoji)、scope 动态来源等。

6.3 配置 commitizen 使用 cz-git

  • 操作 :在根目录 package.json 中添加以下字段:

    json 复制代码
    {
      "scripts": {
        "commit": "cz"
      },
      "config": {
        "commitizen": {
          "path": "cz-git"
        }
      }
    }
  • 作用 :告诉 commitizen 使用 cz-git 作为适配器,这样执行 pnpm commit 时就能进入 cz-git 的交互式提交界面。

第七步:配置 commit-msg 钩子

7.1 创建 commit-msg 钩子文件

  • 操作 :在项目根目录的 .husky/ 目录下创建 commit-msg 文件,并写入以下内容。

    sh 复制代码
    pnpm commitlint --edit "$1"

    然后赋予该文件可执行权限(Linux/macOS 下执行):

    bash 复制代码
    chmod +x .husky/commit-msg
  • 作用 :当开发者执行 git commit 时,Husky 会触发 commit-msg 钩子,此时会运行 commitlint 命令,校验当前提交信息格式是否符合 commitlint.config.js 中定义的规范。如果校验失败,提交会被阻止,并输出错误提示。

  • 为什么需要这个钩子?

    即使我们使用了 cz-git 交互式提交工具,也只能保证通过它生成的提交信息是规范的。但仍有人可能直接使用 git commit -m "..." 绕过交互式提交,导致提交信息不规范。commit-msg 钩子作为最后一道防线,确保任何方式产生的提交信息都符合团队规范。

  • 字段解析

    • pnpm commitlint:使用 pnpm 执行 commitlint 命令。
    • --edit "$1"$1 是 Git 钩子传入的参数,指向包含提交信息的临时文件。--edit 表示读取该文件内容进行校验,而不是从命令行参数读取。
  • 测试方法

    1. 尝试提交一条不符合规范的信息(例如直接写 abc):

      bash 复制代码
      git add .
      git commit -m "abc"

      预期:提交被拒绝,终端会输出类似 "subject may not be empty""type may not be empty" 的错误。

    2. 使用交互式提交工具生成一条规范信息,或手动输入符合格式的信息(如 feat(apps/hrms-admin): 🚀 新增测试页面),提交应成功通过。

  • 补充说明

    • 如果之前没有使用 husky init 自动生成 .husky/commit-msg 文件,可以手动创建。文件内容必须与上述一致。
    • 在 Windows 系统上,可能需要使用 Git Bash 或其他 Unix 兼容环境来确保钩子脚本正常运行。

第八步:测试提交流程

8.1 测试前准备

在开始测试之前,请确认以下内容已完成:

  • ☑ 根目录已安装 huskylint-staged@commitlint/cli@commitlint/config-conventionalcz-gitcommitizen

  • ☑ 根目录存在 lint-staged.config.js

  • ☑ 根目录存在 commitlint.config.js(使用 cz-git 配置)

  • ☑ 根目录存在 eslint.config.mjs(供 lint-staged 从根目录运行时使用)

  • .husky/pre-commit 内容为 pnpm lint-staged

  • .husky/commit-msg 内容为 pnpm commitlint --edit "$1"

  • ☑ 两个钩子文件均有执行权限(Linux/macOS 下 chmod +x .husky/pre-commit .husky/commit-msg

  • ☑ 至少存在一个子包(例如 apps/hrms-admin)且已安装依赖

8.2 测试代码格式检查(pre-commit 钩子)

场景一:提交包含不符合 ESLint/Prettier 规范的文件

  1. 修改 apps/hrms-admin/src/App.vue,故意制造一个格式问题,例如在标签内添加多余空格,或声明一个未使用的变量。

  2. 执行:

    bash 复制代码
    git add .
    git commit -m "test: 测试 pre-commit"
  3. 预期结果

    • Husky 触发 pre-commit 钩子。
    • lint-staged 对暂存文件运行 ESLint 和 Prettier。
    • 如果发现未使用变量,ESLint 会报错并阻止提交。
    • 如果只有格式问题,Prettier 会自动修复文件,然后 Git 会提示你文件已被修改,需要重新 git add 后再提交。
  4. 处理

    • 如果 ESLint 报错且无法自动修复,手动修复代码后重新提交。
    • 如果 Prettier 自动修复了文件,执行 git add . 重新暂存,然后再次提交。

场景二:提交完全符合规范的代码

  1. 确保 App.vue 没有任何格式或代码质量问题(或者使用 pnpm lint 先修复)。

  2. 执行:

    bash 复制代码
    git add .
    git commit -m "test: 测试 pre-commit 通过"
  3. 预期结果lint-staged 运行成功,Git 继续完成提交。

8.3 测试提交信息校验(commit-msg 钩子)

场景一:直接使用 git commit -m 提交不规范信息

  1. 执行:

    bash 复制代码
    git commit --allow-empty -m "随便写"
  2. 预期结果 :Husky 触发 commit-msg 钩子,commitlint 校验失败,提交被阻止,终端输出类似:

    text 复制代码
    ✖   subject may not be empty
    ✖   type may not be empty

场景二:使用交互式提交工具(cz-git)

  1. 执行:

    bash 复制代码
    pnpm commit
  2. 预期结果

    • 终端进入交互式选择流程:

      • 选择提交类型(feat、fix 等,带 emoji)
      • 选择提交范围(apps/hrms-admin、packages/eslint-config、root 等,由 getScopes() 动态生成)
      • 输入简短描述
      • 可选填写详细描述、关联 Issue 等
    • 最终生成的提交信息符合规范,如:

      text 复制代码
      feat(apps/hrms-admin): 🚀 新增员工管理页面
    • commit-msg 钩子校验通过,提交成功。

第九步:避坑指南

坑 1:lint-staged 执行时根目录找不到 ESLint 配置文件

现象 :提交时 pre-commit 钩子报错:

text 复制代码
ESLint couldn't find an eslint.config.(js|mjs|cjs) file.

原因 :lint-staged 在根目录运行 eslint,而 ESLint 9+ 只在当前工作目录查找配置文件,不会自动向下查找子包配置。子包中的 eslint.config.mjs 无法被根目录的 eslint 识别。

解决方案 :在项目根目录创建 eslint.config.mjs,合并共享配置:

javascript 复制代码
import baseConfig from "@hrms/eslint-config/base-eslint-config";
import tsConfig from "@hrms/eslint-config/typescript-eslint-config";
import vueConfig from "@hrms/eslint-config/vue-eslint-config";

export default [
  {
    ignores: ["**/dist/**", "**/node_modules/**", "**/.turbo/**"],
  },
  ...baseConfig,
  ...tsConfig,
  ...vueConfig,
];

预防:在 Monorepo 中,根目录始终需要一个 ESLint 入口配置文件,供根目录工具(lint-staged、CI/CD)使用。


坑 2:.husky/pre-commitcommit-msg 没有执行权限

现象 :提交时提示 husky - script failedPermission denied

原因:钩子文件没有可执行权限。

解决方案

bash 复制代码
chmod +x .husky/pre-commit .husky/commit-msg

预防:初始化 Husky 后立即检查权限,必要时手动赋予执行权限。


坑 3:Husky 8 和 9 的配置差异

现象 :网上很多教程使用 npx husky installhusky add 命令,但你的 Husky 9 环境不生效。

原因:Husky 8 和 9 的初始化方式完全不同:

版本 初始化命令 配置位置 钩子文件
Husky 9 husky init .husky/ 目录 + prepare 脚本 .husky/pre-commit
Husky 8 husky install package.json"husky" 字段 手动创建或 husky add

解决方案:确认 Husky 版本,使用对应的初始化方式。本文基于 Husky 9。

预防 :开始配置前先查看 package.json 中 husky 的版本号。


坑 4:cz-git 的 cz 命令不存在

现象 :执行 pnpm czgit czCommand not found

原因cz-git 本身不提供 cz 命令,需要配合 commitizen 使用。同时需要在 package.json 中配置 config.commitizen.path 指向 cz-git

解决方案

  1. 安装 commitizen:

    bash 复制代码
    pnpm add -D -w commitizen
  2. 在根目录 package.json 中添加:

    json 复制代码
    {
      "scripts": {
        "commit": "cz"
      },
      "config": {
        "commitizen": {
          "path": "cz-git"
        }
      }
    }
  3. 使用 pnpm commit 启动交互式提交。

预防:cz-git 官方文档中明确说明需要与 commitizen 配合使用。


坑 5:zsh 中 workspace:* 通配符被展开

现象:在 zsh 终端执行:

bash 复制代码
pnpm add -D -w @hrms/eslint-config@workspace:*

报错 zsh: no matches found

原因 :zsh 会把 * 当作文件通配符尝试展开。

解决方案:用引号包住参数:

bash 复制代码
pnpm add -D -w "@hrms/eslint-config@workspace:*"

预防 :在任何包含 * 的命令行参数中,养成加引号的习惯。

第十步:小结

本章我们完成了 Husky 9 + lint-staged + commitlint + cz-git 的完整集成,核心成果包括:

  • 根目录安装 huskylint-staged:统一管理全仓库的 Git hooks 和暂存文件检查。
  • 创建 lint-staged.config.js:按文件类型配置 ESLint 和 Prettier 的执行策略。
  • 配置 .husky/pre-commit:在提交前自动执行 lint-staged,确保代码质量和格式。
  • 根目录创建 eslint.config.mjs:解决 lint-staged 从根目录运行时的配置查找问题。
  • 安装并配置 commitlint + cz-git + commitizen:实现提交信息规范校验和交互式提交体验。
  • 配置 .husky/commit-msg:在提交信息生成后自动校验格式,防止绕过交互式提交。

最终形成的提交流程:

text 复制代码
git add .
pnpm commit(或 git commit)
   ↓
pre-commit 钩子 → lint-staged → ESLint/Prettier 检查暂存文件
   ↓
commit-msg 钩子 → commitlint 校验提交信息
   ↓
提交完成 ✅

这套体系让代码规范和提交规范从"手动执行"变为"自动强制",为后续 Turborepo 任务编排和 CI/CD 打下了坚实基础。


写在最后

本章我们通过 Husky 9 与 lint-staged,将代码规范和格式化检查从"手动执行"升级为"提交前自动执行",为 Monorepo 项目建立了一道质量防线。配合前几章的共享 ESLint/Prettier 配置,现在任何子包都能在提交时自动获得一致的代码校验,减少人为疏漏,提升团队协作效率。

更进一步,我们引入了 commitlint 与 cz-git,让提交信息也进入了规范化轨道:交互式提交降低了规范的门槛,commit-msg 钩子则确保任何提交方式都逃不过格式校验。

后续我将继续完善工程化体系,包括引入 Turborepo 任务编排、配置 CI/CD 等,欢迎持续关注。如果本文对你有帮助,欢迎点赞、收藏、评论,也欢迎指出不足之处,一起交流进步。

相关推荐
IC一站式服务1 小时前
数据中心供电架构大变革:从AC/DC转换到超导体的未来之路
架构
MetaLite1 小时前
Java通用枚举驱动下拉框-元数据接口与前端契约
java·前端·状态模式
Json____1 小时前
基于 Node + Vue3 的选课管理系统:从教务业务到全栈实践
前端·node·毕设·wwwoop.com
创新技术阁1 小时前
FastapiAdmin 定时任务实现原理与新建任务实操指南
前端·后端·fastapi
3A Cloud1 小时前
Huashu Design:把 AI Agent 变成一间以 HTML 为画布的设计工作室
前端·人工智能·html
你别说话了1 小时前
Webpack 如何迁移重构到 Vite
前端·webpack·重构
catastrophe_zy1 小时前
如何用 WebCodecs 在浏览器里实现高清录屏 —— 无插件、无水印、直接导出 MP4
前端·javascript·录屏
芳心粽伙饭1 小时前
HTML第七章 表格标签
前端·html
我就是DaLing呀!1 小时前
vue3 + 独立的数据管理 Store实现视频剪辑功能
前端·typescript·vue3·canvas·store