Intentaré escribir un tutorial basado en mi PR en el repositorio de Reatom con una explicación paso a paso: https://github.com/artalar/Reatom/pull/488
Si quieres saber más, puedes leer el problema https://github.com/artalar/reatom/issues/487.
Para agregar un poco de contexto, Reatom es una biblioteca de administración de estado. Los átomos son un concepto en Reatom, una biblioteca de administración de estado para React.
Los complementos de ESLint son extensiones que funcionan con el paquete principal de ESLint para aplicar estándares de codificación específicos. Los complementos contienen una carpeta rules
, que define reglas individuales para hacer cumplir estos estándares.
Cada módulo rule
tiene una propiedad meta
que describe la regla y una propiedad create
que define el comportamiento de la regla .
La función create
toma un argumento context
, que se usa para interactuar con el código que se está verificando, y puede usarlo para definir la lógica de su regla, como requerir convenciones de nomenclatura estrictas para su biblioteca.
Creando un nuevo proyecto TypeScript eslint
npx degit https://github.com/pivaszbs/typescript-template-eslint-plugin reatom-eslint-plugin
Luego, navegue al nuevo directorio del proyecto e instale las dependencias con:
cd reatom-eslint-plugin && npm i
Quiero ser un buen chico, así que inicio git.
git init && git add . && git commit -m "init"
A continuación, abra el archivo package.json
y localice el campo name
. Este campo es esencial porque será el punto de entrada principal para su complemento cuando se use. Puedes cambiarlo por lo siguiente:
"name": "eslint-plugin-reatom"
Alternativamente, puede usar la convención de nomenclatura de paquetes con ámbito:
"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
En el índice general, los archivos se generarán mediante secuencias de comandos, por lo que no debe preocuparse por eso 😀
/* 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 };
En este repositorio, encontrará algunos scripts convenientes para agregar reglas y actualizar documentos. Para agregar una nueva regla, puede usar el siguiente comando:
npm run add-rule atom-rule suggestion
Esto generará tres secciones para la nueva regla: documentación, pruebas y código real. Podemos omitir la sección de documentación por ahora y centrarnos en los dos últimos.
Como entusiasta de TDD (desarrollo basado en pruebas), comenzaremos creando algunas pruebas simples en el archivo 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'}] }, ] });
Si ejecuta las pruebas ahora, fallarán porque aún no hemos implementado atomRule
.
atomRule
es donde definimos el comportamiento de la regla. Aquí hay una implementación simple:
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;
Es una variante simple, pero aquí podemos entender fácilmente lo que está pasando.
Para una mejor comprensión de la estructura AST de su código, puede usar https://astexplorer.net/ o simplemente nodos analizados de console.log.
Aquí hay una pequeña descripción de cada identificador en un pequeño ejemplo:
const kek = atom('kek')
Identifier
: una interfaz TypeScript que representa un nodo identificador en un AST.
const kek = atom ('kek'), kek y atom son nodos de identificadores.
Literal
: una interfaz de TypeScript que representa un nodo de valor literal (cadena, número, booleano, etc.) en un AST. const kek = atom(' kek '), 'kek' es un literal.
CallExpression
: una interfaz de TypeScript que representa un nodo de expresión de llamada de función en un árbol de sintaxis abstracta (AST).
En nuestro ejemplo, atom('kek') es una CallExpression, que consta de atom - Identificador y kek - Literal.
VariableDeclarator
: una interfaz TypeScript que representa un nodo declarador de variables en un AST
En nuestro ejemplo, la expresión completa excepto const es VariableDeclarator kek = atom('kek')
Node
: una interfaz TypeScript que representa un nodo AST genérico.
O simplemente usando astexplorer
Las pruebas finales
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"); `, }, ] });
De las pruebas, entendemos que necesitamos cambiar de alguna manera el código fuente usando nuestra regla.
Agregue una línea simple al informe de contexto.
fix: fixer => fixer.replaceText(node, replaceString)
nodo : puede ser un nodo real o un rango de símbolos que desea reemplazar.
replaceString : qué código espera ver.
No olvide agregar reparable : 'código' o reparable : 'espacio en blanco' para las etiquetas meta de su regla.
Si no está familiarizado con cómo solucionarlo con eslint, intente con su proyecto 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 puede ver, solo tiene mejores errores, protectores de tipo e incluye verificación de importación. Y, por supuesto, hago que la regla sea corregible.
Para actualizar los documentos, puede utilizar el siguiente comando:
npm run update
Este comando actualizará README.md y actualizará los documentos para cada regla (pero debe escribir un poco sobre cada regla en el archivo docs/{rule}).
Además, como dije, no necesita preocuparse por el archivo de índice.
Asegúrese de que la versión esté en su paquete.json.
"version": "1.0.0"
Escriba en término si no es 1.0.0.
npm version 1.0.0
Entonces solo escribe en la raíz.
npm publish
Todo se compilará y publicará con su nombre de paquete definido.
Nombro mi paquete.
@reatom/eslint-plugin
Entonces, necesito instalarlo.
npm i @reatom/eslint-plugin
Y agregar a mi configuración .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' } }
Y todo simplemente funciona (para solo reatom-eslint-plugin
debe escribir “reatom”
en lugar de “@reatom"
en todas partes).
En este tutorial, repasamos el proceso de creación de un complemento ESLint para la biblioteca de administración de estado de Reatom. Nosotros cubrimos:
Recursos para seguir aprendiendo y explorando
Divertirse :)