本篇文章,我们从零开发一个自定义的 Stylelint插件工程。
完整项目地址参见
打开终端工具(我用的iterm),cd 到平时存储项目的目录;
键入mkdir stylelint-plugin 创建一个名为 stylelint-plugin 的文件夹;

cd 进 stylelint-plugin目录,键入npm init ,接着一路回车,根据提示 输入内容即可;
这一步后,项目中会新增一个package.json文件;
仿照StylelintGitHub 构造项目结构如下:
├── lib // 规则相关内容都放在`lib`目录
│ ├── index.js
│ ├── rules
│ │ ├── index.js
│ │ └── selector-class-no-elements // 每个文件夹下是一个规则,文件夹名即为规则名
│ │ └── index.js // 规则内容
│ └── utils
├── package.json
└── README.md
npm install stylelint postcss-selector-parser -D
selector-class-no-elements 规则为例,编写代码。======================================
步骤4中 安装的postcss-selector-parser 插件,就是解析css中选择器的,
先在根目录中创建一个 test.js 文件,内容如下:
const SelectorParser = require('postcss-selector-parser');
const transform = selectors => {
selectors.walk(node => {
console.log(node);
})
}
SelectorParser(transform).processSync('#app');
然后运行 node test.js, log结果如下:

前者 “Selector” 类型 表示 选择器;
后者 “ID” 类型 表示 ID选择器
======================================
lib/rules/selector-class-no-elements/index.js 内容如下:
/*
* @Desc: class选择器不能以"elements-" 开头
* @Author: 前端卡卡西
* @Date: 2022-06-25 17:10:14
*/
const { nameSpace, newMessage, validateOptions, report } = require('../../utils');
const parseSelector = require("../../utils/parseSelector");
const ruleName = nameSpace('selector-class-no-elements');
const messages = newMessage(ruleName, {
expected: (selector => `Expected CLASS selector ".${selector}" not to contain "elements-"`)
})
const rule = primary => {
return (postcssRoot, postcssResult) => {
const validOptions = validateOptions(postcssRoot, postcssResult, {
actual: primary
})
if (!validOptions) return;
// walkRules 为遍历所有 Rule类型节点
postcssRoot.walkRules(ruleNode => {
const selector = ruleNode.selector;
parseSelector(selector, postcssResult, ruleNode, (fullSelector) => {
// walk为遍历,同forEach
fullSelector.walk(selectorNode => {
// 筛选class选择器
if (selectorNode.type !== 'class') return;
const {value, sourceIndex} = selectorNode;
// 筛选以 "elements-" 开头的选择器
if (value.indexOf('elements') !== 0) return;
// 标记提示结束为止
const endIndex = sourceIndex + value.length;
// 提示用户信息
report({
result: postcssResult,
ruleName,
message: messages.expected(value),
node: ruleNode,
index: sourceIndex,
endIndex
})
})
})
})
}
}
rule.ruleName = ruleName;
rule.messages = messages;
module.exports = rule;
lib/utils/index.js 内容如下
const stylelint = require("stylelint")
function nameSpace(ruleName) {
// 前缀视个人情况而定
return `stylelint-plugin/${ruleName}`;
}
function newMessage(ruleName, options) {
return stylelint.utils.ruleMessages(ruleName, options);
}
function validateOptions(result, ruleName, options) {
return stylelint.utils.validateOptions(result, ruleName, options);
}
function report(problem) {
console.log({problem});
return stylelint.utils.report(problem);
}
module.exports = {
nameSpace,
newMessage,
validateOptions,
report
}
lib/utils/parseSelector.js 内容如下:
const SelectorParser = require("postcss-selector-parser");
module.exports = function parseSelector(selector, result, node, callback) {
try {
return SelectorParser(callback).processSync(selector);
} catch(err) {
result.warn(`Cannot parse selector (${err})`, { node, stylelintType: 'parseError' });
return undefined;
}
}
lib/rules/index.js 内容如下:
const selectorClassNoElements = require('./selector-class-no-elements');
const rules = {
"selector-class-no-elements": selectorClassNoElements
}
module.exports = rules;
lib/index.js 内容如下:
const { createPlugin } = require("stylelint");
const { nameSpace } = require("./utils");
const rules = require("./rules");
// 通过createPlugin 创建 plugin
const rulesPlugins = Object.keys(rules).map(ruleName => {
return createPlugin(nameSpace(ruleName), rules[ruleName])
})
module.exports = rulesPlugins;
测试用例 借助 jest 工具。
npm i jest jest-preset-stylelint -D
package.json 中增加如下配置:...
"jest": {
"clearMocks": true,
"collectCoverage": false,
"collectCoverageFrom": [
"lib/**/*.js"
],
"coverageDirectory": "./.coverage/",
"coverageReporters": [
"lcov",
"text"
],
"coverageThreshold": {
"global": {
"branches": 75,
"functions": 75,
"lines": 75,
"statements": 75
}
},
"setupFiles": [
"./jest-setup.js"
],
"testEnvironment": "node",
"roots": [
"lib"
],
"testRegex": ".*\\.test\\.js$|lib/.*/__tests__/.*\\.js$"
},
...
jest-setup.js 文件,内容如下:const getTestRule = require('jest-preset-stylelint/getTestRule');
global.testRule = getTestRule({ plugins: ["./lib"] });
lib/rules/selector-class-no-elements/__tests__/index.js 中编写测试代码:(每个规则下边都加一个__tests__/index.js 测试用例文件)const { messages, ruleName} = require("..");
testRule({
ruleName,
config: true,
// 可通过用例
accept: [
{ code: '.app {}' }
],
// 不可通过用例
reject: [
{ code: '.elements-app {}',
message: messages.expected('elements-app'),
line: 1, // 提示开始行
column: 1, // 提示开始位置
endLine: 1, // 提示结束行
endColumn: 13 // 提示结束位置
}
]
})
npm run testpackage.json 配置:
...
"scripts": {
"test": "jest"
},
...
运行 npm run test

结果如上,大功告成。
├── jest-setup.js
├── lib
│ ├── index.js
│ ├── rules
│ │ ├── index.js
│ │ └── selector-class-no-elements
│ │ ├── __tests__
│ │ │ └── index.js
│ │ └── index.js
│ └── utils
│ ├── index.js
│ └── parseSelector.js
├── package-lock.json
├── package.json
├── test.js
└── README.md
规则开发完成,需要找项目进行覆盖测试。一般这种工具类项目都是直接发布到 NPM仓库,使用时通过 npm install xxx 安装。
要想不发布npm包,本地测试 可以通过 npm link 命令,在本地的 node_modules 中生成一个软链接,链接到开发目录。
npm link 生成软链接
如果 npm link 之后报错:npm ERR! Error: EACCES: permission denied,是因为权限原因,可使用 sudo npm link 重试。
cd 到测试工程,“安装” stylelint-plugin 依赖

打开测试工程的 node_modules, 可以看到:

配置测试工程 .stylelintrc.js
module.exports = {
...
"plugins":[
"stylelint-plugin"
...
],
rules: {
'stylelint-plugin/selector-class-no-elements': true,
...
},
};