Ich werde versuchen, ein Tutorial basierend auf meiner PR im Reatom-Repository mit einer Schritt-für-Schritt-Erklärung zu schreiben: https://github.com/artalar/Reatom/pull/488
Wenn Sie mehr wissen möchten, können Sie die Ausgabe https://github.com/artalar/reatom/issues/487 lesen.
Um etwas Kontext hinzuzufügen: Reatom ist eine Zustandsverwaltungsbibliothek. Atome sind ein Konzept in Reatom, einer Zustandsverwaltungsbibliothek für React.
ESLint-Plugins sind Erweiterungen, die mit dem ESLint-Kernpaket zusammenarbeiten, um bestimmte Codierungsstandards durchzusetzen. Plugins enthalten einen rules
, der individuelle Regeln zur Durchsetzung dieser Standards definiert.
Jedes rule
verfügt über eine meta
, die die Regel beschreibt, und eine create
, die das Verhalten der Regel definiert.
Die create
benötigt ein context
, das für die Interaktion mit dem überprüften Code verwendet wird. Sie können es verwenden, um die Logik Ihrer Regel zu definieren, z. B. um strenge Namenskonventionen für Ihre Bibliothek zu fordern.
Erstellen eines neuen TypeScript-Eslint-Projekts
npx degit https://github.com/pivaszbs/typescript-template-eslint-plugin reatom-eslint-plugin
Navigieren Sie dann zum neuen Projektverzeichnis und installieren Sie die Abhängigkeiten mit:
cd reatom-eslint-plugin && npm i
Ich möchte ein guter Junge sein, also initiiere ich Git.
git init && git add . && git commit -m "init"
Öffnen Sie als Nächstes die Datei package.json
und suchen Sie das name
. Dieses Feld ist wichtig, da es bei der Verwendung der Haupteinstiegspunkt für Ihr Plugin ist. Sie können es wie folgt ändern:
"name": "eslint-plugin-reatom"
Alternativ können Sie die Namenskonvention für bereichsbezogene Pakete verwenden:
"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
Im allgemeinen Index werden Dateien von Skripten generiert, sodass Sie sich darüber keine Sorgen machen müssen 😀
/* 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 };
In diesem Repository finden Sie einige praktische Skripte zum Hinzufügen von Regeln und zum Aktualisieren von Dokumenten. Um eine neue Regel hinzuzufügen, können Sie den folgenden Befehl verwenden:
npm run add-rule atom-rule suggestion
Dadurch werden drei Abschnitte für die neue Regel generiert: Dokumentation, Tests und tatsächlicher Code. Wir können den Dokumentationsabschnitt vorerst überspringen und uns auf die letzten beiden konzentrieren.
Als TDD-Enthusiasten (testgetriebene Entwicklung) erstellen wir zunächst einige einfache Tests in der Datei 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'}] }, ] });
Wenn Sie die Tests jetzt ausführen, schlagen sie fehl, da wir die atomRule
noch nicht implementiert haben.
In der atomRule
definieren wir das Verhalten der Regel. Hier ist eine einfache Implementierung:
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 ist eine einfache Variante, aber hier können wir leicht verstehen, was vor sich geht.
Für ein besseres Verständnis der AST-Struktur Ihres Codes können Sie https://astexplorer.net/ oder einfach von console.log analysierte Knoten verwenden.
Hier ist eine kleine Beschreibung jedes Bezeichners in einem kleinen Beispiel:
const kek = atom('kek')
Identifier
: eine TypeScript-Schnittstelle, die einen Bezeichnerknoten in einem AST darstellt.
const kek = atom ('kek'), kek und atom sind Bezeichnerknoten.
Literal
: eine TypeScript-Schnittstelle, die einen Literalwertknoten (Zeichenfolge, Zahl, Boolescher Wert usw.) in einem AST darstellt. const kek = atom(' kek '), 'kek' ist ein Literal.
CallExpression
: eine TypeScript-Schnittstelle, die einen Funktionsaufruf-Ausdrucksknoten in einem abstrakten Syntaxbaum (AST) darstellt.
In unserem Beispiel ist atom('kek') ein CallExpression, der aus atom – Identifier und kek – Literal besteht.
VariableDeclarator
: eine TypeScript-Schnittstelle, die einen Variablendeklaratorknoten in einem AST darstellt
In unserem Beispiel lautet der gesamte Ausdruck außer const VariableDeclarator kek = atom('kek')
Node
: eine TypeScript-Schnittstelle, die einen generischen AST-Knoten darstellt.
Oder einfach astexplorer verwenden
Die letzten Tests
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"); `, }, ] });
Aus Tests wissen wir, dass wir den Quellcode mithilfe unserer Regel irgendwie ändern müssen.
Fügen Sie dem Kontextbericht eine einfache Zeile hinzu.
fix: fixer => fixer.replaceText(node, replaceString)
Knoten – kann ein tatsächlicher Knoten oder ein Bereich von Symbolen sein, den Sie ersetzen möchten.
replaceString – welcher Code Sie erwarten.
Vergessen Sie nicht, fixable : 'code' oder fixable : 'whitespace' für Ihre Regel-Meta-Tags hinzuzufügen.
Wenn Sie nicht wissen, wie Sie das Problem mit eslint beheben können, probieren Sie es einfach mit Ihrem vorhandenen Projekt aus.
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}"`) }); }
Wie Sie sehen, verfügt es lediglich über bessere Fehler, Typschutz und eine Importprüfung. Und natürlich mache ich die Regel reparierbar.
Um die Dokumente zu aktualisieren, können Sie den folgenden Befehl verwenden:
npm run update
Dieser Befehl aktualisiert README.md und aktualisiert die Dokumente für jede Regel (Sie müssen jedoch etwas über jede Regel in die Datei docs/{rule} schreiben).
Außerdem müssen Sie sich, wie gesagt, keine Sorgen um die Indexdatei machen.
Stellen Sie sicher, dass die Version in Ihrer package.json enthalten ist.
"version": "1.0.0"
Geben Sie term ein, wenn es nicht 1.0.0 ist.
npm version 1.0.0
Dann schreiben Sie einfach in die Wurzel.
npm publish
Alles wird mit Ihrem definierten Paketnamen erstellt und veröffentlicht.
Ich benenne mein Paket.
@reatom/eslint-plugin
Also muss ich es installieren.
npm i @reatom/eslint-plugin
Und zu meiner .eslintrc-Konfiguration hinzufügen.
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' } }
Und alles funktioniert einfach (nur für reatom-eslint-plugin
sollten Sie überall “reatom”
statt “@reatom"
schreiben).
In diesem Tutorial haben wir den Prozess der Erstellung eines ESLint-Plugins für die Reatom-Statusverwaltungsbibliothek durchlaufen. Wir schützen:
Ressourcen für weiteres Lernen und Erkunden
Viel Spaß :)