目录 使用模板初始化 repo 模板的初始结构 使用模板中的脚本添加规则 为 Eslint 插件编写测试 编写 Eslint 规则 小AST解释 最终变体 使用脚本更新文档 插件发布 将它与您的应用程序连接起来 背景 我将尝试根据我在 Reatom 存储库中的 PR 编写一个教程,并提供逐步解释:https: //github.com/artalar/Reatom/pull/488 如果想了解更多,可以阅读issue https://github.com/artalar/reatom/issues/487。 补充一点上下文,Reatom 是一个状态管理库。原子是 Reatom 中的一个概念,Reatom 是 React 的状态管理库。 什么是 ESLint 插件和规则? ESLint 插件是与核心 ESLint 包一起使用以执行特定编码标准的扩展。插件包含一个 文件夹,它定义了执行这些标准的各个规则。 rules 每个 模块都有一个描述 属性和一个定义 行为的 属性。 rule 规则的 meta 规则 create 函数采用 参数,用于与被检查的代码交互,您可以使用它来定义规则的逻辑,例如要求库的严格命名约定。 create context 让我们深入研究代码 初始化回购 创建一个新的 TypeScript eslint 项目 npx degit https://github.com/pivaszbs/typescript-template-eslint-plugin reatom-eslint-plugin 然后,导航到新的项目目录,并安装依赖项: cd reatom-eslint-plugin && npm i 我想做一个好孩子,所以我初始化了 git。 git init && git add . && git commit -m "init" 接下来,打开 文件,找到 字段。这个字段是必不可少的,因为它将是您的插件在使用时的主要入口点。您可以将其更改为以下内容: package.json name "name": "eslint-plugin-reatom" 或者,您可以使用范围包命名约定: "name": "@reatom/eslint-plugin" 初始结构 - scripts // some automation to concentrate on writing rules - docs - rules // here will be generated by npm run add-rule files - src - configs recommended.ts // generated config - rules // all your rules index.ts // Connection point to your plugin, autogenerated by scripts/lib/update-lib-index.ts 😀 一般索引,文件都会由脚本生成,所以不用担心 /* DON'T EDIT THIS FILE. This is generated by 'scripts/lib/update-lib-index.ts' */ import { recommended } from './configs/recommended'; import exampleRule from './rules/example-rule' export const configs = { recommended }; export const rules = { 'example-rule': exampleRule }; 添加规则和更新文档 在此存储库中,您会找到一些方便的脚本来添加规则和更新文档。要添加新规则,您可以使用以下命令: npm run add-rule atom-rule suggestion 这将为新规则生成三个部分:文档、测试和实际代码。我们现在可以跳过文档部分,专注于最后两个部分。 编写测试 作为 TDD(测试驱动开发)爱好者,我们将从在 文件中创建一些简单的测试开始: tests/atom-rule.ts // tests/atom-rule.ts tester.run('atom-rule', atomRule, { valid: [ { code: 'const countAtom = atom(0, "countAtom");' }, ], invalid: [ { code: `const countAtom = atom(0);`, errors: [{ message: 'atom name is not defined' }] }, { code: 'const countAtom = atom(0, "count");', errors: [{ message: 'atom name is defined bad'}] }, ] }); 如果您现在运行测试,它们将会失败,因为我们还没有实现 。 atomRule 编写规则 是我们定义规则行为的地方。这是一个简单的实现: atomRule import { Rule } from "eslint"; const rule: Rule.RuleModule = { meta: { docs: { description: "Add name for every atom call", // simply describe your rule recommended: true, // if it's recommended, then npm run update will add it to recommmended config }, type: "suggestion" }, create: function (context: Rule.RuleContext): Rule.RuleListener { return { VariableDeclaration: node => { // listener for declaration, here we can specifiy more specific selector node.declarations.forEach(d => { if (d.init?.type !== 'CallExpression') return; if (d.init.callee.type !== 'Identifier') return; if (d.init.callee.name !== 'atom') return; if (d.id.type !== 'Identifier') return; // just guard everything that we don't need if (d.init.arguments.length <= 1) { // show error in user code context.report({ message: `atom name is not defined`, // here we can pass what will be underlined by red/yellow line node, }) } if (d.init.arguments[1]?.type !== 'Literal') return; // just another guard if (d.init.arguments[1].value !== d.id.name) { context.report({ message: `atom name is defined bad`, node }) } }) } }; }, }; export default rule; 这是一个简单的变体,但在这里,我们可以很容易地理解发生了什么。 为了更好地理解代码的 AST 结构,您可以使用 或简单地使用 console.log 解析节点。 https://astexplorer.net/ 更好理解 AST 类型的一个小解释 这是一个小示例中每个标识符的简短描述: const kek = atom('kek') :表示 AST 中标识符节点的 TypeScript 接口。 Identifier const = ('kek'), 和 是标识符节点。 kek atom kek atom :表示 AST 中的文字值(字符串、数字、布尔值等)节点的 TypeScript 接口。 const kek = atom(' '), 'kek' 是一个 Literal kek 字面值。 :一个 TypeScript 接口,表示抽象语法树 (AST) 中的函数调用表达式节点。 CallExpression 在我们的示例中, 是一个 CallExpression,它由 atom('kek') atom - Identifier 和 kek - Literal 组成。 :表示 AST 中的变量声明符节点的 TypeScript 接口 VariableDeclarator 在我们的示例中,除了 const 之外的整个表达式是 VariableDeclarator kek = atom('kek') :表示通用 AST 节点的 TypeScript 接口。 Node 或者简单地使用 astexplorer https://astexplorer.net/?embedable=true#/gist/7fe145026f1b15adefeb307427210d38/35f114eb5b9c4d3cb626e76aa6af7782927315ed 最终变体 最后的测试 tester.run('atom-rule', rule, { valid: [ { code: ` import { atom } from '@reatom/framework' const countAtom = atom(0, "countAtom"); ` }, { code: `const countAtom = atom(0);`, }, { code: 'const countAtom = atom(0, "count");', }, ], invalid: [ { code: ` import { atom } from '@reatom/framework' const countAtom = atom(0); `, errors: [{ message: 'atom "countAtom" should has a name inside atom() call', }], output: ` import { atom } from '@reatom/framework' const countAtom = atom(0, "countAtom"); `, }, { code: ` import { atom } from '@reatom/framework' const countAtom = atom(0, "count"); `, errors: [{ message: `atom "countAtom" should be named as it's variable name, rename it to "countAtom"` }], output: ` import { atom } from '@reatom/framework' const countAtom = atom(0, "countAtom"); `, }, ] }); 从测试中,我们了解到我们需要使用我们的规则以某种方式更改源代码。 如何使您的规则可修复? 在上下文报告中添加一行简单的代码。 fix: fixer => fixer.replaceText(node, replaceString) - 可能是您要替换的实际节点或符号范围。 节点 - 您希望看到的代码。 replaceString 不要忘记为您的规则元标记添加 : 'code' 或 : 'whitespace' 。 fixable fixable 如果您不熟悉如何使用 eslint 修复它,请尝试您现有的项目。 eslint --fix ./src 代码本身 import { Rule } from "eslint"; import { CallExpression, Identifier, Literal, VariableDeclarator, Node } from 'estree'; import { isIdentifier, isLiteral } from "../lib"; type AtomCallExpression = CallExpression & { callee: Identifier, arguments: [Literal] | [Literal, Literal] } type AtomVariableDeclarator = VariableDeclarator & { id: Identifier, init: AtomCallExpression } const noname = (atomName: string) => `atom "${atomName}" should has a name inside atom() call`; const invalidName = (atomName: string) => `atom "${atomName}" should be named as it's variable name, rename it to "${atomName}"`; export const atomRule: Rule.RuleModule = { meta: { type: 'suggestion', docs: { recommended: true, description: "Add name for every atom call" }, fixable: 'code' }, create: function (context: Rule.RuleContext): Rule.RuleListener { let importedFromReatom = false; return { ImportSpecifier(node) { const imported = node.imported.name; // @ts-ignore const from = node.parent.source.value; if (from.startsWith('@reatom') && imported === 'atom') { importedFromReatom = true; } }, VariableDeclarator: d => { if (!isAtomVariableDeclarator(d) || !importedFromReatom) return; if (d.init.arguments.length === 1) { reportUndefinedAtomName(context, d); } else if (isLiteral(d.init.arguments[1]) && d.init.arguments[1].value !== d.id.name) { reportBadAtomName(context, d); } } }; } } function isAtomCallExpression(node?: Node | null): node is AtomCallExpression { return node?.type === 'CallExpression' && node.callee?.type === 'Identifier' && node.callee.name === 'atom'; } function isAtomVariableDeclarator(node: VariableDeclarator): node is AtomVariableDeclarator { return isAtomCallExpression(node.init) && isIdentifier(node.id); } function reportUndefinedAtomName(context: Rule.RuleContext, d: AtomVariableDeclarator) { context.report({ message: noname(d.id.name), node: d, fix: fixer => fixer.insertTextAfter(d.init.arguments[0], `, "${d.id.name}"`) }); } function reportBadAtomName(context: Rule.RuleContext, d: AtomVariableDeclarator) { context.report({ message: invalidName(d.id.name), node: d, fix: fixer => fixer.replaceText(d.init.arguments[1], `"${d.id.name}"`) }); } 如您所见,它只是有更好的错误、类型保护,并包括导入检查。而且,当然,我使规则可以修复。 更新文档 要更新文档,您可以使用以下命令: npm run update 此命令将更新 README.md 并更新每个规则的文档(但您需要在 docs/{rule} 文件中写一些关于每个规则的信息)。 另外,正如我所说,您不必担心索引文件。 发布步骤 确保版本在您的 package.json 中。 "version": "1.0.0" 如果不是 1.0.0,请写在 term 中。 npm version 1.0.0 然后只写在根目录中。 npm publish 一切都将使用您定义的包名称构建和发布。 将它与您的应用程序连接起来 我给我的包裹命名。 @reatom/eslint-plugin 所以,我需要安装它。 npm i @reatom/eslint-plugin 并添加到我的 .eslintrc 配置中。 module.exports = { plugins: [ "@reatom" ], // use all rules extends: [ "plugin:@reatom/recommended" ], // or pick some rules: { '@reatom/atom-rule': 'error', // aditional rules, you can see it in PR '@reatom/action-rule': 'error', '@reatom/reatom-prefix-rule': 'error' } } 一切正常(对于 你应该到处写 而不是 )。 reatom-eslint-plugin “reatom” “@reatom" 结论 在本教程中,我们介绍了为 Reatom 状态管理库创建 ESLint 插件的过程。我们涵盖: 如何在 Typescript 中编写 eslint 插件。 如何用测试覆盖它。 如何使其与 --fix 选项一起使用。 如何使用我的模板。 如何发布 eslint 插件。 如何使用 eslint 将其添加到现有存储库 进一步学习和探索的资源 https://github.com/pivaszbs/typescript-template-eslint-plugin https://astexplorer.net/ https://github.com/artalar/reatom/pull/488/files https://eslint.org/docs/latest/extend/plugins https://www.reatom.dev/ https://github.com/artalar/reatom https://docs.npmjs.com/about-semantic-versioning 玩得开心 :)