paint-brush
テンプレート、テスト、パブリケーションを使用して Typescript で ESLint プラグインを作成する方法@antonkrylov322
1,940 測定値
1,940 測定値

テンプレート、テスト、パブリケーションを使用して Typescript で ESLint プラグインを作成する方法

Anton Krylov13m2023/03/20
Read on Terminal Reader

長すぎる; 読むには

テストとテンプレートで typescript を使用して、自分で eslint プラグインを作成します。 AST と、typesscript での AST の処理方法について詳しく知ることができます (よく知っていれば簡単です)。また、この記事の考えの歴史を理解するために、私の PR とコミットの歴史をたどることができます :)
featured image - テンプレート、テスト、パブリケーションを使用して Typescript で ESLint プラグインを作成する方法
Anton Krylov HackerNoon profile picture
0-item

目次

  1. テンプレートを使用してレポを初期化する
  2. テンプレートの初期構造
  3. テンプレートからスクリプトを使用してルールを追加する
  4. Eslint プラグインのテストを作成する
  5. Eslint ルールを書く
  6. 小さなASTの説明
  7. 最終バリアント
  8. スクリプトを使用したドキュメントの更新
  9. プラグイン公開
  10. アプリケーションに接続する

バックグラウンド

Reatom リポジトリでの私の PR に基づいて、段階的な説明を含むチュートリアルを作成してみます: https://github.com/artalar/Reatom/pull/488


詳細を知りたい場合は、 https://github.com/artalar/reatom/issues/487 の問題を読むことができます。


少し補足すると、Reatom は状態管理ライブラリです。アトムは、React の状態管理ライブラリである Reatom の概念です。

ESLint プラグインとルールとは?

ESLint プラグインは、コア ESLint パッケージと連携して特定のコーディング標準を適用する拡張機能です。プラグインには、これらの標準を適用するための個々のルールを定義するrulesフォルダーが含まれています。


ruleモジュールには、ルールを説明するmetaプロパティと、ルールの動作を定義するcreateプロパティがあります。


create関数は、チェック対象のコードと対話するために使用されるcontext引数を受け取ります。これを使用して、ライブラリに厳密な命名規則を要求するなど、ルールのロジックを定義できます。

コードを詳しく見てみましょう

レポの初期化

新しい TypeScript eslint プロジェクトの作成

npx degit https://github.com/pivaszbs/typescript-template-eslint-plugin reatom-eslint-plugin


次に、新しいプロジェクト ディレクトリに移動し、次のように依存関係をインストールします。

 cd reatom-eslint-plugin && npm i


私はいい子になりたいので、git を初期化します。

 git init && git add . && git commit -m "init"


次に、 package.jsonファイルを開き、 nameフィールドを見つけます。このフィールドは、使用時にプラグインのメイン エントリ ポイントになるため、必須です。次のように変更できます。

 "name": "eslint-plugin-reatom"


または、スコープ付きパッケージの命名規則を使用できます。

 "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


一般的なインデックスでは、スクリプトによってファイルが生成されるため、心配する必要はありません😀

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

ルールの追加とドキュメントの更新

このリポジトリには、ルールを追加してドキュメントを更新するための便利なスクリプトがいくつかあります。新しいルールを追加するには、次のコマンドを使用できます。

 npm run add-rule atom-rule suggestion


これにより、新しいルールの 3 つのセクション (ドキュメント、テスト、および実際のコード) が生成されます。ここでは、ドキュメント セクションをスキップして、最後の 2 つのセクションに焦点を当てることができます。

テストを書く

TDD (テスト駆動開発) 愛好家として、 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'}] }, ] });


この時点でテストを実行すると、 atomRuleまだ実装していないため、テストは失敗します。

ルールを書く

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;


これは単純な変種ですが、ここでは何が起こっているのかを簡単に理解できます。


コードの AST 構造をよりよく理解するには、 https://astexplorer.net/または単純に console.log 解析ノードを使用できます。

AST 型付けの簡単な説明 理解を深める

小さな例での各識別子の簡単な説明を次に示します。

const kek = atom('kek')


  1. Identifier : AST の識別子ノードを表す TypeScript インターフェース。

    1. const kek = atom ('kek')、 kek、およびatomは識別子ノードです。


  2. Literal : AST のリテラル値 (文字列、数値、ブール値など) ノードを表す TypeScript インターフェース。 const kek = atom(' kek '), 'kek' はリテラルです。


  3. CallExpression : 抽象構文木 (AST) で関数呼び出し式ノードを表す TypeScript インターフェース。


    1. この例では、 atom('kek') はCallExpression であり、 atom - 識別子と kek - リテラルで構成されています。


  4. VariableDeclarator : AST の変数宣言子ノードを表す TypeScript インターフェース


    1. この例では、const を除く式全体が VariableDeclarator kek = atom('kek')です。


  5. Node : 一般的な AST ノードを表す TypeScript インターフェース。


または単にastexplorerを使用する

最終バリアント

最終テスト

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"); `, }, ] });


テストから、ルールを使用して何らかの形でソース コードを変更する必要があることがわかりました。

ルールを修正可能にする方法

コンテキスト レポートに簡単な行を追加します。

 fix: fixer => fixer.replaceText(node, replaceString)


node - 置き換えたい実際のノードまたはシンボルの範囲です。


replaceString - 表示されると予想されるコード。


ルールのメタ タグにfixable : 'code' またはfixable : 'whitespace' を追加することを忘れないでください。


eslint で修正する方法に慣れていない場合は、既存のプロジェクトで試してみてください。

 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}"`) }); }


ご覧のとおり、より優れたエラー、型ガード、およびインポート チェックが含まれています。そしてもちろん、ルールを修正可能にします。

ドキュメントの更新

ドキュメントを更新するには、次のコマンドを使用できます。

 npm run update


このコマンドは、README.md を更新し、ルールごとにドキュメントを更新します (ただし、docs/{rule} ファイルに各ルールについて少し書く必要があります)。


また、私が言ったように、インデックス ファイルについて心配する必要はありません。

公開ステップ

バージョンが package.json にあることを確認してください。

 "version": "1.0.0"


1.0.0 でない場合は term で記述します。

 npm version 1.0.0


次に、ルートに書き込むだけです。

 npm publish


すべてがビルドされ、定義されたパッケージ名で公開されます。

アプリケーションに接続する

パッケージに名前を付けます。

 @reatom/eslint-plugin


だから、私はそれをインストールする必要があります。

 npm i @reatom/eslint-plugin


そして、私の .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' } }


そして、すべてが機能します ( reatom-eslint-pluginの場合は“@reatom"の代わりに“reatom”と書く必要があります)。

結論

このチュートリアルでは、Reatom 状態管理ライブラリ用の ESLint プラグインを作成するプロセスについて説明しました。カバーする内容:


  1. Typescript で eslint プラグインを作成する方法。
  2. テストでカバーする方法。
  3. --fix オプションで動作させる方法。
  4. 私のテンプレートの使い方。
  5. eslint プラグインの公開方法。
  6. eslint を使用して既存のリポジトリに追加する方法


さらなる学習と探求のためのリソース

  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


楽しむ :)