说句实在话,我见过太多标榜“企业级”的 Monorepo 模板,TypeScript 开得很严格,ESLint 也配了一整套团队约定,唯独 Stylelint 要么是缺位的,要么只是从网上抄了一份装点门面。真正跑起来之后,样式代码的 review 基本靠眼神,每个人提交的 class 命名、属性顺序、颜色格式都是自己的风格。这一篇是这个系列里我投入时间最多的一块之一,因为样式工程化最容易被人轻视,但改起来又最伤筋动骨。我会从 Monorepo 多包场景的特殊性讲起,把 Stylelint 的选型、配置、集成方式、以及我在 pnpm workspace 里踩过的一连串坑完整拆开。
在很多中小团队里,样式出问题不会让流水线红掉,也不会让功能无法上线,它只会在 review 的时候让人多打几行没有营养的评论。真正把 Stylelint 变成工程模板的一部分之后,那些讨论全部消失,大家只需要在 pipeline 里看到绿色,就知道代码是符合规范的。这篇文章的目标很直接:看完之后你能照着把 Stylelint 完整接入现有的 Monorepo 模板,包括共享配置包怎么设计、哪些规则值得开、哪些默认规则是坑,以及为什么同一个配置在不同包之间会失效。
1. 在写第一条规则之前,先想清楚 Monorepo 里的样式治理问题
1.1 为什么单仓库样式治理比想象中复杂
单仓库多包和单包工程最大的区别在于:样式文件不再只属于一个“项目”,而是分散在多个 package、多个应用、多个场景里。你可能会同时拥有一个 React 应用、一个 Vue 应用、一个公共组件库,再加上若干 Node 工具包。每个包对样式的诉求不一样:组件库可能只有纯 scss 变量和 mixin,React 应用可能有 styled-components,Vue 应用大概率是 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,还有人直接拿拼音缩写;颜色值有人写十六进制简写,有人写完整的 rgba;scss 里嵌套层级动辄四五层,读起来非常痛苦;还有项目里同时存在 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.cjs,extends这个共享包即可。
这个方案的最大好处是:配置和依赖绑定在一起,可复用、可版本化、可测试。如果某个业务 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-html:
pnpm --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-JS
React 应用里如果用 styled-components,Stylelint 默认的 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.ts,lint-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-filter=ACM拿到变更列表再过滤样式文件,然后传给 stylelint。主干分支上保留全量 lint,用于兜底检查是否有 PR 漏掉的情况。
一个便捷的 shell 写法示例(Linux/macOS CI 环境):
STYLE_FILES=$(git diff --name-only --diff-filter=ACM 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 里被点名要舒服多了”——这种“无感约束”正是工程化真正想达成的效果。如果这篇文章里的某一段能帮你少花一个晚上排查依赖问题,那这五千多字没有白写。