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.
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 rules
, qui définit des règles individuelles pour appliquer ces normes.
Chaque module rule
possède une propriété meta
qui décrit la règle et une propriété create
qui définit le comportement de la règle .
La fonction create
prend un argument context
, 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.
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 package.json
et localisez le champ name
. 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 :
"name": "eslint-plugin-reatom"
Vous pouvez également utiliser la convention d'attribution de noms de package étendu :
"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
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 };
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.
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
.
L' atomRule
est l'endroit où nous définissons le comportement de la règle. Voici une implémentation 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;
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 https://astexplorer.net/ ou simplement console.log parsed nodes.
Voici une petite description de chaque identifiant dans un petit exemple :
const kek = atom('kek')
Identifier
: une interface TypeScript qui représente un nœud d'identifiant dans un AST.
const kek = atom ('kek'), kek et atom sont des nœuds identificateurs.
Literal
: 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 '), 'kek' est un Littéral.
CallExpression
: une interface TypeScript qui représente un nœud d'expression d'appel de fonction dans un arbre de syntaxe abstraite (AST).
Dans notre exemple, atom('kek') est une CallExpression, qui consiste en atom - Identifier et kek - Literal.
VariableDeclarator
: une interface TypeScript qui représente un nœud de déclaration de variable dans un AST
Dans notre exemple, toute l'expression sauf const est VariableDeclarator kek = atom('kek')
Node
: une interface TypeScript qui représente un nœud AST générique.
Ou simplement en utilisant astexplorer
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.
Ajoutez une simple ligne au rapport de contexte.
fix: fixer => fixer.replaceText(node, replaceString)
nœud - peut être un nœud réel ou une plage de symboles que vous souhaitez remplacer.
replaceString - quel code vous attendez-vous à voir.
N'oubliez pas d'ajouter fixable : 'code' ou fixable : 'whitespace' pour vos balises meta de règle.
Si vous ne savez pas comment résoudre ce problème avec eslint, essayez simplement votre projet existant.
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}"`) }); }
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.
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.
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.
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 reatom-eslint-plugin
vous devez écrire “reatom”
au lieu de “@reatom"
partout).
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 :
Ressources pour un apprentissage et une exploration plus poussés
Amusez-vous :)