Всем привет! Меня зовут Дмитрий Пашкевич, я Frontend разработчик. Эта статья не просто туториал по созданию единой ESLint конфигурации, которую можно переиспользовать между проектами. Это история решения боли диcкуссий о форматировании кода на ревью от проекта к проекту.
Статья будет полезна разработчикам: которые хотят унифицировать подход к форматированию кода в разных проектах; ищут проверенное решение для стандартизации кодовой базы.
Зачем нужен единый плагин/конфиг ESLint?
Единое форматирование кода в команде уменьшает ментальную нагрузку при код‑ревью, чтении/написании кода или старте нового проекта. Оно позволяет сосредоточиться на том как работает код, а не отвлекаться на то, как расставлены точки с запятой.
Представьте, что у вас 5 проектов и в каждом свои правила форматирования. Вы стартуете 6й и копируете конфиги из предыдущих проектов, добавляя новые правила. И так по кругу. Получаем неконсистентные ESLint конфиги во всех проектах, а соответственно не консистентно выглядящий код между проектами. Как итог простые вещи обсуждаются из раза в раз при ревью кода.
В этой статье я расскажу, как написать плагин / конфиг для ESLint и опубликовать его как пакет. Это позволит исправлять, добавлять и изменять необходимые правила в одном месте и подключать как один модуль в другие проекты.
В команде мы используем публикацию в приватный registry, а в рамках этой статьи в исходном коде вы сможете увидеть публикацию в NPM.
Если хочется быстрее увидеть исходный код :)
Весь код можно увидеть в репозитории на Github, а пакет найти в NPM.
Подготовка репозитория
Итак первое с чего начнем это с создания заготовки проекта для ESLint плагина.
Для этого заходим в документацию ESLint раздел «Create Plugin» и воспользуемся рекомендацией по созданию нового проекта. Идем в раздел по установке и выполняем необходимые действия.
Открываем командную строку.
Установка Node.js
Если у вас еще не установлена платформа Node.js, то необходимо установить.
Установка Yeoman
Далее устанавливаем Yeoman — инструмент для генерации шаблонных проектов, если у вас он еще не установлен.
npm i -g yo
Установка генератора ESLint плагинов
Далее установим утилиту для генерации ESLint плагина.
npm i -g generator-eslint
Отлично! Все подготовительные работы выполнены, теперь настало время для создания базового проекта нашего плагина.
Создаем директорию проекта
Создадим директорию нашего проекта.
mkdir eslint-plugin-nimbus-clean
И перейдем в нее.
cd ./eslint-plugin-nimbus-clean
Далее создадим структуру проекта.
yo eslint:plugin
Эта команда запустит wizard по созданию проекта ESLint плагина.
Отвечаем на вопросы мастера установки
Пройдем небольшой опрос.
? What is your name? dipiash
? What is the plugin ID? nimbus-clean
? Type a short description of this plugin: A comprehensive linting solution that sweeps your code clean
? Does this plugin contain custom ESLint rules? No
? Does this plugin contain one or more processors? No
На последние два вопроса был дан ответ «No» так как мы не будем использовать ни кастомных правил, ни кастомных преобразователей на этом этапе, а только определенный набор из комбинации других плагинов.
Дождемся пока генератор создаст стартовый проект и откроем в IDE получившийся проект.
Заводим файл .gitignore
Далее заведем файл «.gitignore», чтобы исключить отправку ненужных файлов в репозиторий.
touch .gitignore
Для того, чтобы не изобретать содержимое этого файла с нуля, всегда пользуюсь сервисом: https://www.toptal.com/developers/gitignore. также можно найти плагины под свою IDE, которые позволяет генерировать этот файл прямо там.
Нас интересует «.gitignore» для Node.js — возьмем содержимое по ссылке и добавим в созданный ранее «.gitignore» файл.
Инициализируем git
Проинициализируем git репозиторий.
git init
Подготовка проекта
Проведем изменения над созданным проектом.
Возможно в будущем понадобится написать кастомные правила, сразу добавим плагин для линтинга ESLint правил и подключим как указано в документации.
npm install eslint-plugin-eslint-plugin --save-dev
В файле package.json в раздел «scripts» добавим две команды: «build» и «pack».
Команда “build” будет собирать наш проект.
rm -rf ./dist && mkdir ./dist && cp -r ./lib/* ./dist
Команда «pack» будет нужна для локальной проверки работы плагина.
npm pack --pack-destination=./dist
Также поправим блоки: «main» и «exports», так как содержимое пакета для публикации в npm будет находиться в директории «dist» и поправим раздел «files».
"main": "./dist/index.js",
"exports": "./dist/index.js",
"files": [
"/dist",
"README.md",
"package.json"
]
Также поправим файл «lib/index.js». Нам будет не нужен require index пакет, поэтому удалим эту часть кода.
ESLint Plugin vs. EsLint Config
При настройке ESLint часто можно увидеть пакеты с названиями начинающиимися на: «eslint‑plugin‑*» и «eslint‑config‑*». В чем же отличие?
Плагины необходимо называть как «eslint‑plugin‑*». При добавлении плагина в свой проект правила не будут включены автоматически и поэтому каждое правило нужно будет включить самостоятельно.
Конфиги необходимо назвать как «eslint‑config‑*». При добавлении конфига в свой проект все правила будут включены автоматически и не нужно будет каждое правило включать самостоятельно.
На практике, плагины нужны, если вы создаете свои правила линтинга кода и хотите, чтобы пользователи плагина могли их включать или выключать самостоятельно. Во всех остальных случаях можно использовать конфиг, так как скорее всего просто переиспользуется набор конфигураций из других плагинов.
Однако плагин может быть использован и как конфиг с общими правилами включенными по умолчанию. И в этой статье мы рассмотрим такой вариант плагина, включающий в себя конфигурацию по умолчанию (recommended). Часто в документации к плагинам можно увидеть, что такие плагины подключаются в секцию extends как «plugin:your‑plugin‑name/recommended» — подробнее можно прочитать в документации ESLint.
Набор конфигов / плагинов
Далее определимся с основными плагинами, которые будем использовать в наших проектах.
ESLint
Это непосредственно сам eslint, из которого мы возьмем рекомендованный конфиг.
Prettier
Чтобы не использовать отдельную конфигурацию для Prettier, добавим в наш плагин/конфиг ESLint правила из пакетов:
eslint‑config‑prettier — отключает все правила, которые не нужны или могут конфликтовать с Prettier
eslint‑plugin‑prettier — позволит нам настроить Prettier как правила ESLint и будет показывать информацию о проблемах как о ESLint проблемах.
Imports
Зададим правила для работы с import/export в нашем коде.
eslint‑plugin‑import — позволит избежать различных проблем при import/export модулей в коде.
eslint‑import‑resolver‑typescript — добавит поддержку TypeScript для предыдущего плагина.
eslint‑plugin‑simple‑import‑sort — позволит настроить сортировку модулей в нужном порядке по определенным правилам.
React
Так как все проекты мы пишем с использованием React, то конечно же добавим поддержку линтинга для кода написанного с использованием React.
eslint‑plugin‑react — правила для линтинга кода на React
eslint‑plugin‑react‑hooks — поможет нам соблюдать правила написания React Hooks
eslint‑plugin‑testing‑library — будет проверять код наших тестов для Testing Library
eslint‑plugin‑jsx‑a11y— будет проверять добавили ли мы правила доступности в наши JSX элементы или нет
Typescript
Так как вся кодовая база пишется на TypeScript, добавим правила для линтинга кода на TypeScript — @typescript‑eslint/eslint‑plugin.
Promises
Добавим плагин для линтинга кода использующего работу с Promise — eslint‑plugin‑promise.
Code quality
И добавим два последних плагина для линтинга качества кода.
eslint‑plugin‑sonarjs — позволит определять возможные баги и использование подозрительных паттернов в коде.
eslint‑plugin‑unicorn — более 100 полезных правил для ESLint.
Установим все перечисленные выше пакеты и добавим их как peerDependencies в package.json.
npm i -D eslint-import-resolver-typescript @typescript-eslint/eslint-plugin eslint-config-prettier eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-prettier eslint-plugin-promise eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-simple-import-sort eslint-plugin-sonarjs eslint-plugin-testing-library eslint-plugin-unicorn
Также, чтобы пользователь при установке нашего пакета смог увидеть весь перечень недостающих проектов, воспользуемся директивой в package.json peerDependenciesMeta и пометим каждую зависимость как optional false.
Правила для ESLint
В этом разделе опишу подключение правил для каждой секции из предыдущего раздела — описывать буду в том же порядке.
Создадим директорию rules в lib — в которой будут лежать файлы для каждой части конфига.
mkdir ./rules/lib
ESLint
Создадим файл для описания конфига ESLint.
touch lib/rules/common.js
И добавим туда правила.
/** eslint */
module.exports = {
// https://eslint.org/docs/latest/rules/curly
"curly": ["error", "all"],
// https://eslint.org/docs/latest/rules/padding-line-between-statements
"padding-line-between-statements": [
"error",
{ "blankLine": "always", "prev": ["const", "let", "var"], "next": "*" },
{ "blankLine": "any", "prev": ["const", "let", "var"], "next": ["const", "let", "var"] },
{ "blankLine": "always", "prev": "*", "next": "return" }
],
// https://eslint.org/docs/latest/rules/no-multiple-empty-lines
"no-multiple-empty-lines": ["error"],
// https://eslint.org/docs/latest/rules/arrow-body-style
"arrow-body-style": ["error", "as-needed"],
// https://eslint.org/docs/latest/rules/prefer-arrow-callback
"prefer-arrow-callback": "off",
// https://eslint.org/docs/latest/rules/no-console
"no-console": ["error", { "allow": ["warn", "info", "error"] }],
// https://eslint.org/docs/latest/rules/no-underscore-dangle
"no-underscore-dangle": [
"error",
{
"allow": ["_id", "__typename", "__schema", "__dirname", "_global"],
"allowAfterThis": true
}
],
}
Prettier
Создаем файл для описания конфига Prettier.
touch lib/rules/prettier.js
Добавляем правила.
/** eslint-plugin-prettier */
module.exports = {
"prettier/prettier": "error",
}
Imports
Создаем файл для описания конфига «eslint‑plugin‑import» и «eslint‑plugin‑simple‑import‑sort».
touch lib/rules/import.js
touch lib/rules/simple-import-sort.js
Добавляем правила.
/** eslint-plugin-import */
module.exports = {
// https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/first.md
"import/first": "error",
// https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/newline-after-import.md
"import/newline-after-import": "error",
// https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/no-duplicates.md
"import/no-duplicates": "error",
// https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/prefer-default-export.md
"import/prefer-default-export": "off",
// https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/no-anonymous-default-export.md
"import/no-anonymous-default-export": [
"error",
{
"allowArray": false,
"allowArrowFunction": false,
"allowAnonymousClass": false,
"allowAnonymousFunction": false,
"allowCallExpression": true,
"allowLiteral": false,
"allowObject": true
}
],
// https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/no-unassigned-import.md
"import/no-unassigned-import": "off",
// https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/no-unused-modules.md
"import/no-unused-modules": "error"
}
React
Создадем файл для описания конфига для React.
touch lib/rules/react.js
Добавляем правила.
/** eslint-plugin-react-* */
module.exports = {
// https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/prop-types.md
"react/prop-types": "off",
// https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/README.md
"react-hooks/exhaustive-deps": [2],
}
TypeScript
Создаем файл для описания конфига TypeScript.
touch lib/rules/typescript.js
Добавляем правила.
/** @typescript-eslint-* */
module.exports = {
// https://typescript-eslint.io/rules/no-use-before-define/
"@typescript-eslint/no-use-before-define": ["error"],
// https://typescript-eslint.io/rules/no-unused-vars/
"@typescript-eslint/no-unused-vars": [
"error"
],
// https://typescript-eslint.io/rules/no-explicit-any/
"@typescript-eslint/no-explicit-any": "error",
// https://typescript-eslint.io/rules/naming-convention/
"@typescript-eslint/naming-convention": [
"error",
{
"selector": "interface",
"format": ["PascalCase"],
"custom": {
"regex": "[A-Za-z]Interface$",
"match": true
}
},
{
"selector": "typeAlias",
"format": ["PascalCase"],
"custom": {
"regex": "[A-Za-z]Type$",
"match": true
}
}
],
// https://typescript-eslint.io/rules/ban-types/
"@typescript-eslint/ban-types": [
"error",
{
"types": {
// un-ban a type that's banned by default
"{}": false
},
"extendDefaults": true
}
]
}
Promises
Создаем файл для описания конфига для Promises.
touch lib/rules/promise.js
Добавляем правила.
/** eslint-plugin-promise */
module.exports = {
// https://github.com/eslint-community/eslint-plugin-promise/blob/main/docs/rules/prefer-await-to-then.md
"promise/prefer-await-to-then": "off",
// https://github.com/eslint-community/eslint-plugin-promise/blob/main/docs/rules/always-return.md
"promise/always-return": "off",
// https://github.com/eslint-community/eslint-plugin-promise/blob/main/docs/rules/catch-or-return.md
"promise/catch-or-return": [2, { "allowThen": true, "allowFinally": true }],
}
Code quality
Создаем файл для описания конфига ESLint.
touch lib/rules/sonarjs.js
touch lib/rules/unicorn.js
Добавляем правила.
/** eslint-plugin-sonarjs */
module.exports = {
// https://github.com/SonarSource/eslint-plugin-sonarjs/blob/master/docs/rules/no-identical-functions.md
"sonarjs/no-identical-functions": ["error", 5],
}
/** eslint-plugin-unicorn */
module.exports = {
// https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-array-reduce.md
"unicorn/no-array-reduce": "off",
// https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-module.md
"unicorn/prefer-module": "off",
// https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-null.md
"unicorn/no-null": "off",
// https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-useless-undefined.md
"unicorn/no-useless-undefined": "off",
// https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/filename-case.md
"unicorn/filename-case": [
"error",
{
"cases": {
"pascalCase": true,
"camelCase": true
},
"ignore": [
"next-env.d.ts",
"vite(st)?.config.ts",
"vite-environment.d.ts",
"\\.spec.ts(x)?",
"\\.types.ts(x)?",
"\\.stories.ts(x)?",
"\\.styled.ts(x)?",
"\\.styles.ts(x)?",
]
}
],
// https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prevent-abbreviations.md
"unicorn/prevent-abbreviations": [
"error",
{
"checkFilenames": false
}
],
}
Публикация NPM пакета
Для того, чтобы опубликовать пакет воспользуемся следующими утилитами:
Эти утилиты позволят версионировать наш ESLint плагин по SemVer и описывать коммиты таким образом, чтобы это происходило в автоматическом режиме и формировался CHANGELOG. Всю настройку вы можете увидеть в репозитории.
Подключение в проект
После публикации в NPM можно установить и подключить пакет в любой из проектов.
npm i eslint-plugin-nimbus-clean
Настройка конфигурации ESLint.
{
"extends": [
"plugin:nimbus-clean/recommended"
]
}
Подробная инструкция описана в README проекта.
Заключение
Это мой опыт, как создавать свои конфиги и плагины для ESLint и публиковать их в NPM.
Используя такой подход вы сможете один раз создать нужную конфигурацию для ваших проектов и потом переиспользовать. Если нужно будет ввести какие‑то изменения в ESLint конфиг — это нужно будет сделать только в одном месте, а в проектах останется только обновить версию по необходимости.
А чтобы вы еще посоветовали добавить в этот плагин?
Весь код можно увидеть в репозитории на Github, а пакет найти в NPM.