paint-brush
如何使用模板、测试和发布在 Typescript 中创建 ESLint 插件经过@antonkrylov322
1,879 讀數
1,879 讀數

如何使用模板、测试和发布在 Typescript 中创建 ESLint 插件

经过 Anton Krylov13m2023/03/20
Read on Terminal Reader

太長; 讀書

使用带测试和模板的打字稿自己编写 eslint 插件。您将更多地了解 AST 以及如何在 typescript 中处理它(是的,如果您很了解它就很容易)。您也可以通过我的 PR 和提交历史来了解本文的思想历史:)
featured image - 如何使用模板、测试和发布在 Typescript 中创建 ESLint 插件
Anton Krylov HackerNoon profile picture
0-item

目录

  1. 使用模板初始化 repo
  2. 模板的初始结构
  3. 使用模板中的脚本添加规则
  4. 为 Eslint 插件编写测试
  5. 编写 Eslint 规则
  6. 小AST解释
  7. 最终变体
  8. 使用脚本更新文档
  9. 插件发布
  10. 将它与您的应用程序连接起来

背景

我将尝试根据我在 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 结构,您可以使用https://astexplorer.net/或简单地使用 console.log 解析节点。

更好理解 AST 类型的一个小解释

这是一个小示例中每个标识符的简短描述:

const kek = atom('kek')


  1. Identifier :表示 AST 中标识符节点的 TypeScript 接口。

    1. const kek = atom ('kek'), kekatom是标识符节点。


  2. Literal :表示 AST 中的文字值(字符串、数字、布尔值等)节点的 TypeScript 接口。 const kek = atom(' kek '), 'kek' 是一个字面值。


  3. CallExpression :一个 TypeScript 接口,表示抽象语法树 (AST) 中的函数调用表达式节点。


    1. 在我们的示例中, atom('kek')是一个 CallExpression,它由atom - Identifier 和 kek - Literal 组成。


  4. VariableDeclarator :表示 AST 中的变量声明符节点的 TypeScript 接口


    1. 在我们的示例中,除了 const 之外的整个表达式是 VariableDeclarator kek = atom('kek')


  5. Node :表示通用 AST 节点的 TypeScript 接口。


或者简单地使用 astexplorer

最终变体

最后的测试

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 - 您希望看到的代码。


不要忘记为您的规则元标记添加fixable : 'code' 或fixable : 'whitespace' 。


如果您不熟悉如何使用 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 插件的过程。我们涵盖:


  1. 如何在 Typescript 中编写 eslint 插件。
  2. 如何用测试覆盖它。
  3. 如何使其与 --fix 选项一起使用。
  4. 如何使用我的模板。
  5. 如何发布 eslint 插件。
  6. 如何使用 eslint 将其添加到现有存储库


进一步学习和探索的资源

  1. https://github.com/pivaszbs/typescript-template-eslint-plugin
  2. https://astexplorer.net/
  3. https://github.com/artalar/reatom/pull/488/files
  4. https://eslint.org/docs/latest/extend/plugins
  5. https://www.reatom.dev/
  6. https://github.com/artalar/reatom
  7. https://docs.npmjs.com/about-semantic-versioning


玩得开心 :)