Monorepo 中 Stylelint 的配置与工程化实践:共享配置包与 pnpm 踩坑指南
说句实在话我见过太多标榜“企业级”的 Monorepo 模板TypeScript 开得很严格ESLint 也配了一整套团队约定唯独 Stylelint 要么是缺位的要么只是从网上抄了一份装点门面。真正跑起来之后样式代码的 review 基本靠眼神每个人提交的 class 命名、属性顺序、颜色格式都是自己的风格。这一篇是这个系列里我投入时间最多的一块之一因为样式工程化最容易被人轻视但改起来又最伤筋动骨。我会从 Monorepo 多包场景的特殊性讲起把 Stylelint 的选型、配置、集成方式、以及我在 pnpm workspace 里踩过的一连串坑完整拆开。在很多中小团队里样式出问题不会让流水线红掉也不会让功能无法上线它只会在 review 的时候让人多打几行没有营养的评论。真正把 Stylelint 变成工程模板的一部分之后那些讨论全部消失大家只需要在 pipeline 里看到绿色就知道代码是符合规范的。这篇文章的目标很直接看完之后你能照着把 Stylelint 完整接入现有的 Monorepo 模板包括共享配置包怎么设计、哪些规则值得开、哪些默认规则是坑以及为什么同一个配置在不同包之间会失效。1. 在写第一条规则之前先想清楚 Monorepo 里的样式治理问题1.1 为什么单仓库样式治理比想象中复杂单仓库多包和单包工程最大的区别在于样式文件不再只属于一个“项目”而是分散在多个 package、多个应用、多个场景里。你可能会同时拥有一个 React 应用、一个 Vue 应用、一个公共组件库再加上若干 Node 工具包。每个包对样式的诉求不一样组件库可能只有纯 scss 变量和 mixinReact 应用可能有 styled-componentsVue 应用大概率是 scoped css。如果把 Stylelint 理解成“一个配置文件跑全仓库”那第一天就会被各类语法差异直接击穿。更麻烦的是 Monorepo 里天然存在“物理边界”和“逻辑边界”的错位。物理上工作区是一个仓库但逻辑上每个 package 应该有自己相对独立的规范继承链。根目录放一套默认规范是合理的但组件库可能需要额外的 scss 规则Vue 包又需要支持 vue 文件内的样式块。Stylelint 本身的配置机制支持这种继承但如果一开始没有把共享配置从使用方剥离开后面很容易演变成每个包复制一份配置的局面。还有一个被很多人忽略的点Monorepo 依赖提升会让样式相关的工具链解析变得不稳定。pnpm 的严格依赖结构下插件和语法解析器的安装位置稍有不对就会出现“root 上能跑通、分包里直接报错”的灵异现象。这部分我在后面专门用一整章讲坑这里先建立一个认知——在 Monorepo 里配 Stylelint本质上是在同时处理工具链依赖解析和配置文件继承两件事比单包工程多了一个维度。1.2 Stylelint 和 Prettier、ESLint 的分工边界很多团队把样式格式化交给 Prettier把样式规范交给 Stylelint这个分工方向是对的但边界经常模糊。Prettier 解决的是“格式一致”比如缩进、引号、换行、分号Stylelint 解决的是“代码质量与约定”比如 class 命名是否符合 BEM、颜色值是否应该用变量、属性是否被重复声明、废弃的 at-rule 是否还在使用。这里有一个关键变化值得注意Stylelint 15 之后官方把一批与格式相关的 rules 移除了强烈建议用户不要再用 Stylelint 做纯格式化工作。这意味着如果你在网上搜到大量旧教程里面配置了declaration-colon-newline-after这类的格式化规则它在新版里会直接报 unknown 或不起作用。如果要让代码风格统一正确姿势是prettier --write如果要强制团队约定才走 Stylelint。我在模板里的做法是Prettier 负责所有语法的格式统一Stylelint 负责“Prettier 格式化不了的那部分约定”。比如selector-class-pattern这种规则Prettier 毫无感知必须交给 Stylelint。同时为了不让两套工具打架Stylelint 配置里不应该再出现纯样式类的规则这也是官方新版推荐的方向。有一些教程还会让你装stylelint-prettier让 Stylelint 在 lint 时顺便检查 prettier 格式实测下来它会拖慢 lint 速度而且在--fix时还会和编辑器保存行为产生争抢我在模板里干脆没有启用。1.3 企业级模板里样式规范应该覆盖哪些范围在搭这个模板之前我列了一份“样式规范需求清单”不是拍脑袋定的而是复盘了过去一年团队里在样式 review 中出现过的所有问题class 命名几乎每个人一套风格有人用驼峰有人用 kebab-case还有人直接拿拼音缩写颜色值有人写十六进制简写有人写完整的 rgbascss 里嵌套层级动辄四五层读起来非常痛苦还有项目里同时存在 scss 的import和现代 sass 的use混用小部分历史包袱。基于这些问题我把企业级模板里的 Stylelint 覆盖范围定成五个维度维度具体关注点示例规则语法正确性落败的写法、无效的声明、未知的伪类block-no-empty、invalid-no-important、selector-type-no-unknown命名约定class、自定义属性、SCSS 变量的命名格式selector-class-pattern、custom-property-pattern、scss/dollar-variable-pattern代码可维护性重复属性、可以简写的属性、嵌套层级过深declaration-block-no-duplicate-properties、max-nesting-depth现代特性约束use 而非 import、现代颜色函数scss/no-old-import、color-function-notation与 Prettier 的边界Stylelint 不再承担格式化职责不配置 stylistic rules这个清单未必适合所有团队比如如果你的项目没有 scss完全不需要 scss 相关规则如果你不用 styled-components也不需要额外加 CSS-in-JS 的语法支持。但框架层级是这样一个共享配置包提供基础规范再通过一个工程特定的配置层覆盖掉不需要的规则最后才是每个 package 的个性化需求。2. Monorepo 里共享 Stylelint 配置的三种组织方式及取舍2.1 方案一只在根目录放一份 .stylelintrc分包共享最简单的做法在仓库根目录放.stylelintrc.cjs所有 package 都不需要建自己的配置文件执行stylelint的时候会自动向上查找最近的配置最终命中根目录文件。对包数量少、技术栈统一的小型仓库这个方案非常高效。你只需要装一份依赖维护一个文件不存在配置漂移的问题。但这个方案在企业级 Monorepo 里很快就会碰壁。一旦某个 package 内出现需要额外规则的文件类型比如 Vue 单文件组件根配置不得不为它专门加overrides时整个配置会变得越来越臃肿技术上可行的前提是代码必须能用 ESLint 和 TypeScript 的覆盖overrides机制但即使能用也不代表好维护。而且根目录配置天然带着“全局统一”的预设违背了 Monorepo 多团队、多技术栈、多节奏的初衷。2.2 方案二独立的共享配置包推荐把 Stylelint 配置抽成一个独立包放在packages/stylelint-config或者packages/shared/lint/stylelint名字叫repo/stylelint-config。这个包自己维护stylelint、stylelint-config-standard、postcss-scss等依赖并导出一个index.cjs配置文件。各 package 只要在自己目录下写一个两行的.stylelintrc.cjsextends这个共享包即可。这个方案的最大好处是配置和依赖绑定在一起可复用、可版本化、可测试。如果某个业务 package 需要额外规则它可以在自己的.stylelintrc里再加一层extends通过 Stylelint 的配置合并机制在共享基础上追加最近规则。更重要的是pnpm 的严格依赖模式下只要共享包在package.json的dependencies里声明了stylelint和所有插件那无论它被哪个 package 安装Stylelint 都能在共享包自己的 node_modules 里找到插件和语法解析器彻底避开“root 能跑、分包找不到”的依赖困境。这也是我在模板里采用的方案。用一个共享配置包来管理规范本质上就是把“规范”当成一等公民纳入了 Monorepo 的包管理体系。2.3 方案三每个 package 完全独立配置每个 package 自己建.stylelintrc自己装依赖自己维护规则。这种方式看起来给了每个团队最大的自由但实际上等于没有规范——配置会在各个包里逐渐漂移最终你会面对三种命名风格、两套颜色写法、以及没有统一兜底规则的混乱状态。我甚至不建议用“分包独立 从线上复制一份公共配置”的折中做法。如果公共部分需要改动你得把所有的 package 都翻出来改一遍。在 Monorepo 里跨包的批量改动并不是不能做只是完全没有必要既然有配置包方案就足够为什么要把维护成本抬起来。2.4 我的最终结构与理由这个模板最终的结构是这样的. ├── packages/ │ └── stylelint-config/ # repo/stylelint-config │ ├── package.json │ ├── rules/ │ │ ├── index.js │ │ ├── scss.js │ │ └── css-in-js.js │ └── index.cjs ├── apps/ │ ├── web/ # React 应用 │ │ └── .stylelintrc.cjs # extends: repo/stylelint-config │ └── dashboard/ # Vue 应用 │ └── .stylelintrc.cjs # extends vue 特定 overrides └── package.json # workspace scripts根目录只放lint:style脚本和 lint-staged 配置不放规则。每条规则的维护入口集中在共享包里业务包只保留极小粒度的个性化覆盖。这样一来共享配置包升级版本之后所有 package 都在依赖锁文件里明确了当前使用的是哪个规范版本配合 CI 的检查可以避免规范偷偷漂移。3. stylelint.config 完整配置拆解选择哪些规则、为什么选它3.1 依赖安装与版本基线在共享配置包packages/stylelint-config/package.json里用pnpm workspace添加依赖。以当前主流的 Stylelint 16 为例Node 版本必须大于等于 18.12.0这一点在搭建模板时就要确认否则后续在旧的 CI 镜像上会直接装都装不上。pnpm --filter repo/stylelint-config add stylelint^16 \ stylelint-config-standard^36 \ stylelint-config-recommended-scss^14 \ postcss-scss^4 \ stylelint-scss^6一个容易混淆的点安装stylelint-config-recommended-scss时它依赖并同时引入了postcss-scss和stylelint-scss理论上你不必手动声明后两者。但在 pnpm 的严格模式下如果你在共享配置里直接使用了scss/前缀的规则为了安全最好也在dependencies里显式声明防止共享包内部解析不到。这属于“花了五秒钟多写两行依赖省掉一次半夜排查故障”的好习惯。如果你同时需要处理 Vue 的.vue文件需要额外安装postcss-htmlpnpm --filter repo/stylelint-config add -D postcss-html3.2 共享包入口配置在packages/stylelint-config/index.cjs里核心配置长这样module.exports { extends: [ stylelint-config-standard, stylelint-config-recommended-scss ], plugins: [], rules: { // 颜色与数值写法 color-hex-length: long, color-function-notation: modern, alpha-value-notation: number, // 命名规范 selector-class-pattern: ^(?:is|has|js|qa)-?|^[a-z][a-zA-Z0-9]*(?:__[A-Za-z0-9])*(?:--[A-zA-Z0-9])?$, custom-property-pattern: ^[a-z][a-z0-9-]*$, // 结构约束 max-nesting-depth: 4, declaration-block-no-duplicate-properties: true, // 不强制要求规则与空行等纯格式相关问题 rule-empty-line-before: null, declaration-empty-line-before: null, comment-empty-line-before: null }, ignoreFiles: [node_modules/**, dist/**, coverage/**] };这段配置里有几个值得解释一下的决策。color-hex-length: long而不是默认的short是因为完整六位十六进制可读性更好也便于与设计稿里的颜色值直接对照。color-function-notation: modern会把rgb(0, 0, 0)自动转成rgb(0 0 0)如果你团队成员不习惯现代写法也可以关掉但既然是模板我更倾向面向未来。把rule-empty-line-before等系列规则置为null是因为这些属于纯格式类新版 Stylelint 标准配置里已经拿掉了大部分保留在旧版本里极易和 Prettier 打架。3.3 SCSS 专属规则追加如果项目里用到 scss我建议把 SCSS 专属规则单独放到rules/scss.js再合并进去。这样不至于把共享包的入口文件堆得太长同时也能让不使用 scss 的包保持轻量。以下几条是我在实际项目里确认过有价值的rules: { scss/at-rule-no-unknown: true, scss/no-old-import: true, scss/dollar-variable-pattern: ^[a-z][a-z0-9-]*$, scss/dollar-variable-empty-line-before: null, scss/operator-no-unspaced: true, scss/load-partial-extension: always, scss/at-import-partial-extension: always }scss/no-old-import是一条非常有价值的现代规范规则。scss 官方很早就在推use取代import但历史项目里import屡禁不止因为总有人觉得“旧的又不是不能跑”。在样式代码里直接启用scss/no-old-import用机器判断消灭讨论这是我在团队落地时觉得最好用的一条规则。operator-no-unspaced则是针对 scss 计算表达式$x $y这类书写不规范的强制纠错器如果团队里有从 stylus 转过来的人很容易踩到这种写法。需要特别注意的是在配置里同时使用customSyntax: postcss-scss是一种常见姿势但如果你用stylelint-config-recommended-scss它内部已经设置好了不需要在入口再重复指定。重复指定的副作用是当你想再用overrides去处理.vue文件时customSyntax 会被全局覆盖导致 Vue 文件里的样式解析失败。这是一个非常隐蔽的坑下一章我会展开讲。3.4 通过 overrides 支持多语言场景企业级 Monorepo 中不同应用到不同技术栈是常态。Vue 单文件组件的.vue文件里既有也可以在overrides里对.vue文件的scoped支持虽然 scoped 现在由 SFC 编译器处理样式块本身仍是纯 CSS。配置写起来是这样的overrides: [ { files: [**/*.vue], customSyntax: postcss-html, rules: { selector-max-id: 0, scss/no-old-import: null } }, { files: [**/*.scss], customSyntax: postcss-scss } ]这里的手法很关键先对**/*.vue设置customSyntax: postcss-html再对**/*.scss设置postcss-scss。Stylelint 的 overrides 匹配优先级与顺序相关可以保证 scss 变体和 vue 文件都被正确解析。如果你在全局层面写了customSyntax这两个 overrides 局部配置都会失效。这个问题我在这篇文章第 4 章里会做一次完整的排查复盘。3.5 如果你用了 styled-components 等 CSS-in-JSReact 应用里如果用 styled-componentsStylelint 默认的 CSS 解析器完全不认识模板字符串里的样式代码。无论你 search 到多少“用 stylelint-config-styled-components 插件”的旧帖子我要说的是到了 Stylelint 16 的时代stylelint-config-styled-components的那套方案已经基本不更新了更通用的方式是安装postcss-styled-syntax然后对.js/.tsx等文件开启自定义语法pnpm --filter repo/stylelint-config add postcss-styled-syntaxoverrides: [ { files: [**/*.{js,jsx,ts,tsx}], customSyntax: postcss-styled-syntax, rules: { selector-class-pattern: null, declaration-block-no-duplicate-properties: true } } ]需要坦白说明的是CSS-in-JS 里的 Stylelint 校验能力天然弱于独立 CSS/SCSS 文件很多规则会失效。我亲测下来rules 里偏结构类的规则比如block-no-empty、declaration-block-no-duplicate-properties尚可正常工作偏命名类的规则比如selector-class-pattern基本废掉因为模板字符串里既有 JS 表达式又有 CSS 片段AST 结构已经和纯 CSS 不一样了。所以对于 CSS-in-JS 项目我的建议是 Stylelint 的约束范围主要放在“别写出无效声明”这个层面更严格的命名约定最好是交给组件代码层面的审查工具去管。4. 真实踩坑记录pnpm workspace 下 Stylelint 的依赖陷阱排查4.1 现象一分包里样式文件全部报 syntax error我遇到第一个“灵异事件”是根目录执行pnpm lint:style没问题但切到packages/button目录对同一个文件单独执行pnpm stylelint src/index.scss立刻冒出十几条来自postcss的语法错误。报错信息大致长这样Unexpected unknown type scss不过我立即发现根目录的pnpm lint:style是在根 context 下执行的而 app/package 下执行时Stylelint 会向上寻找配置找到的配置文件在packages/stylelint-config共享包里而这个共享包内部只安装了postcss-scss但stylelint被安装的位置是 workspace 根目录的node_modules/.pnpm下。问题来了pnpm 的 node_modules 是物理隔离的。共享配置包可以通过 npm 依赖关系正常加载postcss-scss但 Stylelint 二进制的插件解析逻辑走的是“从执行目录向上查找 node_modules”的机制。而分包执行时向上找到的 node_modules 里不一定能看到共享包内部依赖的postcss-scss于是语法解析失败。排查链路是这样的先用pnpm why postcss-scss追依赖来源确认它存在于根目录的.pnpm商店再在 execute 时用stylelint --config指定完整路径发现 syntax error 消失最后定位到问题不在于“依赖没装”而在于“Stylelint 从哪个位置解析 customSyntax 配置”。最后更直接的修复是在共享配置包的dependencies里显式写入postcss-scss同时保证所有消费包通过 workspace 依赖引用了共享包而不是通过 root 命令直接跑全局的 stylelint。4.2 现象二extends里的配置被静默忽略另一个让我花了整晚排查的问题是某个 package 里的.stylelintrc.cjs写了extends: [repo/stylelint-config]但实际 lint 时只有最基本的内置规则生效共享配置里所有规则都没加载。直接执行stylelint --print-config看到的结果与共享包里的完整配置完全不一致。根因出在 package 名称解析上。当时共享包的名字写成了repo/stylelint-config但在packages/stylelint-config/package.json里漏配了main: index.cjs。Stylelint 解析extends时会尝试把字符串当作 npm 包名 require如果没有 main 字段它只能解析到包目录本身而目录里没有合法的配置文件入口于是静默降级成“不加载任何扩展”。这个过程不会在 CLI 上输出任何错误只有--print-config能看出异常。修复方式是补上main字段并且额外设置了exports字段来避免未来的 resolve 歧义{ name: repo/stylelint-config, version: 0.1.0, main: index.cjs, exports: { .: ./index.cjs } }经历过这次之后我把所有共享配置包都加了--print-config检查步骤写进验证脚本防止以后再出现“配置看起来存在但实际没加载”的静默失败。4.3 现象三modern 颜色函数导致 PostCSS 解析冲突Stylelint 16 的默认配置里color-function-notation: modern是开启的这要求 CSS 里的rgb()/hsl()使用空格分隔参数。对于纯 css 文件这没有任何问题。但对于需要兼容老浏览器的项目如果产物里混着rgb(0, 0, 0)和rgb(0 0 0)构建工具链条里如果有一个旧版 autoprefixer 或 postcss-preset-env 版本在转译时可能会冲突。报错信息不会出现在 Stylelint 里而是出现在构建阶段表现为“已声明属性”或“自动前缀生成失败”。排查起来很绕因为 Stylelint 和构建阶段看似互不相干。实际根因是--fix时 Stylelint 把rgb(0, 0, 0)改成了rgb(0 0 0)而旧构建链不认识后者。我在模板里给出的兜底方案是如果业务依赖的构建链路较旧不要开启color-function-notation: modern直接用默认值或设为null如果团队愿意推进现代 CSS那就同步升级到postcss-preset-env4.x 或更新版本确保构建链同样支持空格分隔语法。这是典型的“Stylelint 配置牵连构建”的例子写进指南里可以让后来人少走几小时弯路。4.4 现象四vscode 插件提示 disable 但又找不到文件这是编辑器集成层面的一个略显恼人的问题配置好 vscode-stylelint 插件后打开.scss文件插件报[stylelint] Unknown word (CssSyntaxError)。这通常不是配置问题而是插件默认只对css语言激活没有把scss加入 validate 列表。需要在.vscode/settings.json里显式声明{ stylelint.enable: true, stylelint.validate: [css, scss, vue, postcss], editor.codeActionsOnSave: { source.fixAll.stylelint: explicit } }vue和postcss都需要在 validate 数组里显式加上否则插件不会识别这些语言。如果项目使用 styled-components还需要在 validate 里加上typescript和javascriptreact不过这里要注意开启之后插件会对整套模板字符串内容都做解析性能有明显的下降。实测下来我一般不建议在业务包默认开启 CSS-in-JS 的编辑器校验更推荐把它留在 CI 阶段跑避免开发时编辑器卡顿。5. 把 Stylelint 嵌进开发流程脚本、lint-staged 与 CI 增量检查5.1 package.json 脚本设计与执行频率有了完整配置还必须让 Stylelint 出现在正确的执行时机。在 Monorepo 根目录的package.json里我保留了四个与样式相关的脚本{ scripts: { lint:style: stylelint \**/*.{css,scss,vue}\ --ignore-path .gitignore, lint:style:fix: stylelint \**/*.{css,scss,vue}\ --fix --ignore-path .gitignore, lint:style:changed: lint-staged, lint:style:ci: stylelint \**/*.{css,scss,vue}\ --ignore-path .gitignore --max-warnings 0 } }--max-warnings 0是 CI 里最关键的参数。Stylelint 对部分规则会有 warning 级别普通的stylelint执行遇到 warning 时退出码仍然是 0这会导致所有人都认为流水线是绿的但隐患并没解决。加上--max-warnings 0后任何一个 warning 都会让退出码变为 1真正实现零容忍。另一个细节是 glob 模式的双引号必须保留。如果不加引号在 zsh 里 glob 由 shell 展开进入 stylelint 的参数可能只剩下一部分文件尤其在 Monorepo 多层目录下会肉眼很难发现地漏掉某些子包。加了双引号之后glob 交给 stylelint 内部的 glob 库处理所有文件会被统一匹配。5.2 lint-staged 配置只校验暂存文件完整仓库运行 stylelint 在大仓库里可能耗时几十秒甚至几分钟让开发者在每次 commit 前跑完整校验显然不现实。lint-staged 只处理暂存区里的文件是性能与覆盖面的最佳平衡。这里有一件容易忽视的事不能只对样式文件配置 stylelint因为你可能会在同一个提交里同时改动a.scss和b.tslint-staged 会分别执行不同工具对各自文件做检查互不干扰。根目录的.lintstagedrc.cjs中样式相关部分module.exports { **/*.{css,scss,vue}: [stylelint --fix --allow-empty, prettier --write], **/*.{js,ts,tsx,jsx}: [eslint --fix, prettier --write] };有几个细节需要说明。--allow-empty是为了避免在某些情况下 lint-staged 传入空文件列表导致命令报错。prettier --write放最后一环是为了保证无论 stylelint --fix 做了多少改动最终格式一定符合 prettier 输出。顺序别反否则 prettier 修改完再被 stylelint fix 一回可能出现二次修改。但这些命令想在 root 下执行stylelint 二进制的路径至关重要。由于我们用了repo/stylelint-config共享配置包在 lint-staged 的配置文件里可以用require.resolve找到实际二进制位置也可以直接依赖 workspace 根 node_modules 中的stylelint可执行文件。在 pnpm workspace 中pnpm 会把 root 的依赖提升到根node_modules/.bin所以大多数情况下直接写stylelint也能运行。但如果未来有人把共享包独立发布出仓库使用最好把 lint-staged 和 stylelint 都显式声明在根 devDependencies 里避免环境差异。5.3 在 CI 里做增量还是全量很多 Monorepo 模板在 CI 里直接跑全量的pnpm lint:style。包不多的时候没有问题一旦 apps 数量到 15 个以上每次全量 lint 的时间会随着代码规模线性增长。如果团队执行的还是“每次合并前全量跑”最终会演变成大家为了通过 CI 不得不把 lint 时间也纳入整个研发节奏。更好的做法是区分 PR 与主干分支。PR 里只检查本次变更涉及的文件用git diff --name-only --diff-filterACM拿到变更列表再过滤样式文件然后传给 stylelint。主干分支上保留全量 lint用于兜底检查是否有 PR 漏掉的情况。一个便捷的 shell 写法示例Linux/macOS CI 环境STYLE_FILES$(git diff --name-only --diff-filterACM origin/main...HEAD -- *.css *.scss *.vue) if [ -n $STYLE_FILES ]; then echo $STYLE_FILES | xargs pnpm exec stylelint --max-warnings 0 fixargs传参在文件数量巨大时可能超出命令行长度限制但通常一次 PR 里改动的样式文件很少会多到触发这个问题。如果团队的 PR 动辄改数百个文件那需要走中间文件或风格参数文件的方式处理但那是另一个层面的问题。5.4 分享一个让 CI 真正“意思到位”的小技巧最后补充一个我踩过几次之后沉淀下来的习惯CI 里除了跑 stylelint还在配置共享包里放了一个test脚本专门用于验证共享配置本身能被正确解析并且导出的规则没有非法值cd packages/stylelint-config pnpm stylelint --config index.cjs --print-config src/__fixtures__/normal.css /dev/null这样做的意义在于很多人改动共享配置包里的某条规则时可能不小心把值写成了 Stylelint 无法识别的类型导致所有下游包的 lint 全挂。CI 里加一个极轻量的冒烟测试能在共享包发布之前就发现这个问题。类似地我还在共享包里放了一小组 fixtures 文件正常与异常跑一轮stylelint --config index.cjs x.css断言期望的退出码。这套东西整体上花的代码量不大但对模板的稳定性贡献非常可观。最后说一句实际的从零搭一套企业级 Monorepo 模板最大的收获不是把所有工具都配齐而是让每个工具都出现在该出现的位置并且可以独立升级、独立验证。Stylelint 的共享配置包化带来的直接结果是团队不用再为样式规则发生争论流水线会自动把不符合约定的代码挡在门外。我还记得第一次跑通 lint-staged 的样式自动修复团队里一位后端兼职写前端的同事反馈说“这比 code review 里被点名要舒服多了”——这种“无感约束”正是工程化真正想达成的效果。如果这篇文章里的某一段能帮你少花一个晚上排查依赖问题那这五千多字没有白写。