paint-brush
So erstellen Sie Ihr ESLint-Plugin in Typescript mit einer Vorlage, Tests und Veröffentlichungvon@antonkrylov322
1,924 Lesungen
1,924 Lesungen

So erstellen Sie Ihr ESLint-Plugin in Typescript mit einer Vorlage, Tests und Veröffentlichung

von Anton Krylov13m2023/03/20
Read on Terminal Reader
Read this story w/o Javascript

Zu lang; Lesen

Schreiben Sie das Eslint-Plugin selbst mit TypeScript mit Tests und Vorlage. Sie erfahren mehr über AST und wie man damit in TypeScript umgeht (ja, es ist einfach, wenn Sie es gut kennen). Sie können auch meine PR und die Geschichte der Commits durchgehen, um die Gedankengeschichte dieses Artikels zu verstehen :)
featured image - So erstellen Sie Ihr ESLint-Plugin in Typescript mit einer Vorlage, Tests und Veröffentlichung
Anton Krylov HackerNoon profile picture
0-item

Inhaltsverzeichnis

  1. Repo mithilfe einer Vorlage initialisieren
  2. Ausgangsstruktur einer Vorlage
  3. Regeln mithilfe von Skripts aus einer Vorlage hinzufügen
  4. Schreiben Sie Tests für das Eslint-Plugin
  5. Schreiben Sie die Eslint-Regel
  6. Kleine AST-Erklärung
  7. Letzte Variante
  8. Aktualisieren von Dokumenten mithilfe von Skripten
  9. Plugin-Veröffentlichung
  10. Verbinden Sie es mit Ihrer Anwendung

Hintergrund

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.

Was sind die ESLint-Plugins und -Regeln?

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.

Lassen Sie uns in den Code eintauchen

Repo initialisieren

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"

Ausgangsstruktur

 - 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 };

Regeln hinzufügen und Dokumente aktualisieren

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.

Schreiben Sie Tests

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.

Die Regel schreiben

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.

Eine kleine Erklärung zum besseren Verständnis von AST-Typisierungen

Hier ist eine kleine Beschreibung jedes Bezeichners in einem kleinen Beispiel:

const kek = atom('kek')


  1. Identifier : eine TypeScript-Schnittstelle, die einen Bezeichnerknoten in einem AST darstellt.

    1. const kek = atom ('kek'), kek und atom sind Bezeichnerknoten.


  2. Literal : eine TypeScript-Schnittstelle, die einen Literalwertknoten (Zeichenfolge, Zahl, Boolescher Wert usw.) in einem AST darstellt. const kek = atom(' kek '), 'kek' ist ein Literal.


  3. CallExpression : eine TypeScript-Schnittstelle, die einen Funktionsaufruf-Ausdrucksknoten in einem abstrakten Syntaxbaum (AST) darstellt.


    1. In unserem Beispiel ist atom('kek') ein CallExpression, der aus atom – Identifier und kek – Literal besteht.


  4. VariableDeclarator : eine TypeScript-Schnittstelle, die einen Variablendeklaratorknoten in einem AST darstellt


    1. In unserem Beispiel lautet der gesamte Ausdruck außer const VariableDeclarator kek = atom('kek')


  5. Node : eine TypeScript-Schnittstelle, die einen generischen AST-Knoten darstellt.


Oder einfach astexplorer verwenden

Letzte Variante

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.

Wie können Sie Ihre Regel reparierbar machen?

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

Code selbst

 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.

Aktualisierung der Dokumente

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.

Schritt veröffentlichen

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.

Verbinden Sie es mit Ihrer Anwendung

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).

Abschluss

In diesem Tutorial haben wir den Prozess der Erstellung eines ESLint-Plugins für die Reatom-Statusverwaltungsbibliothek durchlaufen. Wir schützen:


  1. So schreiben Sie ein Eslint-Plugin in Typescript.
  2. Wie man es mit Tests abdeckt.
  3. So bringen Sie es mit der Option --fix zum Laufen.
  4. So verwenden Sie meine Vorlage.
  5. So veröffentlichen Sie das Eslint-Plugin.
  6. So fügen Sie es mit eslint zu Ihrem vorhandenen Repository hinzu


Ressourcen für weiteres Lernen und Erkunden

  1. https://github.com/pivaszbs/typescript-template-eslint-plugin
  2. https://astexplorer.net/
  3. https://github.com/artalar/reatom/pull/488/files
  4. https://eslint.org/docs/latest/extend/plugins
  5. https://www.reatom.dev/
  6. https://github.com/artalar/reatom
  7. https://docs.npmjs.com/about-semantic-versioning


Viel Spaß :)