Tabla de contenido Inicializar repositorio usando una plantilla Estructura inicial de una plantilla Agregar reglas usando scripts de una plantilla Escribir pruebas para el complemento Eslint Escribir la regla de Eslint Pequeña explicación de AST última variante Actualización de documentos mediante scripts Publicación de complementos Conéctelo con su aplicación Fondo 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. ¿Qué son los complementos y las reglas de ESLint? 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 , que define reglas individuales para hacer cumplir estos estándares. rules Cada módulo tiene una propiedad que describe la y una propiedad que define el comportamiento . rule meta regla create de la regla La función toma un argumento , 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. create context Sumerjámonos en el código Inicializar repositorio 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 y localice el campo . Este campo es esencial porque será el punto de entrada principal para su complemento cuando se use. Puedes cambiarlo por lo siguiente: package.json name "name": "eslint-plugin-reatom" Alternativamente, puede usar la convención de nomenclatura de paquetes con ámbito: "name": "@reatom/eslint-plugin" Estructura 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 😀 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 }; Agregar reglas y actualizar documentos 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. escribir pruebas 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 Escribir la regla es donde definimos el comportamiento de la regla. Aquí hay una implementación simple: 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; 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 o simplemente nodos analizados de console.log. https://astexplorer.net/ Una pequeña explicación para escribir AST Mejor comprensión Aquí hay una pequeña descripción de cada identificador en un pequeño ejemplo: const kek = atom('kek') : una interfaz TypeScript que representa un nodo identificador en un AST. Identifier const = ('kek'), y son nodos de identificadores. kek atom kek atom : una interfaz de TypeScript que representa un nodo de valor literal (cadena, número, booleano, etc.) en un AST. const kek = atom(' '), 'kek' es un Literal kek literal. : una interfaz de TypeScript que representa un nodo de expresión de llamada de función en un árbol de sintaxis abstracta (AST). CallExpression En nuestro ejemplo, es una CallExpression, que consta de atom('kek') atom - Identificador y kek - Literal. : una interfaz TypeScript que representa un nodo declarador de variables en un AST VariableDeclarator En nuestro ejemplo, la expresión completa excepto const es VariableDeclarator kek = atom('kek') : una interfaz TypeScript que representa un nodo AST genérico. Node O simplemente usando astexplorer https://astexplorer.net/?embedable=true#/gist/7fe145026f1b15adefeb307427210d38/35f114eb5b9c4d3cb626e76aa6af7782927315ed Variante final 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. ¿Cómo hacer que su regla sea reparable? Agregue una línea simple al informe de contexto. fix: fixer => fixer.replaceText(node, replaceString) : puede ser un nodo real o un rango de símbolos que desea reemplazar. nodo : qué código espera ver. replaceString No olvide agregar : 'código' o : 'espacio en blanco' para las etiquetas meta de su regla. reparable reparable Si no está familiarizado con cómo solucionarlo con eslint, intente con su proyecto existente. eslint --fix ./src Código mismo 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. Actualización de los documentos 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. Paso de publicación 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. Conéctelo con su aplicación 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 debe escribir en lugar de en todas partes). reatom-eslint-plugin “reatom” “@reatom" Conclusión 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: Cómo escribir un complemento de eslint en Typescript. Cómo cubrirlo con pruebas. Cómo hacer que funcione con la opción --fix. Cómo usar mi plantilla. Cómo publicar el complemento eslint. Cómo agregarlo a su repositorio existente con eslint Recursos para seguir aprendiendo y explorando https://github.com/pivaszbs/typescript-template-eslint-plugin https://astexplorer.net/ https://github.com/artalar/reatom/pull/488/files https://eslint.org/docs/latest/extend/plugins https://www.reatom.dev/ https://github.com/artalar/reatom https://docs.npmjs.com/about-semantic-versioning Divertirse :)