Публикация правила как часть плагина

Структура ESLint-плагина

После создания собственного правила следующим этапом становится его публикация в составе ESLint-плагина. Такой подход позволяет распространять сразу набор правил, конфигураций и вспомогательных компонентов через единый npm-пакет.

Типичная структура проекта выглядит следующим образом:

eslint-plugin-myplugin/
├── lib/
│   ├── rules/
│   │   ├── no-console-log.js
│   │   ├── require-foo.js
│   │   └── index.js
│   └── configs/
│       ├── recommended.js
│       └── strict.js
├── package.json
├── README.md
└── LICENSE

В современных версиях ESLint структура может быть упрощена, однако логическое разделение на правила и конфигурации сохраняется практически всегда.

Пример отдельного правила:

module.exports = {
    meta: {
        type: "problem",
        docs: {
            description: "Запрещает использование console.log"
        },
        schema: []
    },

    create(context) {
        return {
            CallEx * pression(node) {
                if (
                    node.callee.object &&
                    node.callee.object.name === "console" &&
                    node.callee.property &&
                    node.callee.property.name === "log"
                ) {
                    context.report({
                        node,
                        message: "Использование console.log запрещено"
                    });
                }
            }
        };
    }
};

Экспорт правил

Для того чтобы ESLint мог получить доступ к правилам плагина, они должны быть экспортированы через основной объект плагина.

Файл:

// lib/rules/index.js

module.exports = {
    "no-console-log": require("./no-console-log"),
    "require-foo": require("./require-foo")
};

В современных версиях часто используется непосредственный экспорт из главного файла:

const noConsoleLog = require("./lib/rules/no-console-log");
const requireFoo = require("./lib/rules/require-foo");

module.exports = {
    rules: {
        "no-console-log": noConsoleLog,
        "require-foo": requireFoo
    }
};

После публикации пользователь сможет активировать правило следующим образом:

{
    "plugins": ["myplugin"],
    "rules": {
        "myplugin/no-console-log": "error"
    }
}

Префикс имени правила всегда соответствует имени плагина.


Формирование объекта плагина

Главный экспорт npm-пакета представляет собой объект, содержащий различные расширения ESLint.

Пример:

module.exports = {
    rules: {
        "no-console-log": require("./lib/rules/no-console-log"),
        "require-foo": require("./lib/rules/require-foo")
    },

    configs: {
        recommended: require("./lib/configs/recommended")
    }
};

Свойство rules содержит все пользовательские правила.

Свойство configs содержит готовые наборы настроек.

Дополнительно могут присутствовать:

module.exports = {
    rules: {},
    configs: {},
    processors: {},
    environments: {}
};

Каждая секция расширяет функциональность ESLint определённым образом.


Создание рекомендованной конфигурации

Большинство популярных плагинов поставляются вместе с рекомендованным набором правил.

Пример:

// lib/configs/recommended.js

module.exports = {
    plugins: ["myplugin"],

    rules: {
        "myplugin/no-console-log": "error",
        "myplugin/require-foo": "warn"
    }
};

После этого конфигурация подключается к экспорту плагина:

module.exports = {
    rules: {
        "no-console-log": require("../rules/no-console-log"),
        "require-foo": require("../rules/require-foo")
    },

    configs: {
        recommended: require("./recommended")
    }
};

Использование:

{
    "extends": [
        "plugin:myplugin/recommended"
    ]
}

Подобный подход позволяет централизованно управлять настройками и обеспечивает единообразное использование правил.


Именование npm-пакета

ESLint использует специальные соглашения для поиска плагинов.

Для неименованных пакетов:

eslint-plugin-myplugin

Подключение:

{
    "plugins": ["myplugin"]
}

Для scoped-пакетов:

@company/eslint-plugin-myplugin

Подключение:

{
    "plugins": ["@company/myplugin"]
}

ESLint автоматически убирает префикс eslint-plugin- при регистрации.

Корректное именование крайне важно, поскольку именно по нему ESLint определяет принадлежность пакета к категории плагинов.


Настройка package.json

Минимальный вариант:

{
    "name": "eslint-plugin-myplugin",
    "version": "1.0.0",
    "main": "index.js",
    "keywords": [
        "eslint",
        "eslintplugin"
    ]
}

Более полный пример:

{
    "name": "eslint-plugin-myplugin",
    "version": "1.0.0",
    "description": "Набор пользовательских правил ESLint",
    "main": "index.js",
    "license": "MIT",
    "keywords": [
        "eslint",
        "eslint-plugin",
        "lint",
        "javascript"
    ],
    "peerDependencies": {
        "eslint": "^9.0.0"
    }
}

Особое внимание следует уделять разделу peerDependencies.

Он показывает, с какими версиями ESLint протестирован плагин.


Поддержка Flat Config

Начиная с ESLint 9 основной системой конфигурации становится Flat Config.

Плагин может предоставлять готовую конфигурацию для нового формата.

Пример:

const noConsoleLog = require("./rules/no-console-log");

module.exports = {
    rules: {
        "no-console-log": noConsoleLog
    },

    configs: {
        recommended: {
            plugins: {
                myplugin: {
                    rules: {
                        "no-console-log": noConsoleLog
                    }
                }
            },

            rules: {
                "myplugin/no-console-log": "error"
            }
        }
    }
};

Использование:

const myplugin = require("eslint-plugin-myplugin");

module.exports = [
    myplugin.configs.recommended
];

Поддержка Flat Config становится обязательным требованием для современных ESLint-плагинов.


Документирование правил

Каждое опубликованное правило должно сопровождаться документацией.

Обычно создаётся отдельный каталог:

docs/
├── no-console-log.md
├── require-foo.md

Типичная структура документа:

# no-console-log

Запрещает использование console.log.

## Неправильно

```js
console.log("debug");

Правильно

logger.info("debug");

Документация должна содержать:

- назначение правила;
- мотивацию использования;
- примеры корректного кода;
- примеры некорректного кода;
- описание параметров настройки;
- информацию об автоматическом исправлении.

---

### Связь документации с правилом

Поле `docs` внутри `meta` используется для описания правила.

Пример:

```javascript
meta: {
    type: "problem",

    docs: {
        description: "Запрещает использование console.log",
        recommended: true
    },

    schema: []
}

В старых версиях ESLint некоторые инструменты автоматически извлекали данные из этого объекта для генерации документации.

Даже если автоматическая генерация не используется, наличие заполненного блока docs считается хорошей практикой.


Проверка плагина перед публикацией

Перед публикацией необходимо проверить несколько аспектов:

  1. Корректность импорта правил.
  2. Отсутствие синтаксических ошибок.
  3. Работу рекомендованной конфигурации.
  4. Совместимость с целевой версией ESLint.
  5. Корректность сообщений диагностики.
  6. Работу автоматических исправлений.
  7. Корректность документации.

Локальная проверка может выполняться через тестовый проект:

npm pack

Затем:

npm install ../eslint-plugin-myplugin-1.0.0.tgz

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


Публикация в npm

Авторизация:

npm login

Проверка содержимого пакета:

npm pack

Публикация:

npm publish

Для scoped-пакетов может потребоваться:

npm publish --access public

После публикации пакет становится доступным через npm-реестр.

Установка пользователями:

npm install eslint-plugin-myplugin --save-dev

Либо:

npm install @company/eslint-plugin-myplugin --save-dev

Версионирование правил

Изменения в правилах должны соответствовать принципам Semantic Versioning.

Patch (1.0.1)

Исправления ошибок:

1.0.0 → 1.0.1

Примеры:

  • исправление ложных срабатываний;
  • исправление ошибок автофикса;
  • исправление документации.

Minor (1.1.0)

Добавление новых возможностей без нарушения совместимости:

1.0.0 → 1.1.0

Примеры:

  • новое правило;
  • новые параметры существующего правила;
  • новая рекомендованная конфигурация.

Major (2.0.0)

Ломающие изменения:

1.0.0 → 2.0.0

Примеры:

  • изменение поведения правила;
  • удаление параметров;
  • изменение рекомендованных настроек;
  • отказ от поддержки старых версий ESLint.

Организация набора правил

По мере роста плагина количество правил может значительно увеличиваться.

Распространённая структура:

lib/
├── rules/
│   ├── best-practices/
│   ├── style/
│   ├── security/
│   └── performance/

Примеры групп:

  • правила качества кода;
  • правила стиля;
  • правила безопасности;
  • правила производительности;
  • правила архитектурных ограничений.

Для крупных проектов часто создаются несколько конфигураций:

configs: {
    recommended,
    strict,
    style,
    security
}

Это позволяет подключать только необходимые группы правил.


Автоматическое формирование экспорта правил

При большом количестве правил ручное перечисление становится неудобным.

Пример автоматической регистрации:

const fs = require("fs");
const path = require("path");

const rules = {};

for (const file of fs.readdirSync(__dirname)) {
    if (file === "index.js") {
        continue;
    }

    const name = path.basename(file, ".js");

    rules[name] = require(`./${file}`);
}

module.exports = rules;

Подобный подход уменьшает объём служебного кода и упрощает сопровождение крупных ESLint-плагинов.


Типичный экспорт полноценного плагина

const noConsoleLog = require("./lib/rules/no-console-log");
const requireFoo = require("./lib/rules/require-foo");

module.exports = {
    rules: {
        "no-console-log": noConsoleLog,
        "require-foo": requireFoo
    },

    configs: {
        recommended: {
            plugins: ["myplugin"],

            rules: {
                "myplugin/no-console-log": "error",
                "myplugin/require-foo": "warn"
            }
        }
    }
};

Такой пакет представляет собой полностью готовый ESLint-плагин, содержащий собственные правила, наборы конфигураций и инфраструктуру для распространения через npm.