Vou tentar escrever um tutorial baseado no meu PR no repositório Reatom com uma explicação passo a passo: https://github.com/artalar/Reatom/pull/488
Se você quiser saber mais, pode ler a edição https://github.com/artalar/reatom/issues/487.
Para adicionar um pouco de contexto, Reatom é uma biblioteca de gerenciamento de estado. Atoms são um conceito no Reatom, uma biblioteca de gerenciamento de estado para React.
Os plug-ins ESLint são extensões que funcionam com o pacote ESLint principal para impor padrões de codificação específicos. Os plug-ins contêm uma pasta rules
, que define regras individuais para aplicar esses padrões.
Cada módulo rule
possui uma propriedade meta
que descreve a regra e uma propriedade create
que define o comportamento da regra .
A função create
recebe um argumento context
, que é usado para interagir com o código que está sendo verificado, e você pode usá-lo para definir a lógica de sua regra, como exigir convenções de nomenclatura estritas para sua biblioteca.
Criando um novo projeto eslint TypeScript
npx degit https://github.com/pivaszbs/typescript-template-eslint-plugin reatom-eslint-plugin
Em seguida, navegue até o novo diretório do projeto e instale as dependências com:
cd reatom-eslint-plugin && npm i
Eu quero ser um bom menino, então eu init git.
git init && git add . && git commit -m "init"
Em seguida, abra o arquivo package.json
e localize o campo name
. Este campo é essencial porque será o ponto de entrada principal para o seu plugin quando for usado. Você pode alterá-lo para o seguinte:
"name": "eslint-plugin-reatom"
Como alternativa, você pode usar a convenção de nomenclatura de pacote com escopo:
"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
No índice geral, os arquivos serão gerados por scripts, então você não precisa se preocupar com isso 😀
/* 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 };
Neste repositório, você encontrará alguns scripts convenientes para adicionar regras e atualizar documentos. Para adicionar uma nova regra, você pode usar o seguinte comando:
npm run add-rule atom-rule suggestion
Isso gerará três seções para a nova regra: documentação, testes e código real. Podemos pular a seção de documentação por enquanto e focar nas duas últimas.
Como entusiasta de TDD (desenvolvimento orientado a testes), começaremos criando alguns testes simples no arquivo 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'}] }, ] });
Se você executar os testes agora, eles falharão porque ainda não implementamos o atomRule
.
A atomRule
é onde definimos o comportamento da regra. Aqui está uma implementação simples:
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;
É uma variante simples, mas aqui podemos entender facilmente o que está acontecendo.
Para uma melhor compreensão da estrutura AST do seu código, você pode usar https://astexplorer.net/ ou simplesmente nós parsed console.log.
Aqui está uma pequena descrição de cada identificador em um pequeno exemplo:
const kek = atom('kek')
Identifier
: uma interface TypeScript que representa um nó identificador em um AST.
const kek = atom ('kek'), kek e atom são nós identificadores.
Literal
: uma interface TypeScript que representa um nó de valor literal (string, número, booleano etc.) em um AST. const kek = atom(' kek '), 'kek' é um Literal.
CallExpression
: uma interface TypeScript que representa um nó de expressão de chamada de função em uma árvore de sintaxe abstrata (AST).
Em nosso exemplo, atom('kek') é uma CallExpression, que consiste em atom - Identifier e kek - Literal.
VariableDeclarator
: uma interface TypeScript que representa um nó declarator de variável em um AST
Em nosso exemplo, toda a expressão exceto const é VariableDeclarator kek = atom('kek')
Node
: uma interface TypeScript que representa um nó AST genérico.
Ou simplesmente usando o astexplorer
os testes finais
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"); `, }, ] });
A partir dos testes, entendemos que precisamos de alguma forma alterar o código-fonte usando nossa regra.
Adicione uma linha simples ao relatório de contexto.
fix: fixer => fixer.replaceText(node, replaceString)
nó - pode ser um nó real ou um intervalo de símbolos que você deseja substituir.
replaceString - qual código você espera ver.
Não se esqueça de adicionar fixable : 'code' ou fixable : 'whitespace' para suas meta tags de regra.
Se você não estiver familiarizado com como corrigi-lo com o eslint, tente em seu projeto existente.
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}"`) }); }
Como você pode ver, ele só tem erros melhores, guardas de tipo e inclui verificação de importação. E, claro, eu torno a regra corrigível.
Para atualizar os documentos, você pode usar o seguinte comando:
npm run update
Este comando irá atualizar o README.md e atualizar os documentos para cada regra (mas você precisa escrever um pouco sobre cada regra no arquivo docs/{rule}).
Além disso, como eu disse, você não precisa se preocupar com o arquivo de índice.
Certifique-se de que a versão esteja em seu package.json.
"version": "1.0.0"
Escreva em termos se não for 1.0.0.
npm version 1.0.0
Depois é só escrever na raiz.
npm publish
Tudo será construído e publicado com o nome do pacote definido.
Eu nomeio meu pacote.
@reatom/eslint-plugin
Então, eu preciso instalá-lo.
npm i @reatom/eslint-plugin
E adicione à minha configuração .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' } }
E tudo simplesmente funciona (para apenas reatom-eslint-plugin
você deve escrever “reatom”
em vez de “@reatom"
em todos os lugares).
Neste tutorial, percorremos o processo de criação de um plug-in ESLint para a biblioteca de gerenciamento de estado Reatom. Cobrimos:
Recursos para aprendizado e exploração adicionais
Divirta-se :)