Как создать свой плагин ESLint в Typescript с помощью шаблона, тестов и публикации
20 марта 2023 г.Содержание
- Инициализировать репозиторий с помощью шаблона
- Исходная структура шаблона
- Добавление правил с помощью скриптов из шаблона
- Написать тесты для подключаемого модуля Eslint
- Записать правило Эслинта
- Небольшое объяснение AST
- Окончательный вариант
- Обновление документов с помощью скриптов
- Публикация плагинов
- Подключите его к своему приложению.
Фон
Я постараюсь написать туториал на основе моего PR в репозитории Reatom с пошаговым объяснением: https ://github.com/artalar/Reatom/pull/488
Если вы хотите узнать больше, вы можете прочитать выпуск https://github.com/artalar/reatom/issues/487.
Чтобы добавить немного контекста, Reatom — это библиотека управления состоянием. Атомы — это концепция в Reatom, библиотеке управления состоянием для React.
Что такое плагины и правила 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
Это создаст три раздела для нового правила: документация, тесты и фактический код. Мы можем пока пропустить раздел документации и сосредоточиться на последних двух.
Написание тестов
Как энтузиаст 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')
-
Идентификатор
: интерфейс TypeScript, представляющий узел идентификатора в AST. -
const kek = atom('kek'), kek, и atom являются узлами идентификаторов.< /p>
2.
.Literal
: интерфейс TypeScript, который представляет узел буквального значения (строка, число, логическое значение и т. д.) в AST. const kek = atom(‘kek’), ‘kek’ — это литерал
3. CallExpression
: интерфейс TypeScript, представляющий узел выражения вызова функции в абстрактном синтаксическом дереве (AST).
-
В нашем примере atom(‘kek’) — это CallExpression, который состоит из atom — идентификатора и kek — литерала.
4.
.VariableDeclarator
: интерфейс TypeScript, представляющий узел декларатора переменных в AST
-
В нашем примере все выражение, кроме const, представляет собой VariableDeclarator kek = atom(‘kek’)
5.
Узел
: интерфейс TypeScript, представляющий общий узел AST.
Или просто с помощью 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)
узел — может быть фактическим узлом или диапазоном символов, которые вы хотите заменить.
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.
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”
).
Заключение
В этом руководстве мы рассмотрели процесс создания подключаемого модуля ESLint для библиотеки управления состоянием Reatom. Мы покрываем:
- Как написать плагин eslint на Typescript.
- Как покрыть это тестами.
- Как заставить его работать с параметром --fix.
- Как использовать мой шаблон.
- Как опубликовать подключаемый модуль eslint.
- Как добавить его в существующий репозиторий с помощью eslint
Ресурсы для дальнейшего изучения и изучения
- 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
Удачи :)
Оригинал