paint-brush
Como criar seu plug-in ESLint no TypeScript com um modelo, testes e publicaçãopor@antonkrylov322
1,940 leituras
1,940 leituras

Como criar seu plug-in ESLint no TypeScript com um modelo, testes e publicação

por Anton Krylov13m2023/03/20
Read on Terminal Reader

Muito longo; Para ler

Escreva o plug-in eslint sozinho usando o texto datilografado com testes e modelo. Você saberá mais sobre o AST e como lidar com ele no typecscript (sim, é fácil se você o conhecer bem). Além disso, você pode percorrer meu PR e histórico de commits para entender o histórico de pensamentos deste artigo :)
featured image - Como criar seu plug-in ESLint no TypeScript com um modelo, testes e publicação
Anton Krylov HackerNoon profile picture
0-item

Índice

  1. Inicializar repositório usando um modelo
  2. Estrutura inicial de um modelo
  3. Adicionando regras usando scripts de um modelo
  4. Escrever testes para o plug-in Eslint
  5. Escreva a regra de Eslint
  6. Pequena explicação AST
  7. variante final
  8. Atualizando documentos usando scripts
  9. Publicação de plugins
  10. Conecte-o com seu aplicativo

Fundo

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.

Quais são os plug-ins e regras do ESLint?

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.

Vamos mergulhar no código

Inicializar repositório

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"

Estrutura inicial

 - 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 };

Adicionando regras e atualizando documentos

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.

Escrever testes

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 .

Escrevendo a regra

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.

Uma pequena explicação para a digitação AST Melhor compreensão

Aqui está uma pequena descrição de cada identificador em um pequeno exemplo:

const kek = atom('kek')


  1. Identifier : uma interface TypeScript que representa um nó identificador em um AST.

    1. const kek = atom ('kek'), kek e atom são nós identificadores.


  2. 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.


  3. CallExpression : uma interface TypeScript que representa um nó de expressão de chamada de função em uma árvore de sintaxe abstrata (AST).


    1. Em nosso exemplo, atom('kek') é uma CallExpression, que consiste em atom - Identifier e kek - Literal.


  4. VariableDeclarator : uma interface TypeScript que representa um nó declarator de variável em um AST


    1. Em nosso exemplo, toda a expressão exceto const é VariableDeclarator kek = atom('kek')


  5. Node : uma interface TypeScript que representa um nó AST genérico.


Ou simplesmente usando o astexplorer

Variante Final

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.

Como tornar sua regra corrigível?

Adicione uma linha simples ao relatório de contexto.

 fix: fixer => fixer.replaceText(node, replaceString)


- 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

próprio código

 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.

Atualizando os documentos

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.

Etapa de Publicação

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.

Conecte-o com seu aplicativo

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).

Conclusão

Neste tutorial, percorremos o processo de criação de um plug-in ESLint para a biblioteca de gerenciamento de estado Reatom. Cobrimos:


  1. Como escrever um plugin eslint em Typescript.
  2. Como cobri-lo com testes.
  3. Como fazê-lo funcionar com a opção --fix.
  4. Como usar meu modelo.
  5. Como publicar o plugin eslint.
  6. Como adicioná-lo ao seu repositório existente com eslint


Recursos para aprendizado e exploração adicionais

  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


Divirta-se :)