在前端开发中,Stylelint 是一个强大的、可扩展的 CSS/SCSS/Less Linter。它能够帮助我们强制执行编码规范,捕获潜在的错误,并提高代码质量。虽然 Stylelint 提供了大量内置规则,但在某些特定场景下,我们可能需要实现一些项目特有的规则,这时就需要用到 自定义 Stylelint 插件。
本教程将详细讲解如何创建自定义 Stylelint 插件,并提供多个实用的示例,每个示例都包含详细的代码和解释。
1. Stylelint 插件核心概念
在深入代码之前,我们先了解一下 Stylelint 插件的一些核心概念:
1.1 PostCSS AST (抽象语法树)
Stylelint 的底层依赖于 PostCSS。PostCSS 是一个用 JavaScript 编写的工具,可以将 CSS 代码解析成一个抽象语法树 (AST),然后通过插件对这个 AST 进行处理(转换、分析、优化等)。Stylelint 插件就是利用 PostCSS 解析出的 AST 来进行代码检查的。
理解 PostCSS AST 的结构对于编写 Stylelint 插件至关重要。一个简单的 CSS 片段:
.foo {
color: red;
font-size: 16px;
}
会被 PostCSS 解析成类似这样的 AST 结构(简化版):
Root
Rule (selector: ".foo")
Declaration (prop: "color", value: "red")
Declaration (prop: "font-size", value: "16px")
Stylelint 插件通过遍历这个 AST,访问不同的节点(如 Root、Rule、Declaration、AtRule、Comment 等),然后对这些节点进行检查。
1.2 Stylelint 规则结构
一个 Stylelint 规则通常由以下几个部分组成:
-
ruleName: 规则的唯一标识符,通常是plugin-name/rule-name的形式。 -
messages: 一个对象,包含规则可能发出的所有错误消息。这些消息通过键值对定义,方便在规则内部引用。 -
meta: 一个可选对象,用于提供规则的元数据,如url(指向规则文档)、fixable(是否可自动修复)。 -
rule函数 (或create函数) : 这是规则的核心逻辑所在。它接收两个参数:primaryOption: 规则的主要选项(例如,一个字符串、一个布尔值或一个数组)。secondaryOptions: 规则的次要选项,通常是一个对象,用于更复杂的配置。- 这个函数返回一个函数,该函数接收
root(PostCSS AST 的根节点) 和result(Stylelint 的结果对象) 作为参数。在这个返回的函数中,我们遍历 AST 并执行检查。
-
report函数: Stylelint 提供的一个工具函数,用于报告错误。它接收一个包含错误信息的对象作为参数,例如message、node(触发错误的 AST 节点)、index(错误在节点值中的索引) 等。
1.3 插件的入口文件 (index.js)
一个 Stylelint 插件通常是一个 NPM 包。它的入口文件 (index.js) 负责导出所有自定义规则。
// index.js
const stylelint = require('stylelint');
const rules = require('./lib/rules'); // 导入所有规则
const plugin = stylelint.createPlugin(
'my-custom-plugin', // 插件名称
rules // 规则对象
);
module.exports = plugin;
2. 搭建 Stylelint 插件项目
首先,我们需要创建一个项目文件夹,并初始化 package.json。
mkdir stylelint-plugin-my-rules
cd stylelint-plugin-my-rules
npm init -y
安装 Stylelint 作为开发依赖:
npm install stylelint postcss --save-dev
项目结构通常如下:
stylelint-plugin-my-rules/
├── index.js # 插件的入口文件
├── package.json
├── .stylelintrc.json # 用于测试的 Stylelint 配置文件
├── lib/
│ ├── rules/ # 存放所有规则的目录
│ │ ├── rule-one.js # 规则一的实现
│ │ ├── rule-one.test.js # 规则一的测试
│ │ ├── rule-two.js # 规则二的实现
│ │ ├── rule-two.test.js # 规则二的测试
│ │ └── index.js # 导出所有规则的辅助文件
│ └── utils/ # 存放通用工具函数的目录 (可选)
└── node_modules/
lib/rules/index.js 文件用于统一导出所有规则,方便 index.js 入口文件导入。
// lib/rules/index.js
module.exports = {
'no-important-in-properties': require('./no-important-in-properties'),
'no-duplicate-properties-within-rule': require('./no-duplicate-properties-within-rule'),
'selector-class-pattern': require('./selector-class-pattern'),
'property-value-unit-allowed': require('./property-value-unit-allowed'),
// 更多规则...
};
3. 自定义 Stylelint 插件示例
接下来,我们将通过四个具体的示例来详细讲解如何编写自定义 Stylelint 插件。每个示例都包含规则的实现和对应的测试代码。
示例 1: no-important-in-properties (禁止在属性值中使用 !important)
问题描述: 强制团队成员避免使用 !important 关键字来覆盖样式,因为它会导致 CSS 优先级难以管理。
规则名称: my-rules/no-important-in-properties
实现思路:
- 遍历 PostCSS AST 中的所有
Declaration节点。 - 对于每个
Declaration节点,检查其important属性是否为true。 - 如果为
true,则报告错误。
代码实现:
lib/rules/no-important-in-properties.js
// lib/rules/no-important-in-properties.js
// 引入 stylelint 模块,用于创建规则和报告错误
const stylelint = require('stylelint');
// 获取 stylelint.utils,其中包含 report 等实用函数
const { report, ruleMessages, validateOptions } = stylelint.utils;
// 定义规则的名称。它将作为 Stylelint 配置中的一个键。
// 格式通常是 `plugin-name/rule-name`
const ruleName = 'my-rules/no-important-in-properties';
// 定义规则可能发出的所有错误消息。
// 这样可以集中管理消息,方便修改和国际化。
const messages = ruleMessages(ruleName, {
rejected: (property) => `Unexpected !important in "${property}"`, // 错误消息模板,接收属性名作为参数
});
// 定义规则的元数据 (可选)。
// meta.url: 指向规则文档的 URL。
// meta.fixable: 如果规则可以自动修复,则设置为 true。
const meta = {
url: 'https://example.com/stylelint-plugin-my-rules/no-important-in-properties',
fixable: false, // 这个规则不能自动修复
};
// 规则的核心逻辑函数。
// 它接收 primaryOption 和 secondaryOptions 作为参数。
// 返回一个函数,该函数接收 PostCSS AST 的根节点 (root) 和 Stylelint 的结果对象 (result)。
const rule = (primaryOption, secondaryOptions) => {
// 验证规则的选项。
// primaryOption 期望是布尔值 true,表示启用此规则。
// secondaryOptions 在此规则中未使用。
const validOptions = validateOptions(
result,
ruleName,
{
actual: primaryOption,
possible: [true], // 规则只接受 true 作为 primaryOption
},
{
actual: secondaryOptions,
possible: {}, // 规则不接受 secondaryOptions
optional: true,
},
);
// 如果选项无效,则不执行规则检查。
if (!validOptions) {
return;
}
// 返回一个函数,这个函数才是 Stylelint 真正用来遍历 AST 和执行检查的。
return (root, result) => {
// 遍历 AST 中的所有 "decl" (declaration) 节点。
// PostCSS 提供了 walkDecls 方法来遍历所有声明。
root.walkDecls((decl) => {
// decl 是一个 PostCSS Declaration 节点对象。
// 它有一个 important 属性,如果值为 !important,则为 true。
if (decl.important) {
// 如果检测到 !important,则报告错误。
report({
ruleName, // 规则名称
result, // Stylelint 结果对象
node: decl, // 触发错误的 AST 节点 (这里是 Declaration 节点)
message: messages.rejected(decl.prop), // 使用之前定义的消息模板,传入属性名
});
}
});
};
};
// 将规则的各个部分导出。
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;
测试代码:
lib/rules/no-important-in-properties.test.js
// lib/rules/no-important-in-properties.test.js
// 引入 stylelint 模块,用于测试规则
const stylelint = require('stylelint');
// 引入要测试的规则
const rule = require('./no-important-in-properties');
// 获取 stylelint.testUtils,其中包含 testRule 函数
const { testRule } = stylelint;
// 定义规则名称
const ruleName = rule.ruleName;
// 定义规则消息
const messages = rule.messages;
// 使用 testRule 函数来编写测试用例
testRule({
ruleName, // 要测试的规则名称
config: true, // 规则的配置,这里设置为 true 表示启用规则
// 有效代码测试用例 (不应该有报错)
accept: [
{
code: '.foo { color: red; }',
description: 'should accept declaration without !important',
},
{
code: '.bar { background: url("image.png"); }',
description: 'should accept declaration with url',
},
{
code: 'a { display: block; }',
description: 'should accept simple declaration',
},
],
// 无效代码测试用例 (应该有报错)
reject: [
{
code: '.foo { color: red !important; }',
description: 'should reject declaration with !important',
// 期望的报错信息
message: messages.rejected('color'),
// 报错的行号和列号
line: 1,
column: 14,
},
{
code: 'a { font-size: 16px !important; }',
description: 'should reject another declaration with !important',
message: messages.rejected('font-size'),
line: 1,
column: 18,
},
{
code: `
.box {
margin: 10px !important;
padding: 20px;
}
`,
description: 'should reject !important in multi-line rule',
message: messages.rejected('margin'),
line: 3,
column: 18,
},
],
});
示例 2: no-duplicate-properties-within-rule (禁止在同一规则块内出现重复属性)
问题描述: 防止在同一个 CSS 规则块(例如 .foo { ... })中定义两次相同的 CSS 属性,这通常是笔误或代码冗余。
规则名称: my-rules/no-duplicate-properties-within-rule
实现思路:
- 遍历 PostCSS AST 中的所有
Rule节点(即 CSS 选择器规则)。 - 对于每个
Rule节点,维护一个已检查属性的集合。 - 遍历
Rule节点下的所有Declaration节点。 - 在遍历过程中,如果遇到一个属性名已经在集合中存在,则报告错误。否则,将该属性名添加到集合中。
代码实现:
lib/rules/no-duplicate-properties-within-rule.js
// lib/rules/no-duplicate-properties-within-rule.js
const stylelint = require('stylelint');
const { report, ruleMessages, validateOptions } = stylelint.utils;
const ruleName = 'my-rules/no-duplicate-properties-within-rule';
const messages = ruleMessages(ruleName, {
rejected: (property) => `Unexpected duplicate property "${property}" within the same rule`,
});
const meta = {
url: 'https://example.com/stylelint-plugin-my-rules/no-duplicate-properties-within-rule',
fixable: false, // 不能自动修复
};
const rule = (primaryOption, secondaryOptions) => {
const validOptions = validateOptions(
result,
ruleName,
{
actual: primaryOption,
possible: [true],
},
{
actual: secondaryOptions,
possible: {},
optional: true,
},
);
if (!validOptions) {
return;
}
return (root, result) => {
// 遍历 AST 中的所有 "rule" 节点 (即 CSS 规则块,如 .foo { ... })
root.walkRules((ruleNode) => {
// 使用 Set 来存储当前规则块中已经遇到的属性名,以便快速检查重复
const seenProperties = new Set();
// 遍历当前规则块 (ruleNode) 的所有子节点。
// PostCSS 节点的 `nodes` 属性包含了其所有子节点。
// 这里我们只关心 `declaration` 类型的子节点。
ruleNode.walkDecls((decl) => {
// decl.prop 是声明的属性名 (例如 'color', 'font-size')
const propertyName = decl.prop;
// 检查当前属性名是否已经在 seenProperties 集合中存在
if (seenProperties.has(propertyName)) {
// 如果存在,说明是重复属性,报告错误
report({
ruleName,
result,
node: decl, // 报告错误的节点是重复的声明
message: messages.rejected(propertyName), // 使用定义好的错误消息
});
} else {
// 如果是第一次遇到这个属性,将其添加到集合中
seenProperties.add(propertyName);
}
});
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;
测试代码:
lib/rules/no-duplicate-properties-within-rule.test.js
// lib/rules/no-duplicate-properties-within-rule.test.js
const stylelint = require('stylelint');
const rule = require('./no-duplicate-properties-within-rule');
const { testRule } = stylelint;
const ruleName = rule.ruleName;
const messages = rule.messages;
testRule({
ruleName,
config: true,
accept: [
{
code: '.foo { color: red; font-size: 16px; }',
description: 'should accept rule with unique properties',
},
{
code: '.bar { background: url("image.png"); color: blue; }',
description: 'should accept rule with different properties',
},
{
code: `
.baz {
margin: 10px;
padding: 20px;
border: 1px solid black;
}
`,
description: 'should accept multi-line rule with unique properties',
},
{
code: `
.foo { color: red; }
.bar { color: blue; } /* Different rules can have same properties */
`,
description: 'should accept same property in different rules',
},
{
code: `.foo { --my-var: red; --my-var: blue; }`,
description: 'should accept duplicate custom properties (CSS variables)',
// Note: By default, this rule will reject custom properties as well.
// If you want to allow them, you'd need to add logic to check if decl.prop starts with '--'.
// For this example, we'll keep it simple and reject all duplicates.
},
],
reject: [
{
code: '.foo { color: red; color: blue; }',
description: 'should reject duplicate property in single line',
message: messages.rejected('color'),
line: 1,
column: 20, // 第二个 color 的起始列
},
{
code: `
.bar {
font-size: 16px;
color: red;
font-size: 1em; /* Duplicate */
}
`,
description: 'should reject duplicate property in multi-line rule',
message: messages.rejected('font-size'),
line: 5,
column: 11, // 第二个 font-size 的起始列
},
{
code: `
a {
padding: 10px;
margin: 5px;
padding: 20px; /* Duplicate */
display: block;
}
`,
description: 'should reject duplicate property with other properties in between',
message: messages.rejected('padding'),
line: 5,
column: 11,
},
],
});
示例 3: selector-class-pattern (强制类名使用 kebab-case 格式)
问题描述: 强制所有 CSS 类名都遵循 kebab-case (短横线命名法),例如 my-class 而不是 myClass 或 my_class。
规则名称: my-rules/selector-class-pattern
实现思路:
- 遍历 PostCSS AST 中的所有
Rule节点。 - 对于每个
Rule节点,获取其selector字符串。 - 使用 PostCSS 的
selector-parser库来解析复杂的选择器字符串,提取出所有的类名。 - 对每个提取出的类名,使用正则表达式检查其是否符合 kebab-case 格式。
- 如果不符合,则报告错误。
注意: 这个示例需要额外安装 postcss-selector-parser 库。
npm install postcss-selector-parser --save-dev
代码实现:
lib/rules/selector-class-pattern.js
// lib/rules/selector-class-pattern.js
const stylelint = require('stylelint');
const { report, ruleMessages, validateOptions } = stylelint.utils;
const selectorParser = require('postcss-selector-parser'); // 引入选择器解析器
const ruleName = 'my-rules/selector-class-pattern';
const messages = ruleMessages(ruleName, {
expected: (selector, pattern) => `Expected class selector "${selector}" to match pattern "${pattern}"`,
});
const meta = {
url: 'https://example.com/stylelint-plugin-my-rules/selector-class-pattern',
fixable: false, // 自动修复类名模式通常很复杂,不建议自动修复
};
// 默认的 kebab-case 正则表达式
// ^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$
// - ^[a-z]:必须以小写字母开头
// - [a-z0-9]*:后面可以跟任意数量的小写字母或数字
// - (?:-[a-z0-9]+)*:非捕获组,表示可以有零个或多个 `-` 后面跟着至少一个字母或数字的组合
const defaultPattern = '^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$';
const rule = (primaryOption, secondaryOptions) => {
// 验证规则选项
// primaryOption 可以是一个布尔值 true (使用默认模式) 或一个正则表达式字符串
// secondaryOptions 在此规则中未使用
const validOptions = validateOptions(
result,
ruleName,
{
actual: primaryOption,
possible: [true, (value) => typeof value === 'string' && value.length > 0], // 允许 true 或非空字符串
},
{
actual: secondaryOptions,
possible: {},
optional: true,
},
);
if (!validOptions) {
return;
}
// 根据 primaryOption 确定使用的正则表达式
const pattern = typeof primaryOption === 'string' ? primaryOption : defaultPattern;
const regex = new RegExp(pattern);
return (root, result) => {
// 遍历 AST 中的所有 "rule" 节点
root.walkRules((ruleNode) => {
// 使用 selectorParser 解析当前规则的 selector 字符串
selectorParser((selectors) => {
// 遍历解析后的所有选择器 (例如,.foo, .bar, #baz 等)
selectors.walkClasses((classNode) => {
// classNode 是一个表示 CSS 类选择器的节点
// classNode.value 是类名本身 (例如 'my-class', 'myClass')
const className = classNode.value;
// 检查类名是否符合正则表达式模式
if (!regex.test(className)) {
// 如果不符合,则报告错误
report({
ruleName,
result,
node: ruleNode, // 报告错误的节点是整个 Rule 节点
// 由于 classNode 没有直接的 source 信息,我们通常报告整个 ruleNode,
// 并在 message 中指明具体是哪个类名不符合。
// 如果需要更精确的定位,可以计算 classNode 在 selector 字符串中的索引。
index: classNode.sourceIndex, // 报告错误在 selector 字符串中的起始索引
message: messages.expected(className, pattern),
});
}
});
}).processSync(ruleNode.selector); // 同步处理选择器字符串
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;
测试代码:
lib/rules/selector-class-pattern.test.js
// lib/rules/selector-class-pattern.test.js
const stylelint = require('stylelint');
const rule = require('./selector-class-pattern');
const { testRule } = stylelint;
const ruleName = rule.ruleName;
const messages = rule.messages;
// 默认的 kebab-case 模式
const defaultPattern = '^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$';
testRule({
ruleName,
config: true, // 使用默认的 kebab-case 模式
accept: [
{
code: '.my-class { color: red; }',
description: 'should accept kebab-case class name',
},
{
code: '.another-long-class-name { font-size: 16px; }',
description: 'should accept longer kebab-case class name',
},
{
code: '.c1 { display: block; }',
description: 'should accept single letter and number class name',
},
{
code: '.my-class-123 { width: 100%; }',
description: 'should accept kebab-case with numbers',
},
{
code: '#id-selector { /* ID selectors are ignored */ }',
description: 'should ignore ID selectors',
},
{
code: 'div { /* Element selectors are ignored */ }',
description: 'should ignore element selectors',
},
{
code: '[data-test="foo"] { /* Attribute selectors are ignored */ }',
description: 'should ignore attribute selectors',
},
{
code: '.parent .child-element { /* Multiple selectors */ }',
description: 'should accept multiple valid class names in a selector',
},
],
reject: [
{
code: '.myClass { color: red; }',
description: 'should reject camelCase class name',
message: messages.expected('myClass', defaultPattern),
line: 1,
column: 1,
},
{
code: '.my_class { font-size: 16px; }',
description: 'should reject snake_case class name',
message: messages.expected('my_class', defaultPattern),
line: 1,
column: 1,
},
{
code: '.MyClass { display: block; }',
description: 'should reject PascalCase class name',
message: messages.expected('MyClass', defaultPattern),
line: 1,
column: 1,
},
{
code: '.123-class { width: 100%; }',
description: 'should reject class name starting with a number',
message: messages.expected('123-class', defaultPattern),
line: 1,
column: 1,
},
{
code: `
.container { /* Valid */ }
.ChildElement { /* Invalid */ }
`,
description: 'should reject PascalCase in multi-line CSS',
message: messages.expected('ChildElement', defaultPattern),
line: 3,
column: 9,
},
{
code: '.parent .ChildElement { /* Invalid */ }',
description: 'should reject invalid class name in complex selector',
message: messages.expected('ChildElement', defaultPattern),
line: 1,
column: 10,
},
],
});
// 测试自定义模式
testRule({
ruleName,
config: '^[A-Z][a-zA-Z0-9]*$', // 允许 PascalCase 模式
accept: [
{
code: '.MyComponent { color: blue; }',
description: 'should accept PascalCase with custom pattern',
},
{
code: '.AnotherOne { font-size: 1em; }',
description: 'should accept another PascalCase with custom pattern',
},
],
reject: [
{
code: '.my-component { color: red; }',
description: 'should reject kebab-case with PascalCase pattern',
message: messages.expected('my-component', '^[A-Z][a-zA-Z0-9]*$'),
line: 1,
column: 1,
},
{
code: '.myComponent { font-size: 16px; }',
description: 'should reject camelCase with PascalCase pattern',
message: messages.expected('myComponent', '^[A-Z][a-zA-Z0-9]*$'),
line: 1,
column: 1,
},
],
});
示例 4: property-value-unit-allowed (限制特定属性的单位)
问题描述: 强制某些 CSS 属性只能使用特定的单位。例如,font-size 只能使用 px 或 rem,而不能使用 em 或 pt。
规则名称: my-rules/property-value-unit-allowed
实现思路:
- 遍历 PostCSS AST 中的所有
Declaration节点。 - 获取属性名和属性值。
- 根据配置,检查当前属性是否在需要限制单位的列表中。
- 如果需要限制,则从属性值中提取单位。
- 检查提取的单位是否在允许的单位列表中。
- 如果不符合,则报告错误。
注意: 提取单位需要一些正则表达式或字符串处理。
代码实现:
lib/rules/property-value-unit-allowed.js
// lib/rules/property-value-unit-allowed.js
const stylelint = require('stylelint');
const { report, ruleMessages, validateOptions } = stylelint.utils;
const ruleName = 'my-rules/property-value-unit-allowed';
const messages = ruleMessages(ruleName, {
rejected: (property, unit, allowedUnits) =>
`Unexpected unit "${unit}" for property "${property}". Allowed units are: ${allowedUnits.join(', ')}`,
});
const meta = {
url: 'https://example.com/stylelint-plugin-my-rules/property-value-unit-allowed',
fixable: false, // 自动修复单位需要改变值,通常不自动修复
};
// 辅助函数:从属性值中提取单位
// 匹配数字后紧跟的非空白字符,直到遇到空白或字符串结束
// 示例: "16px" -> "px", "1.5rem" -> "rem", "20%" -> "%", "auto" -> ""
function extractUnit(value) {
const match = value.match(/(\d+.?\d*|.\d+)([a-zA-Z%]+)/);
return match ? match[2] : '';
}
const rule = (primaryOption, secondaryOptions) => {
// 验证规则选项
// primaryOption 期望是一个对象,键是属性名,值是允许的单位数组
// 例如: { 'font-size': ['px', 'rem'], 'width': ['%', 'px', 'vw'] }
const validOptions = validateOptions(
result,
ruleName,
{
actual: primaryOption,
possible: [
(value) =>
typeof value === 'object' &&
value !== null &&
!Array.isArray(value) &&
Object.keys(value).every(
(prop) => Array.isArray(value[prop]) && value[prop].every((unit) => typeof unit === 'string'),
),
],
},
{
actual: secondaryOptions,
possible: {},
optional: true,
},
);
if (!validOptions) {
return;
}
return (root, result) => {
// 遍历 AST 中的所有 "decl" (declaration) 节点
root.walkDecls((decl) => {
const property = decl.prop;
const value = decl.value;
// 检查当前属性是否在配置中需要限制单位
if (primaryOption[property]) {
const allowedUnits = primaryOption[property];
const unit = extractUnit(value);
// 如果提取到了单位,并且该单位不在允许的列表中
if (unit && !allowedUnits.includes(unit)) {
report({
ruleName,
result,
node: decl,
message: messages.rejected(property, unit, allowedUnits),
});
}
}
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;
测试代码:
lib/rules/property-value-unit-allowed.test.js
// lib/rules/property-value-unit-allowed.test.js
const stylelint = require('stylelint');
const rule = require('./property-value-unit-allowed');
const { testRule } = stylelint;
const ruleName = rule.ruleName;
const messages = rule.messages;
testRule({
ruleName,
config: {
'font-size': ['px', 'rem'],
'width': ['%', 'px', 'vw'],
'line-height': [''], // 允许无单位值 (例如 '1.5')
},
accept: [
{
code: '.foo { font-size: 16px; }',
description: 'should accept px for font-size',
},
{
code: '.bar { font-size: 1.2rem; }',
description: 'should accept rem for font-size',
},
{
code: '.baz { width: 100%; }',
description: 'should accept percentage for width',
},
{
code: '.qux { width: 200px; }',
description: 'should accept px for width',
},
{
code: '.abc { width: 50vw; }',
description: 'should accept vw for width',
},
{
code: '.def { line-height: 1.5; }',
description: 'should accept unitless value for line-height',
},
{
code: '.ghi { margin: 10em; }',
description: 'should ignore properties not in config',
},
{
code: '.jkl { font-size: 0; }',
description: 'should accept zero value without unit',
},
{
code: '.mno { padding: auto; }',
description: 'should accept keyword values without unit',
},
],
reject: [
{
code: '.foo { font-size: 1em; }',
description: 'should reject em for font-size',
message: messages.rejected('font-size', 'em', ['px', 'rem']),
line: 1,
column: 15,
},
{
code: '.bar { font-size: 12pt; }',
description: 'should reject pt for font-size',
message: messages.rejected('font-size', 'pt', ['px', 'rem']),
line: 1,
column: 15,
},
{
code: '.baz { width: 100em; }',
description: 'should reject em for width',
message: messages.rejected('width', 'em', ['%', 'px', 'vw']),
line: 1,
column: 12,
},
{
code: `
.qux {
font-size: 1.25em; /* Invalid */
width: 50vh; /* Invalid */
}
`,
description: 'should reject multiple invalid units in multi-line CSS',
warnings: [
{
message: messages.rejected('font-size', 'em', ['px', 'rem']),
line: 3,
column: 18,
},
{
message: messages.rejected('width', 'vh', ['%', 'px', 'vw']),
line: 4,
column: 16,
},
],
},
{
code: '.abc { line-height: 16px; }',
description: 'should reject px for line-height (only unitless allowed)',
message: messages.rejected('line-height', 'px', ['']),
line: 1,
column: 18,
},
],
});
4. 整合与使用自定义插件
-
在
package.json中配置脚本:
为了方便测试,可以在package.json中添加一个lint脚本:{ "name": "stylelint-plugin-my-rules", "version": "1.0.0", "description": "A collection of custom Stylelint rules.", "main": "index.js", "scripts": { "test": "jest", "lint": "stylelint "**/*.css"" }, "keywords": [ "stylelint", "stylelint-plugin", "css" ], "author": "Your Name", "license": "MIT", "devDependencies": { "jest": "^29.7.0", "postcss": "^8.4.39", "postcss-selector-parser": "^6.1.0", "stylelint": "^16.7.0" } }注意: 运行测试需要安装
jest。npm install jest --save-dev -
配置
.stylelintrc.json:
在你的项目根目录(或者插件项目根目录用于测试)创建.stylelintrc.json文件,并添加你的自定义插件和规则。{ "plugins": [ "./index.js" // 如果你的插件发布到 npm,这里可以是 "stylelint-plugin-my-rules" ], "rules": { "my-rules/no-important-in-properties": true, "my-rules/no-duplicate-properties-within-rule": true, "my-rules/selector-class-pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$", "my-rules/property-value-unit-allowed": { "font-size": ["px", "rem"], "width": ["%", "px", "vw"], "line-height": [""] }, // 也可以配置 Stylelint 内置规则 "indentation": 2, "color-no-invalid-hex": true } }这里,
"plugins": ["./index.js"]指向了你本地的插件入口文件。如果你将插件发布到 npm,并安装到其他项目中,那么路径会是stylelint-plugin-my-rules(假设你的插件包名为stylelint-plugin-my-rules)。 -
运行 Stylelint:
在你的项目根目录创建一个test.css文件,包含一些符合和不符合你规则的代码:/* test.css */ .my-valid-class { color: red; font-size: 16px; width: 100%; line-height: 1.5; } .InvalidClass { /* Should be rejected by selector-class-pattern */ color: blue !important; /* Should be rejected by no-important-in-properties */ font-size: 1.2em; /* Should be rejected by property-value-unit-allowed */ width: 200px; color: green; /* Should be rejected by no-duplicate-properties-within-rule */ } .another-valid-class { margin: 10px; /* This property is not covered by property-value-unit-allowed */ }然后运行:
npm run lint你将看到 Stylelint 报告的错误,这些错误包含了你自定义规则的报错信息。
总结
通过以上示例,我们详细了解了 Stylelint 自定义插件的开发流程:
- 理解 PostCSS AST: 这是所有 Stylelint 规则的基础。
- 定义规则结构: 包括
ruleName、messages、meta和核心rule函数。 - 遍历 AST 节点: 使用
root.walkDecls()、root.walkRules()等方法。 - 检查节点属性: 根据业务逻辑检查
decl.prop、decl.value、decl.important、ruleNode.selector等。 - 报告错误: 使用
stylelint.utils.report()函数。 - 编写测试: 使用
stylelint.testUtils.testRule()确保规则的正确性。 - 配置和使用: 在
.stylelintrc.json中引用插件和规则。
自定义 Stylelint 插件为前端团队提供了极大的灵活性,可以根据项目或团队的特定需求,创建高度定制化的代码规范检查,从而进一步提升代码质量和开发效率。