我将尝试根据我在 Reatom 存储库中的 PR 编写一个教程,并提供逐步解释:https: //github.com/artalar/Reatom/pull/488
如果想了解更多,可以阅读issue https://github.com/artalar/reatom/issues/487。
补充一点上下文,Reatom 是一个状态管理库。原子是 Reatom 中的一个概念,Reatom 是 React 的状态管理库。
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 解析节点。
这是一个小示例中每个标识符的简短描述:
const kek = atom('kek')
Identifier
:表示 AST 中标识符节点的 TypeScript 接口。
const kek = atom ('kek'), kek和atom是标识符节点。
Literal
:表示 AST 中的文字值(字符串、数字、布尔值等)节点的 TypeScript 接口。 const kek = atom(' kek '), 'kek' 是一个字面值。
CallExpression
:一个 TypeScript 接口,表示抽象语法树 (AST) 中的函数调用表达式节点。
在我们的示例中, atom('kek')是一个 CallExpression,它由atom - Identifier 和 kek - Literal 组成。
VariableDeclarator
:表示 AST 中的变量声明符节点的 TypeScript 接口
在我们的示例中,除了 const 之外的整个表达式是 VariableDeclarator kek = atom('kek')
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 插件的过程。我们涵盖:
进一步学习和探索的资源
玩得开心 :)