Повышаем продуктивность разработки: магия общей ESLint конфигурации
- четверг, 7 сентября 2023 г. в 00:00:14
Всем привет! Меня зовут Дмитрий Пашкевич, я Frontend разработчик. Эта статья не просто туториал по созданию единой ESLint конфигурации, которую можно переиспользовать между проектами. Это история решения боли диcкуссий о форматировании кода на ревью от проекта к проекту.
Статья будет полезна разработчикам: которые хотят унифицировать подход к форматированию кода в разных проектах; ищут проверенное решение для стандартизации кодовой базы.
Единое форматирование кода в команде уменьшает ментальную нагрузку при код‑ревью, чтении/написании кода или старте нового проекта. Оно позволяет сосредоточиться на том как работает код, а не отвлекаться на то, как расставлены точки с запятой.
Представьте, что у вас 5 проектов и в каждом свои правила форматирования. Вы стартуете 6й и копируете конфиги из предыдущих проектов, добавляя новые правила. И так по кругу. Получаем неконсистентные ESLint конфиги во всех проектах, а соответственно не консистентно выглядящий код между проектами. Как итог простые вещи обсуждаются из раза в раз при ревью кода.
В этой статье я расскажу, как написать плагин / конфиг для ESLint и опубликовать его как пакет. Это позволит исправлять, добавлять и изменять необходимые правила в одном месте и подключать как один модуль в другие проекты.
В команде мы используем публикацию в приватный registry, а в рамках этой статьи в исходном коде вы сможете увидеть публикацию в NPM.
Весь код можно увидеть в репозитории на Github, а пакет найти в NPM.
Итак первое с чего начнем это с создания заготовки проекта для ESLint плагина.
Для этого заходим в документацию ESLint раздел «Create Plugin» и воспользуемся рекомендацией по созданию нового проекта. Идем в раздел по установке и выполняем необходимые действия.
Открываем командную строку.
Если у вас еще не установлена платформа Node.js, то необходимо установить.
Далее устанавливаем Yeoman — инструмент для генерации шаблонных проектов, если у вас он еще не установлен.
npm i -g yo
Далее установим утилиту для генерации 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», чтобы исключить отправку ненужных файлов в репозиторий.
touch .gitignore
Для того, чтобы не изобретать содержимое этого файла с нуля, всегда пользуюсь сервисом: https://www.toptal.com/developers/gitignore. также можно найти плагины под свою IDE, которые позволяет генерировать этот файл прямо там.
Нас интересует «.gitignore» для Node.js — возьмем содержимое по ссылке и добавим в созданный ранее «.gitignore» файл.
Проинициализируем 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 часто можно увидеть пакеты с названиями начинающиимися на: «eslint‑plugin‑*» и «eslint‑config‑*». В чем же отличие?
Плагины необходимо называть как «eslint‑plugin‑*». При добавлении плагина в свой проект правила не будут включены автоматически и поэтому каждое правило нужно будет включить самостоятельно.
Конфиги необходимо назвать как «eslint‑config‑*». При добавлении конфига в свой проект все правила будут включены автоматически и не нужно будет каждое правило включать самостоятельно.
На практике, плагины нужны, если вы создаете свои правила линтинга кода и хотите, чтобы пользователи плагина могли их включать или выключать самостоятельно. Во всех остальных случаях можно использовать конфиг, так как скорее всего просто переиспользуется набор конфигураций из других плагинов.
Однако плагин может быть использован и как конфиг с общими правилами включенными по умолчанию. И в этой статье мы рассмотрим такой вариант плагина, включающий в себя конфигурацию по умолчанию (recommended). Часто в документации к плагинам можно увидеть, что такие плагины подключаются в секцию extends как «plugin:your‑plugin‑name/recommended» — подробнее можно прочитать в документации ESLint.
Далее определимся с основными плагинами, которые будем использовать в наших проектах.
Это непосредственно сам eslint, из которого мы возьмем рекомендованный конфиг.
Чтобы не использовать отдельную конфигурацию для Prettier, добавим в наш плагин/конфиг ESLint правила из пакетов:
eslint‑config‑prettier — отключает все правила, которые не нужны или могут конфликтовать с Prettier
eslint‑plugin‑prettier — позволит нам настроить Prettier как правила ESLint и будет показывать информацию о проблемах как о ESLint проблемах.
Зададим правила для работы с import/export в нашем коде.
eslint‑plugin‑import — позволит избежать различных проблем при import/export модулей в коде.
eslint‑import‑resolver‑typescript — добавит поддержку TypeScript для предыдущего плагина.
eslint‑plugin‑simple‑import‑sort — позволит настроить сортировку модулей в нужном порядке по определенным правилам.
Так как все проекты мы пишем с использованием 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‑eslint/eslint‑plugin.
Добавим плагин для линтинга кода использующего работу с Promise — eslint‑plugin‑promise.
И добавим два последних плагина для линтинга качества кода.
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.
В этом разделе опишу подключение правил для каждой секции из предыдущего раздела — описывать буду в том же порядке.
Создадим директорию rules в lib — в которой будут лежать файлы для каждой части конфига.
mkdir ./rules/lib
Создадим файл для описания конфига 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.
touch lib/rules/prettier.js
Добавляем правила.
/** eslint-plugin-prettier */
module.exports = {
"prettier/prettier": "error",
}
Создаем файл для описания конфига «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.
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.
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.
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 }],
}
Создаем файл для описания конфига 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
}
],
}
Для того, чтобы опубликовать пакет воспользуемся следующими утилитами:
Эти утилиты позволят версионировать наш ESLint плагин по SemVer и описывать коммиты таким образом, чтобы это происходило в автоматическом режиме и формировался CHANGELOG. Всю настройку вы можете увидеть в репозитории.
После публикации в NPM можно установить и подключить пакет в любой из проектов.
npm i eslint-plugin-nimbus-clean
Настройка конфигурации ESLint.
{
"extends": [
"plugin:nimbus-clean/recommended"
]
}
Подробная инструкция описана в README проекта.
Это мой опыт, как создавать свои конфиги и плагины для ESLint и публиковать их в NPM.
Используя такой подход вы сможете один раз создать нужную конфигурацию для ваших проектов и потом переиспользовать. Если нужно будет ввести какие‑то изменения в ESLint конфиг — это нужно будет сделать только в одном месте, а в проектах останется только обновить версию по необходимости.
А чтобы вы еще посоветовали добавить в этот плагин?
Весь код можно увидеть в репозитории на Github, а пакет найти в NPM.