手把手教你写ESlint 插件

746 阅读6分钟

由于项目的历史原因,项目中同一个功能的依赖就会有多个,比如用于excel解析和导出的依赖就有excel.js和xlsx.js,为了提高项目的提高项目的可维护性和减少项目的体积,通过调研决定采用自定义eslint来进行依赖规范。本文章的目的是记录并介绍如何编写一个适用于团队风格的 eslint 插件和规则并在项目中应用。

创建项目

ESLint 官方为了方便开发者开发插件,提供了使用 Yeoman 模板 (generator-eslint) 。 Yeoman 是一个为 web 应用提供脚手架的工具,它提供了三种能力:

yo——脚手架,自动生成工具

Grunt、gulp——构建工具

Bower、npm——包管理工具 这里就用到了 Yeaman 脚手架插件市场中的 eslint 项目生成工具

//全局安装一下
npm install -g yo generator-eslint

//创建个文件夹
mkdir eslint-plugin
cd eslint-plugin

//创建项目结构
yo eslint:plugin

//按照需求填写即可
? What is your name? @oec-pearl-kits/eslint-plugin
? What is the plugin ID? import
? Type a short description of this plugin: bytedance 国际化电商自定义插件
? Does this plugin contain custom ESLint rules? Yes
? Does this plugin contain one or more processors? YES(是否需要处理除js以外的文件例如.vue)
   create package.json
   create lib/index.js
   create README.md

这里有个坑需要特别注意,不然到时候再引入eslint的时候,会导致找不到插件或者找不到规则。即:plugin的命名, 我这里的命名是@oec-pearl-kits/eslint-plugin, 意思@oec-pearl-kits这个scope下的eslint插件。在命名的时候,建议采用@xxxx/eslint-plugin,这个命名规则在官方的文档也没有找到规范,确实是一个坑!

上面的代码主要是创建了 eslint 的项目模板,现在我们来创建 rule 相关的文件:

yo eslint:rule

//按照需求填写即可
? What is your name? pearl-import-validate
? Where will this rule be published? ESLint Plugin
? What is the rule ID? pearl-import-validate(规则的名称)
? Type a short description of this rule: 规范项目的导入依赖
? Type a short example of the code that will fail: 

//上面的步骤生成了 rule 对应的 说明文件、模板和单元测试文件,现在安装一下依赖
npm i

生成代码结构如下:

├── README.md
├── docs // 使用文档
│   └── rules
│       └── pearl-import-validate.md
├── lib // eslint 规则开发
│   ├── index.js
│   └── rules // 此目录下可以构建多个规则
│       └── pearl-import-validate.js
├── package.json
└── tests // 单元测试
    └── lib
        └── rules
            └── pearl-import-validate.js

Eslint运行原理

  1. 代码解析: eslint 通过解释器把我们写的代码转换成对应的 AST 抽象语法树,不同的语法需要用不同的解析器来解析,如Vue、TSX

  2. 深度遍历AST,监听匹配过程: 解析完 AST 后,eslint 会以 从上层到下层、从下层到上层的方式,遍历选择器两次

  3. 触发监听选择器rule回调: 在深度遍历的过程中,生效的每条规则(自己写的规则)都会对其中的某一个或多个选择器(ast树中的选择器)进行监听,每当匹配到选择器,监听该选择器的rule,都会触发对应的回调。

开发一个规则

前期准备

{
 "type": "Program",
 "start": 0,
 "end": 34,
 "body": [
   {
     "type": "ImportDeclaration",
     "start": 0,
     "end": 34,
     "specifiers": [
       {
         "type": "ImportDefaultSpecifier",
         "start": 7,
         "end": 12,
         "local": {
           "type": "Identifier",
           "start": 7,
           "end": 12,
           "name": "React"
         }
       },
       {
         "type": "ImportSpecifier",
         "start": 16,
         "end": 18,
         "imported": {
           "type": "Identifier",
           "start": 16,
           "end": 18,
           "name": "FC"
         },
         "local": {
           "type": "Identifier",
           "start": 16,
           "end": 18,
           "name": "FC"
         }
       }
     ],
     "source": {
       "type": "Literal",
       "start": 26,
       "end": 33,
       "value": "react",
       "raw": "'react'"
     }
   }
 ],
 "sourceType": "module"
}

根据AST树,对于依赖相关的,只需要在 ImportDeclaration 进行处理就好了。

  1. 先写个简单的例子

rule 代码

// lib/rules/pearl-import-validate.js
module.exports = {
  meta: {
    type: 'suggestion', // `problem`, `suggestion`, or `layout`
    docs: {
      description: "规范项目的导入依赖",
      recommended: false,
      url: null, // URL to the documentation page for this rule
    },
    // 是否自动格式化
    fixable: 'code',
    // 获取配置文件传递的属性
    schema: [], // Add a schema if the rule has options
    messages: {
      // 提示语
      avoidImportPackage: "Avoid using import named '{{ sourcePackage }}', advise to use import named '{{ finalPackage }}'",
    }
  },

  create(context) {
    return {
      ImportDeclaration(node) {
        if(node.source.value.includes('bad-import-declaration')) {
          context.report({
            node,
            // 指定提示语
            messageId: 'avoidImportPackage',
            data:{
              // 提供给上面的messages
              sourcePackage: 'bad-import-declaration',
              finalPackage: 'good-import-declaration'
            },
            // 自动fix
            fix: fixer => fixer.replaceText(node, `import somepackage from 'good-import-declaration';`)
          });
        }
      },
    };
  },
};

单元测试

// tests/lib/rules/pearl-import-validate.js
const rule = require("../../../lib/rules/pearl-import-validate"),
  RuleTester = require("eslint").RuleTester;

var config = {
  parserOptions: {
    ecmaVersion: 2020,
    sourceType: 'module',
  }
}

const ruleTester = new RuleTester(config);
ruleTester.run("pearl-import-validate", rule, {
  valid: [
    {
      code: "import somepackage from 'good-import-declaration';",
    },
  ],

  invalid: [
    {
      code: "import somepackage from 'bad-import-declaration';",
      errors: [
        {
          messageId: 'avoidImportPackage'
        }
      ],
      output: "import somepackage from 'good-import-declaration';",
    },
  ],
});

让我们一步一步来看看这里的所有东西:

  • create 方法将接受一个上下文参数,该参数稍后将用于创建有关已发现问题的报告。

  • 此方法将返回一个新的 ImportDeclaration 规则。 如果您对其他规则感兴趣,请查看官方文档

  • 我们正在检查某个节点导入是否包含查询(在我们的例子中是 bad-import-declaration)

  • 在此 if 语句中,我们通过使用以下参数从上下文对象调用方法来生成新报告:

    • node:触发规则的实际节点(类似于堆栈跟踪)位置。
    • message:运行规则并发现问题后应显示的消息。
    • 将用于修复导入语句的修复方法。 在这种情况下,它是一种使用修复器作为参数的方法,然后使用当前节点和应该添加的新值而不是节点来调用称为 replaceText 的修复器方法。

集成项目

  • 发布npm 包

这里参考文档

  • 项目集成

    npm install @oec-pearl-kits/eslint-plugin -D
    
    // .eslintrc.js
    module.exports = {
    root: true,
    extends: ['@jupiter-app'],
    parserOptions: {
        tsconfigRootDir: __dirname,
        project: ['../tsconfig.json'],
    },
    plugins: ['@oec-pearl-kits'],
    rules: {
        '@oec-pearl-kits/pearl-import-validate': 'error'
    },
    };
    
    

本地调试

现在已经开发完了,如果每次都发版本校验会很麻烦,可以使用 npm link 命令。它会把包依赖映射到本地,从而避免等待远端的最新代码。

  • cd 进入 eslint-plugin 目录,执行 npm link。

  • cd 进入 引入插件的项目目录,执行 npm link @oec-pearl-kits/eslint-plugin

  • 当 eslint-plugin 有修改后,重新在eslint-plugin中执行npm link。然后在实际引入的项目中关闭编辑器重新打开,插件即生效

获取配置参数

  1. 配置文件传参

    context 可以通过配合schema获取配置文件传递的参数 adviseForbidPackages :

    module.exports = {
    root: true,
    extends: ['@jupiter-app'],
    parserOptions: {
        tsconfigRootDir: __dirname,
        project: ['../tsconfig.json'],
    },
    plugins: ['@oec-pearl-kits'],
    rules: {
        'react-hooks/exhaustive-deps': [
        'warn',
        {
            additionalHooks: 'useRecoilCallback',
        },
        ],
        '@oec-pearl-kits/pearl-import-validate': [
        'error',
        {
            adviseForbidPackages: {
            'query-string': 'qs',
            lodash: 'lodash-es',
            },
        },
        ]
    },
    };
    
    

    rule中Create获取 adviseForbidPackages :

    create(context) {
        const { adviseForbidPackages } = context.options[0]
        return {
        ImportDeclaration(node) {
            if(node.source.value in adviseForbidPackages) {
            context.report({
                node,
                // 指定提示语
                messageId: 'avoidImportPackage',
                data:{
                // 提供给上面的messages
                sourcePackage: 'bad-import-declaration',
                finalPackage: 'good-import-declaration'
                },
                // 自动fix
                fix: fixer => fixer.replaceText(node, `import somepackage from 'good-import-declaration';`)
            });
            }
        },
        };
    },
    

    schema配置:

        schema: [{
            type: 'object',
            properties: {
                adviseForbidPackages: {
                type:"object"
                }
            }
        }]
    
  2. 单元测试适配配置参数

    const rule = require("../../../lib/rules/pearl-import-validate")
    const RuleTester = require("eslint").RuleTester;
    
    var config = {
        parserOptions: {
            ecmaVersion: 2020,
            sourceType: 'module',
        }
    }
    
    const option =  {
        adviseForbidPackages: {
        
        },
    }
    
    const ruleTester = new RuleTester(config);
    
    ruleTester.run("pearl-import-validate",rule,{
        valid: [
            {
                code: "import somepackage from 'good-import-declaration';",
                options:[option],
            },
        ],
    
        invalid: [
            {
                code: "import somepackage from 'bad-import-declaration';",
                errors: [
                    {
                        messageId: 'avoidImportPackage'
                    }
                ],
                output: "import somepackage from 'good-import-declaration';",
            },
        ],
    });
    
    

参考文档