• 从零开发 stylelint规则(插件)


    本篇文章,我们从零开发一个自定义的 Stylelint插件工程。
    完整项目地址参见

    一、准备阶段

    1. PostCss官网 (Stylelint使用PostCss先将css内容解析成AST抽象语法树,再通过对AST进行分析处理,达成想要的结果。没错Eslint也是这样);
    2. Stylelint官网 (想学习一项技术,先看看它的官网)
    3. StylelintGitHub (照猫画虎)
    4. 开发环境:macOs-12.1 Apple M1 Pro(不太重要,可能跟后期的插件有关)

    二、开发阶段

    1. 创建目录

    打开终端工具(我用的iterm),cd 到平时存储项目的目录;
    键入mkdir stylelint-plugin 创建一个名为 stylelint-plugin 的文件夹;
    在这里插入图片描述

    2. npm 初始化

    cd 进 stylelint-plugin目录,键入npm init ,接着一路回车,根据提示 输入内容即可;
    这一步后,项目中会新增一个package.json文件;

    3. 构造项目结构

    仿照StylelintGitHub 构造项目结构如下:

    ├── lib  // 规则相关内容都放在`lib`目录
    │   ├── index.js
    │   ├── rules
    │   │   ├── index.js
    │   │   └── selector-class-no-elements  // 每个文件夹下是一个规则,文件夹名即为规则名
    │   │       └── index.js   // 规则内容
    │   └── utils
    ├── package.json
    └── README.md
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    4. 安装部分依赖
    npm install stylelint postcss-selector-parser -D
    
    • 1
    1. 我们以开发 selector-class-no-elements 规则为例,编写代码。
      这个规则意义为:class选择器不能以’elements-'开头
      因为第三方组件库(ElementUI、Ant Design等),组件名称是以’elements-'开头,开发过程中不要对这些三方组件覆盖样式,以免造成难以预料的bug。

    ======================================

    这里我们插一下,先看看css解析后的内容是什么样的

    步骤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');
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    然后运行 node test.js, log结果如下:
    在这里插入图片描述
    前者 “Selector” 类型 表示 选择器;
    后者 “ID” 类型 表示 ID选择器

    ======================================

    5. 正式开发:

    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;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62

    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
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    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;
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    lib/rules/index.js 内容如下:

    const selectorClassNoElements = require('./selector-class-no-elements');
    
    const rules = {
      "selector-class-no-elements": selectorClassNoElements
    }
    
    module.exports = rules;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    6. 编写测试用例

    测试用例 借助 jest 工具。

    1. 先安装对应依赖:
    npm i jest jest-preset-stylelint -D
    
    • 1
    1. 再配置jest,在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$"
      },
    ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    1. 根目录下创建 jest-setup.js 文件,内容如下:
    const getTestRule = require('jest-preset-stylelint/getTestRule');
    
    global.testRule = getTestRule({ plugins: ["./lib"] });
    
    
    • 1
    • 2
    • 3
    • 4
    1. 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 // 提示结束位置
        }
      ]
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    7. npm run test

    package.json 配置:

    ...
    "scripts": {
        "test": "jest"
    },
    ...
    
    • 1
    • 2
    • 3
    • 4
    • 5

    运行 npm run test
    在这里插入图片描述
    结果如上,大功告成。

    8. 最终目录结构
    ├── 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    三、项目覆盖测试

    规则开发完成,需要找项目进行覆盖测试。一般这种工具类项目都是直接发布到 NPM仓库,使用时通过 npm install xxx 安装。

    要想不发布npm包,本地测试 可以通过 npm link 命令,在本地的 node_modules 中生成一个软链接,链接到开发目录。

    1. 在上边的目录中执行 npm link 生成软链接
      在这里插入图片描述

    如果 npm link 之后报错:npm ERR! Error: EACCES: permission denied,是因为权限原因,可使用 sudo npm link 重试。

    1. cd 到测试工程,“安装” stylelint-plugin 依赖
      在这里插入图片描述
      打开测试工程的 node_modules, 可以看到:
      在这里插入图片描述

    2. 配置测试工程 .stylelintrc.js

    module.exports = {
      ...
      "plugins":[
      	"stylelint-plugin"
      	...
      ],
      rules: {
        'stylelint-plugin/selector-class-no-elements': true,
        ...
      },
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
  • 相关阅读:
    Java环形链表_leetcode141(快慢指针、set)
    路由事件2
    注解、AOP
    Android Studio插件开发 - Dora SDK的IDE插件
    Llama 2: 深入探讨ChatGPT的开源挑战者
    arping命令详解
    安防监控/智能分析EasyCVR视频汇聚平台海康/大华/宇视摄像头国标语音GB28181语音对讲配置流程
    【luogu P5320】勘破神机(数学)(数列特征方程)(第一类斯特林数)
    编码踩坑——记一次fastjson引发的空指针问题、引用标识$ref
    ROS2——分布式通信(十二)
  • 原文地址:https://blog.csdn.net/RedaTao/article/details/125460293