Table des matières Initialiser le dépôt à l'aide d'un modèle Structure initiale d'un modèle Ajout de règles à l'aide de scripts à partir d'un modèle Ecrire des tests pour le plugin Eslint Écrire la règle d'Eslint Petite explication AST Variante finale Mise à jour des documents à l'aide de scripts Publication de plugins Connectez-le à votre application Arrière-plan Je vais essayer d'écrire un tutoriel basé sur mon PR dans le référentiel Reatom avec une explication étape par étape : https://github.com/artalar/Reatom/pull/488 Si vous voulez en savoir plus, vous pouvez lire le numéro https://github.com/artalar/reatom/issues/487. Pour ajouter un peu de contexte, Reatom est une bibliothèque de gestion d'état. Les atomes sont un concept dans Reatom, une bibliothèque de gestion d'état pour React. Que sont les plugins et les règles ESLint ? Les plugins ESLint sont des extensions qui fonctionnent avec le package principal ESLint pour appliquer des normes de codage spécifiques. Les plugins contiennent un dossier , qui définit des règles individuelles pour appliquer ces normes. rules Chaque module possède une propriété qui décrit la et une propriété qui définit le comportement . rule meta règle create de la règle La fonction prend un argument , qui est utilisé pour interagir avec le code en cours de vérification, et vous pouvez l'utiliser pour définir la logique de votre règle, comme exiger des conventions de nommage strictes pour votre bibliothèque. create context Plongeons dans le code Initialiser le dépôt Création d'un nouveau projet eslint TypeScript npx degit https://github.com/pivaszbs/typescript-template-eslint-plugin reatom-eslint-plugin Ensuite, accédez au nouveau répertoire du projet et installez les dépendances avec : cd reatom-eslint-plugin && npm i Je veux être un bon garçon, alors j'initie git. git init && git add . && git commit -m "init" Ensuite, ouvrez le fichier et localisez le champ . Ce champ est essentiel car il sera le principal point d'entrée de votre plugin lors de son utilisation. Vous pouvez le modifier comme suit : package.json name "name": "eslint-plugin-reatom" Vous pouvez également utiliser la convention d'attribution de noms de package étendu : "name": "@reatom/eslint-plugin" Structure initiale - 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 😀 Dans l'index général, les fichiers seront générés par des scripts, vous n'avez donc pas à vous en soucier /* 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 }; Ajout de règles et mise à jour de documents Dans ce référentiel, vous trouverez des scripts pratiques pour ajouter des règles et mettre à jour des documents. Pour ajouter une nouvelle règle, vous pouvez utiliser la commande suivante : npm run add-rule atom-rule suggestion Cela générera trois sections pour la nouvelle règle : documentation, tests et code réel. Nous pouvons ignorer la section de documentation pour l'instant et nous concentrer sur les deux derniers. Écrire des tests En tant que passionné de TDD (développement piloté par les tests), nous allons commencer par créer quelques tests simples dans le fichier : 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 vous exécutez les tests maintenant, ils échoueront car nous n'avons pas encore implémenté l' . atomRule Écrire la règle L' est l'endroit où nous définissons le comportement de la règle. Voici une implémentation 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; C'est une variante simple, mais ici, on comprend facilement ce qui se passe. Pour une meilleure compréhension de la structure AST de votre code, vous pouvez utiliser ou simplement console.log parsed nodes. https://astexplorer.net/ Une petite explication pour une meilleure compréhension des typages AST Voici une petite description de chaque identifiant dans un petit exemple : const kek = atom('kek') : une interface TypeScript qui représente un nœud d'identifiant dans un AST. Identifier const = ('kek'), et sont des nœuds identificateurs. kek atom kek atom : une interface TypeScript qui représente un nœud de valeur littérale (chaîne, nombre, booléen, etc.) dans un AST. const kek = atom(' '), 'kek' est un Literal kek Littéral. : une interface TypeScript qui représente un nœud d'expression d'appel de fonction dans un arbre de syntaxe abstraite (AST). CallExpression Dans notre exemple, est une CallExpression, qui consiste en atom('kek') atom - Identifier et kek - Literal. : une interface TypeScript qui représente un nœud de déclaration de variable dans un AST VariableDeclarator Dans notre exemple, toute l'expression sauf const est VariableDeclarator kek = atom('kek') : une interface TypeScript qui représente un nœud AST générique. Node Ou simplement en utilisant astexplorer https://astexplorer.net/?embedable=true#/gist/7fe145026f1b15adefeb307427210d38/35f114eb5b9c4d3cb626e76aa6af7782927315ed Variante finale Les épreuves 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"); `, }, ] }); D'après les tests, nous comprenons que nous devons en quelque sorte modifier le code source en utilisant notre règle. Comment rendre votre règle réparable ? Ajoutez une simple ligne au rapport de contexte. fix: fixer => fixer.replaceText(node, replaceString) - peut être un nœud réel ou une plage de symboles que vous souhaitez remplacer. nœud - quel code vous attendez-vous à voir. replaceString N'oubliez pas d'ajouter : 'code' ou : 'whitespace' pour vos balises meta de règle. fixable fixable Si vous ne savez pas comment résoudre ce problème avec eslint, essayez simplement votre projet existant. eslint --fix ./src Code lui-même 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}"`) }); } Comme vous pouvez le voir, il a juste de meilleures erreurs, des gardes de type et inclut la vérification de l'importation. Et, bien sûr, je rends la règle réparable. Mise à jour des docs Pour mettre à jour les documents, vous pouvez utiliser la commande suivante : npm run update Cette commande mettra à jour README.md et mettra à jour les documents pour chaque règle (mais vous devez écrire un peu sur chaque règle dans le fichier docs/{rule}). De plus, comme je l'ai dit, vous n'avez pas à vous soucier du fichier d'index. Publier l'étape Assurez-vous que la version se trouve dans votre package.json. "version": "1.0.0" Écrivez en terme si ce n'est pas 1.0.0. npm version 1.0.0 Ensuite, écrivez simplement à la racine. npm publish Tout sera construit et publié avec votre nom de package défini. Connectez-le à votre application Je nomme mon colis. @reatom/eslint-plugin Donc, je dois l'installer. npm i @reatom/eslint-plugin Et ajouter à ma configuration .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' } } Et tout fonctionne (pour vous devez écrire au lieu de partout). reatom-eslint-plugin “reatom” “@reatom" Conclusion Dans ce didacticiel, nous avons parcouru le processus de création d'un plugin ESLint pour la bibliothèque de gestion d'état Reatom. Nous couvrons : Comment écrire un plugin eslint en Typescript. Comment le couvrir de tests. Comment le faire fonctionner avec l'option --fix. Comment utiliser mon modèle. Comment publier le plugin eslint. Comment l'ajouter à votre référentiel existant avec eslint Ressources pour un apprentissage et une exploration plus poussés 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 Amusez-vous :)