Всем привет! Меня зовут Дмитрий Пашкевич, я 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 в нашем коде.

React

Так как все проекты мы пишем с использованием React, то конечно же добавим поддержку линтинга для кода написанного с использованием React.

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 пакета

Для того, чтобы опубликовать пакет воспользуемся следующими утилитами:

  1. release‑it + @release‑it/bumper + @release‑it/conventional‑changelog

  2. @commitlint/cli + @commitlint/config‑conventional

  3. commitizen + cz‑git + cz‑conventional‑changelog

Эти утилиты позволят версионировать наш ESLint плагин по SemVer и описывать коммиты таким образом, чтобы это происходило в автоматическом режиме и формировался CHANGELOG. Всю настройку вы можете увидеть в репозитории.

Подключение в проект

После публикации в NPM можно установить и подключить пакет в любой из проектов.

npm i eslint-plugin-nimbus-clean

Настройка конфигурации ESLint.

{
    "extends": [
      "plugin:nimbus-clean/recommended"
    ]
}

Подробная инструкция описана в README проекта.

Заключение

Это мой опыт, как создавать свои конфиги и плагины для ESLint и публиковать их в NPM.

Используя такой подход вы сможете один раз создать нужную конфигурацию для ваших проектов и потом переиспользовать. Если нужно будет ввести какие‑то изменения в ESLint конфиг — это нужно будет сделать только в одном месте, а в проектах останется только обновить версию по необходимости.

А чтобы вы еще посоветовали добавить в этот плагин?

Весь код можно увидеть в репозитории на Github, а пакет найти в NPM.

Комментарии (0)