Lint, Format и Git Hooks

Linting, formatting и Git hooks помогают автоматически поддерживать качество и единообразие кода.

Они решают разные задачи:

Инструмент Назначение
ESLint Находит потенциальные ошибки и нарушения правил JavaScript
Prettier Автоматически форматирует код
EditorConfig Согласует базовые настройки редакторов
Husky Позволяет хранить и настраивать Git hooks в проекте
lint-staged Запускает команды только для подготовленных к коммиту файлов
commitlint Проверяет формат сообщения коммита
Conventional Commits Определяет единый формат сообщений коммитов

Типичный процесс:

Разработчик изменяет файлы
          ↓
Prettier форматирует код
          ↓
ESLint проверяет и исправляет код
          ↓
git commit
          ↓
pre-commit запускает lint-staged
          ↓
commit-msg проверяет сообщение коммита
          ↓
Git создаёт коммит

Линтер и форматтер не заменяют тестирование:

ESLint     — статический анализ кода
Prettier   — внешний вид кода
Tests      — проверка поведения программы
TypeScript — проверка типов

Содержание


ESLint

ESLint — инструмент статического анализа JavaScript-кода.

Он может находить:

Пример:

function calculateTotal(items) {
  let result = 0;

  for (const item of items) {
    result += item.price;
  }

  return reslt;
}

Переменная reslt не объявлена. ESLint сообщит об ошибке:

'reslt' is not defined  no-undef

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


Установка ESLint

Для npm-проекта:

npm install --save-dev eslint @eslint/js globals

Сокращённая запись:

npm i -D eslint @eslint/js globals

Проверка версии:

npx eslint --version

Команда инициализации:

npm init @eslint/config

Она задаёт несколько вопросов и создаёт конфигурацию с учётом типа проекта.


Flat config ESLint

Современная конфигурация ESLint называется flat config.

Обычно она хранится в одном из файлов:

eslint.config.js
eslint.config.mjs
eslint.config.cjs

Если проект не использует ES-модули, удобно создать:

eslint.config.mjs

Пример базовой конфигурации для браузерного JavaScript:

import js from "@eslint/js";
import globals from "globals";

export default [
  {
    ignores: [
      "dist/**",
      "build/**",
      "coverage/**",
      "node_modules/**"
    ]
  },

  js.configs.recommended,

  {
    files: ["src/**/*.{js,mjs}"],

    languageOptions: {
      ecmaVersion: "latest",
      sourceType: "module",

      globals: {
        ...globals.browser
      }
    },

    rules: {
      "no-console": "warn",
      "no-debugger": "error",
      "no-var": "error",
      "prefer-const": "error",
      "eqeqeq": ["error", "always"]
    }
  }
];

Проверка проекта:

npx eslint .

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

npx eslint . --fix

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


Структура конфигурации ESLint

Flat config экспортирует массив конфигурационных объектов:

export default [
  {
    // Общая конфигурация
  },
  {
    // Конфигурация для определённых файлов
  }
];

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

files

Определяет файлы, для которых действует конфигурация:

{
  files: ["src/**/*.js"]
}

Несколько расширений:

{
  files: ["src/**/*.{js,mjs,cjs}"]
}

Конфигурация для тестов:

{
  files: ["tests/**/*.js", "**/*.test.js"]
}

ignores

Исключает файлы и каталоги из проверки:

{
  ignores: [
    "dist/**",
    "coverage/**",
    "public/vendor/**",
    "**/*.min.js"
  ]
}

Обычно не нужно проверять:

languageOptions

Настраивает синтаксис и окружение:

{
  languageOptions: {
    ecmaVersion: "latest",
    sourceType: "module"
  }
}

Значения sourceType:

module     — ES-модули с import и export
commonjs   — CommonJS с require и module.exports
script     — обычный скрипт

Глобальные переменные

Для браузерного проекта:

import globals from "globals";

export default [
  {
    languageOptions: {
      globals: {
        ...globals.browser
      }
    }
  }
];

После этого ESLint распознает:

window
document
navigator
location
localStorage

Для Node.js:

{
  files: ["scripts/**/*.js"],

  languageOptions: {
    globals: {
      ...globals.node
    }
  }
}

ESLint будет распознавать:

process
Buffer
__dirname
setImmediate

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

{
  files: ["**/*.test.js"],

  languageOptions: {
    globals: {
      ...globals.jest
    }
  }
}

Набор должен соответствовать реальному тестовому инструменту.


Правила ESLint

Правила задаются в объекте rules:

{
  rules: {
    "no-debugger": "error",
    "no-console": "warn",
    "prefer-const": "error"
  }
}

У каждого правила есть уровень строгости:

Значение Число Поведение
"off" 0 Правило отключено
"warn" 1 Показывается предупреждение
"error" 2 Показывается ошибка

Примеры эквивалентных записей:

{
  rules: {
    "no-console": "warn",
    "no-debugger": 2
  }
}

Предпочтительнее использовать строковые значения: они понятнее при чтении конфигурации.

Правила с параметрами

Некоторые правила принимают дополнительные настройки:

{
  rules: {
    "eqeqeq": ["error", "always"],
    "quotes": ["error", "double"],
    "semi": ["error", "always"]
  }
}

Первый элемент массива задаёт уровень, остальные — параметры правила.

eqeqeq

Требует строгого сравнения:

{
  rules: {
    "eqeqeq": ["error", "always"]
  }
}

Нежелательно:

if (value == 10) {
  console.log("Равно");
}

Предпочтительно:

if (value === 10) {
  console.log("Равно");
}

no-var

Запрещает var:

{
  rules: {
    "no-var": "error"
  }
}

Нежелательно:

var userName = "Анна";

Предпочтительно:

const userName = "Анна";

prefer-const

Требует const, если переменная не переназначается:

{
  rules: {
    "prefer-const": "error"
  }
}

Нежелательно:

let userName = "Анна";
console.log(userName);

Предпочтительно:

const userName = "Анна";
console.log(userName);

no-unused-vars

Находит неиспользуемые переменные:

{
  rules: {
    "no-unused-vars": [
      "error",
      {
        "argsIgnorePattern": "^_",
        "varsIgnorePattern": "^_"
      }
    ]
  }
}

Параметр разрешает намеренно неиспользуемые имена с _:

function handleError(_error, request, response) {
  response.status(500).send("Ошибка");
}

Такое соглашение должно быть единым для проекта.

no-console

Ограничивает использование Console API:

{
  rules: {
    "no-console": "warn"
  }
}

Можно разрешить отдельные методы:

{
  rules: {
    "no-console": [
      "warn",
      {
        allow: ["warn", "error"]
      }
    ]
  }
}

В серверных приложениях console может быть допустим, хотя для рабочих систем обычно используют структурированный логгер.

no-debugger

Запрещает оставлять инструкцию debugger:

{
  rules: {
    "no-debugger": "error"
  }
}

Проблемный код:

function submitForm() {
  debugger;
  sendRequest();
}

Рекомендуемая конфигурация

Пакет @eslint/js предоставляет стандартные наборы правил:

import js from "@eslint/js";

export default [
  js.configs.recommended
];

Рекомендуемая конфигурация включает правила, направленные прежде всего на обнаружение возможных ошибок.

Её можно расширить:

import js from "@eslint/js";
import globals from "globals";

export default [
  {
    ignores: ["dist/**", "coverage/**"]
  },

  js.configs.recommended,

  {
    files: ["src/**/*.js"],

    languageOptions: {
      ecmaVersion: "latest",
      sourceType: "module",

      globals: {
        ...globals.browser
      }
    },

    rules: {
      "eqeqeq": ["error", "always"],
      "no-console": ["warn", { allow: ["warn", "error"] }],
      "no-debugger": "error",
      "no-var": "error",
      "prefer-const": "error"
    }
  }
];

Плагины ESLint

Плагины добавляют правила для определённых технологий.

Распространённые направления:

TypeScript
React
Vue
Node.js
импорты
тестовые фреймворки
доступность интерфейсов
безопасность

Правила плагинов обычно имеют префикс:

react/jsx-key
import/no-unresolved
@typescript-eslint/no-floating-promises

Плагин необходимо:

  1. установить;
  2. импортировать в конфигурацию;
  3. зарегистрировать или подключить его готовый набор;
  4. включить необходимые правила.

Не следует устанавливать множество плагинов без необходимости. Каждый дополнительный набор правил усложняет конфигурацию и может замедлять проверку.


Отключение правил ESLint

Правило можно временно отключить для одной строки:

console.log(user); // eslint-disable-line no-console

Или для следующей строки:

// eslint-disable-next-line no-console
console.log(user);

Для фрагмента:

/* eslint-disable no-console */

console.log("Начало");
console.log("Конец");

/* eslint-enable no-console */

Для всего файла:

/* eslint-disable no-console */

Отключение правила должно быть обоснованным. Не рекомендуется скрывать ошибки широким комментарием:

/* eslint-disable */

Лучше отключить только конкретное правило и объяснить причину:

// Библиотека требует запись диагностического сообщения в консоль.
// eslint-disable-next-line no-console
console.warn("Используется резервный режим");

Команды ESLint

Проверить весь проект:

npx eslint .

Проверить каталог:

npx eslint src

Проверить конкретный файл:

npx eslint src/app.js

Исправить автоматически:

npx eslint . --fix

Запретить предупреждения в CI:

npx eslint . --max-warnings=0

Вывести итоговую конфигурацию для файла:

npx eslint --print-config src/app.js

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


Prettier

Prettier — автоматический форматтер кода.

Он отвечает за внешний вид:

Prettier не предназначен для глубокого анализа логических ошибок.

Например, исходный код:

const user={id:42,name:"Анна",roles:["admin","editor"]}

После форматирования:

const user = {
  id: 42,
  name: "Анна",
  roles: ["admin", "editor"],
};

Установка Prettier

npm install --save-dev prettier

Проверка форматирования:

npx prettier . --check

Автоматическое форматирование:

npx prettier . --write

Форматирование одного файла:

npx prettier src/app.js --write

Конфигурация Prettier

Конфигурация может храниться в разных форматах:

.prettierrc
.prettierrc.json
.prettierrc.yml
prettier.config.js
prettier.config.mjs

Пример .prettierrc.json:

{
  "printWidth": 80,
  "tabWidth": 2,
  "useTabs": false,
  "semi": true,
  "singleQuote": false,
  "trailingComma": "all",
  "bracketSpacing": true,
  "arrowParens": "always",
  "endOfLine": "lf"
}

printWidth

Рекомендуемая длина строки:

{
  "printWidth": 80
}

Это не строгий запрет. Prettier может оставить более длинную строку, если безопасно перенести её невозможно.

tabWidth

Размер одного уровня отступа:

{
  "tabWidth": 2
}

useTabs

Использовать табуляцию вместо пробелов:

{
  "useTabs": false
}

semi

Добавлять точку с запятой:

{
  "semi": true
}

singleQuote

Использовать одинарные кавычки в JavaScript:

{
  "singleQuote": true
}

Настройка не изменяет требования JSON: в JSON ключи и строки всё равно записываются в двойных кавычках.

trailingComma

Добавлять завершающую запятую:

{
  "trailingComma": "all"
}

Пример:

const user = {
  id: 42,
  name: "Анна",
};

endOfLine

Стиль переноса строк:

{
  "endOfLine": "lf"
}

Это помогает избежать лишних изменений файлов между Windows, Linux и macOS.


.prettierignore

Файл .prettierignore исключает файлы из форматирования:

node_modules
dist
build
coverage
public/vendor
package-lock.json
*.min.js

Не следует без необходимости исключать исходный код проекта.

Некоторые сгенерированные файлы можно форматировать, но это увеличивает объём изменений и может конфликтовать с генератором.


Prettier и ESLint

ESLint и Prettier частично пересекаются в правилах оформления.

Например, ESLint может проверять:

кавычки
точки с запятой
отступы
переносы
запятые

Одновременно эти же части кода изменяет Prettier. Несогласованные настройки приводят к конфликту:

ESLint требует один вариант
Prettier записывает другой вариант

Для отключения конфликтующих правил ESLint используется пакет:

npm install --save-dev eslint-config-prettier

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

import js from "@eslint/js";
import eslintConfigPrettier from "eslint-config-prettier";

export default [
  js.configs.recommended,

  {
    rules: {
      "no-debugger": "error",
      "prefer-const": "error"
    }
  },

  eslintConfigPrettier
];

eslint-config-prettier не запускает Prettier. Он только отключает правила ESLint, конфликтующие с форматтером.

Рекомендуемое разделение:

ESLint  — качество и потенциальные ошибки
Prettier — форматирование

Проверка выполняется двумя командами:

npx eslint .
npx prettier . --check

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

npx eslint . --fix
npx prettier . --write

Форматирование в редакторе

Редактор можно настроить на автоматическое форматирование при сохранении.

Пример .vscode/settings.json:

{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit"
  }
}

Настройки в .vscode/settings.json относятся к Visual Studio Code. Пользователям других редакторов они не мешают.

Важно установить расширения:

ESLint
Prettier
EditorConfig

Если форматирование выполняется и Prettier, и встроенным форматтером редактора, результат может быть непредсказуемым. Для проекта лучше выбрать один основной форматтер.


EditorConfig

EditorConfig задаёт базовые правила редактирования файлов независимо от конкретного редактора.

Конфигурация хранится в файле:

.editorconfig

Она может определять:

Пример:

root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true

[*.md]
trim_trailing_whitespace = false

[Makefile]
indent_style = tab

root

root = true

Останавливает поиск других .editorconfig в родительских каталогах.

Без этого редактор может найти конфигурацию выше по файловой системе и объединить её с настройками проекта.

charset

charset = utf-8

Задаёт кодировку файлов.

end_of_line

end_of_line = lf

Использует переносы строк LF.

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

insert_final_newline

insert_final_newline = true

Добавляет перенос строки в конце файла.

indent_style

Пробелы:

indent_style = space

Табуляция:

indent_style = tab

indent_size

indent_size = 2

Задаёт размер уровня отступа.

trim_trailing_whitespace

trim_trailing_whitespace = true

Удаляет пробелы в конце строк.

Для Markdown иногда используют:

[*.md]
trim_trailing_whitespace = false

В Markdown два пробела в конце строки могут иметь специальное значение и создавать перенос.


EditorConfig и Prettier

EditorConfig и Prettier также частично пересекаются.

EditorConfig задаёт общие настройки редактора:

кодировка
отступы
переносы строк
завершающая строка
пробелы в конце строк

Prettier форматирует структуру поддерживаемых языков:

JavaScript
TypeScript
JSON
CSS
HTML
Markdown
YAML

Обычно используют оба инструмента:

EditorConfig — базовое поведение редактора
Prettier     — полное форматирование кода

Их настройки должны совпадать:

# .editorconfig
indent_size = 2
end_of_line = lf
{
  "tabWidth": 2,
  "endOfLine": "lf"
}

Git hooks

Git hook — скрипт, который Git запускает при определённом событии.

Локальные хуки находятся в каталоге:

.git/hooks

Примеры:

Hook Когда выполняется
pre-commit Перед созданием коммита
prepare-commit-msg Перед открытием редактора сообщения
commit-msg После получения сообщения, до создания коммита
post-commit После создания коммита
pre-push Перед отправкой коммитов в удалённый репозиторий

Обычный каталог .git/hooks не отслеживается Git. Поэтому для общей настройки команды используют Husky или другой менеджер hooks.


pre-commit

Hook pre-commit выполняется до создания коммита.

Подходящие проверки:

Пример логики:

npx lint-staged

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

pre-commit должен выполняться быстро. Если в нём запускать полную сборку и все интеграционные тесты, разработчики начнут обходить проверку.

Долгие проверки лучше выполнять:


commit-msg

Hook commit-msg проверяет сообщение коммита.

Git передаёт ему путь к временному файлу с сообщением:

npx --no -- commitlint --edit "$1"

Он позволяет запретить сообщения вида:

update
fix
changes
work
исправления

и потребовать структурированный формат:

feat(auth): add password reset

Если сообщение не соответствует правилам, коммит не создаётся.


Обход Git hooks

Git позволяет пропустить некоторые клиентские хуки:

git commit --no-verify

Сокращённый вариант:

git commit -n

Поэтому локальный hook не является абсолютной гарантией качества или безопасности.

Критичные проверки необходимо повторять в CI:

ESLint
Prettier --check
тесты
проверка типов
проверка сборки
commitlint при необходимости

Hook помогает разработчику получить быструю обратную связь, а CI обеспечивает единый обязательный контроль.


Husky

Husky упрощает хранение Git hooks внутри проекта.

Установка:

npm install --save-dev husky

Инициализация:

npx husky init

Обычно команда:

Пример:

{
  "scripts": {
    "prepare": "husky"
  }
}

После установки зависимостей npm выполняет prepare, и Husky настраивает hooks для локального репозитория.

В CI или архиве без каталога .git поведение установки Husky может потребовать отдельной настройки.


Настройка pre-commit в Husky

Файл:

.husky/pre-commit

Содержимое:

npx lint-staged

При попытке создать коммит Git запустит lint-staged.

Не рекомендуется без необходимости запускать ESLint по всему проекту:

npx eslint .

В большом проекте такая проверка может быть медленной. Для изменяемых файлов лучше использовать lint-staged, а полный проект проверять в CI.


lint-staged

lint-staged запускает команды только для файлов, добавленных в индекс Git.

Установка:

npm install --save-dev lint-staged

Файлы попадают в индекс после команды:

git add src/app.js

Состояние индекса можно проверить:

git status

lint-staged не должен форматировать весь проект при каждом коммите. Он выбирает только подготовленные файлы, подходящие под шаблоны.


Конфигурация lint-staged

Конфигурацию можно добавить в package.json:

{
  "lint-staged": {
    "*.{js,mjs,cjs}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{json,css,scss,html,md,yml,yaml}": [
      "prettier --write"
    ]
  }
}

Здесь для JavaScript сначала выполняется ESLint, затем Prettier.

Для остальных поддерживаемых форматов запускается только Prettier.

Отдельный файл конфигурации:

lint-staged.config.mjs
export default {
  "*.{js,mjs,cjs}": [
    "eslint --fix",
    "prettier --write"
  ],

  "*.{json,css,scss,html,md,yml,yaml}": [
    "prettier --write"
  ]
};

Запуск вручную:

npx lint-staged

lint-staged передаёт командам имена выбранных файлов. Поэтому не нужно добавлять точку:

Правильно:
eslint --fix

Нежелательно:
eslint . --fix

Второй вариант проверит весь проект, а не только staged-файлы.


Как lint-staged изменяет файлы

Допустим, разработчик выполнил:

git add src/app.js
git commit -m "fix: correct user validation"

Далее происходит:

Git запускает pre-commit
          ↓
Husky выполняет npx lint-staged
          ↓
lint-staged находит staged-файл src/app.js
          ↓
ESLint исправляет доступные ошибки
          ↓
Prettier форматирует файл
          ↓
Исправления добавляются в коммит
          ↓
Git продолжает создание коммита

Если исправить проблему автоматически нельзя, команда завершается ошибкой:

Коммит не создан

Разработчик должен исправить код, снова добавить файл и повторить коммит:

git add src/app.js
git commit -m "fix: correct user validation"

Частично подготовленные файлы

Один файл может содержать:

Пример:

git add -p src/app.js

lint-staged умеет работать с частично подготовленными файлами, временно скрывая неподготовленные изменения и затем восстанавливая их.

Несмотря на это, при сложных изменениях рекомендуется:

Просмотр подготовленных изменений:

git diff --staged

Conventional Commits

Conventional Commits — соглашение о формате сообщений коммитов.

Основной шаблон:

type(scope): description

Расширенный шаблон:

type(scope)!: description

body

footer

Пример:

feat(auth): add password reset

С областью без критического изменения:

fix(api): handle empty response

Без области:

docs: update installation guide

Типы Conventional Commits

feat

Новая функциональность:

feat(profile): add avatar upload

fix

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

fix(cart): prevent duplicate products

docs

Изменение документации:

docs: describe environment variables

style

Изменения оформления кода, не влияющие на его поведение:

style: format configuration files

style не означает изменение CSS-дизайна. Изменение внешнего вида интерфейса может быть feat или fix в зависимости от задачи.

refactor

Изменение структуры кода без добавления функции и без исправления пользовательской ошибки:

refactor(auth): extract token service

perf

Оптимизация производительности:

perf(search): cache normalized queries

test

Добавление или изменение тестов:

test(api): add user validation cases

build

Изменение сборки или зависимостей:

build: update bundler configuration

ci

Изменение CI-конфигурации:

ci: add lint job

chore

Служебное изменение:

chore: update development dependencies

Не следует использовать chore для любых изменений без разбора. Если изменение исправляет ошибку или добавляет функцию, лучше выбрать fix или feat.

revert

Отмена предыдущего изменения:

revert: remove experimental cache

Scope

scope уточняет часть проекта:

auth
api
cart
profile
config
deps
ui
database

Примеры:

feat(auth): add two-factor authentication
fix(api): return 404 for missing user
refactor(database): extract query builder
test(cart): cover empty cart behavior

Scope является необязательным, если правила проекта не требуют обратного.

Названия областей должны быть короткими и единообразными. Не рекомендуется одновременно использовать варианты:

auth
authentication
login
authorization

если они обозначают одну и ту же часть системы.


Description

Краткое описание идёт после двоеточия и пробела:

fix(api): handle expired access token

Хорошее описание отвечает на вопрос:

Что делает этот коммит?

Предпочтительно:

add password reset
handle empty API response
remove deprecated endpoint
update installation instructions

Менее информативно:

changes
update
fix bug
work
final version

Часто описание пишут:

Правила языка и формулировок определяет команда.


Тело коммита

Тело объясняет причину и детали изменения:

fix(cache): prevent stale user profile

Invalidate the profile cache after updating the user's email.
Previously, the old value remained visible until the cache expired.

Тело отделяется от заголовка пустой строкой.

В нём можно описать:

Не стоит пересказывать в теле каждую изменённую строку. Это уже видно в diff.


Breaking changes

Несовместимое изменение можно отметить символом !:

feat(api)!: remove deprecated user endpoint

Или футером:

feat(api): change user response format

BREAKING CHANGE: the `fullName` field was replaced by `firstName`
and `lastName`.

Можно использовать оба варианта:

feat(api)!: change user response format

BREAKING CHANGE: clients must read `firstName` and `lastName`
instead of `fullName`.

Breaking change означает, что потребителям потребуется изменить код, конфигурацию или способ использования API.


Связь Conventional Commits с версиями

Соглашение удобно использовать вместе с Semantic Versioning:

MAJOR.MINOR.PATCH

Типичное соответствие:

Коммит Изменение версии
fix PATCH
feat MINOR
BREAKING CHANGE MAJOR

Например:

1.4.2 → 1.4.3   fix
1.4.2 → 1.5.0   feat
1.4.2 → 2.0.0   breaking change

Автоматическое повышение версии зависит от настроек инструмента выпуска. Сам формат Conventional Commits не публикует релиз самостоятельно.


Проверка сообщений через commitlint

Для автоматической проверки Conventional Commits используется commitlint.

Установка:

npm install --save-dev \
  @commitlint/cli \
  @commitlint/config-conventional

Создайте файл:

commitlint.config.mjs

Содержимое:

export default {
  extends: ["@commitlint/config-conventional"]
};

Проверка сообщения вручную:

echo "feat(auth): add password reset" | npx commitlint

Некорректный пример:

echo "updated files" | npx commitlint

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


Настройка commit-msg в Husky

Создайте файл:

.husky/commit-msg

Содержимое:

npx --no -- commitlint --edit "$1"

После этого команда:

git commit -m "feat(auth): add login form"

пройдёт проверку.

Команда:

git commit -m "added login form"

будет отклонена.

Параметр $1 содержит путь к файлу, в котором Git сохранил сообщение коммита.


Настройка правил commitlint

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

export default {
  extends: ["@commitlint/config-conventional"],

  rules: {
    "header-max-length": [2, "always", 100],
    "subject-empty": [2, "never"],
    "type-empty": [2, "never"],
    "subject-full-stop": [2, "never", "."]
  }
};

Как и в ESLint, уровни задаются числами:

0 — отключено
1 — предупреждение
2 — ошибка

Ограничение разрешённых типов

export default {
  extends: ["@commitlint/config-conventional"],

  rules: {
    "type-enum": [
      2,
      "always",
      [
        "feat",
        "fix",
        "docs",
        "style",
        "refactor",
        "perf",
        "test",
        "build",
        "ci",
        "chore",
        "revert"
      ]
    ]
  }
};

Ограничение scope

export default {
  extends: ["@commitlint/config-conventional"],

  rules: {
    "scope-enum": [
      2,
      "always",
      [
        "api",
        "auth",
        "cart",
        "config",
        "profile",
        "ui"
      ]
    ]
  }
};

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


Полный пример настройки

Установите зависимости:

npm install --save-dev \
  eslint \
  @eslint/js \
  globals \
  prettier \
  eslint-config-prettier \
  husky \
  lint-staged \
  @commitlint/cli \
  @commitlint/config-conventional

package.json

{
  "name": "example-project",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "lint": "eslint .",
    "lint:fix": "eslint . --fix",
    "format": "prettier . --write",
    "format:check": "prettier . --check",
    "check": "npm run lint && npm run format:check",
    "prepare": "husky"
  },
  "lint-staged": {
    "*.{js,mjs,cjs}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{json,css,scss,html,md,yml,yaml}": [
      "prettier --write"
    ]
  }
}

eslint.config.mjs

import js from "@eslint/js";
import globals from "globals";
import eslintConfigPrettier from "eslint-config-prettier";

export default [
  {
    ignores: [
      "node_modules/**",
      "dist/**",
      "build/**",
      "coverage/**",
      "**/*.min.js"
    ]
  },

  js.configs.recommended,

  {
    files: ["src/**/*.{js,mjs}"],

    languageOptions: {
      ecmaVersion: "latest",
      sourceType: "module",

      globals: {
        ...globals.browser
      }
    },

    rules: {
      "eqeqeq": ["error", "always"],
      "no-console": [
        "warn",
        {
          allow: ["warn", "error"]
        }
      ],
      "no-debugger": "error",
      "no-var": "error",
      "prefer-const": "error",
      "no-unused-vars": [
        "error",
        {
          argsIgnorePattern: "^_",
          varsIgnorePattern: "^_"
        }
      ]
    }
  },

  {
    files: [
      "eslint.config.mjs",
      "commitlint.config.mjs",
      "lint-staged.config.mjs",
      "scripts/**/*.js"
    ],

    languageOptions: {
      globals: {
        ...globals.node
      }
    }
  },

  eslintConfigPrettier
];

.prettierrc.json

{
  "printWidth": 80,
  "tabWidth": 2,
  "useTabs": false,
  "semi": true,
  "singleQuote": false,
  "trailingComma": "all",
  "bracketSpacing": true,
  "arrowParens": "always",
  "endOfLine": "lf"
}

.prettierignore

node_modules
dist
build
coverage
public/vendor
package-lock.json
*.min.js

.editorconfig

root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true

[*.md]
trim_trailing_whitespace = false

[Makefile]
indent_style = tab

commitlint.config.mjs

export default {
  extends: ["@commitlint/config-conventional"],

  rules: {
    "header-max-length": [2, "always", 100],
    "subject-empty": [2, "never"],
    "type-empty": [2, "never"],
    "subject-full-stop": [2, "never", "."]
  }
};

.husky/pre-commit

npx lint-staged

.husky/commit-msg

npx --no -- commitlint --edit "$1"

Инициализация проекта

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

npm install

Если Husky ещё не инициализирован:

npx husky init

Проверьте, что в .husky/pre-commit осталась нужная команда:

npx lint-staged

Создайте .husky/commit-msg:

npx --no -- commitlint --edit "$1"

Проверьте ESLint:

npm run lint

Проверьте Prettier:

npm run format:check

Исправьте файлы:

npm run lint:fix
npm run format

Проверьте hooks:

git add .
git commit -m "chore: configure code quality tools"

Пример рабочего процесса

Разработчик изменяет файл:

const loadUser=async(id)=>{
const response=await fetch(`/api/users/${id}`)
return response.json()
}

Затем добавляет его в индекс:

git add src/api.js

Проверяет подготовленные изменения:

git diff --staged

Создаёт коммит:

git commit -m "feat(api): add user loading"

Hook pre-commit запускает:

lint-staged

ESLint и Prettier преобразуют код:

const loadUser = async (id) => {
  const response = await fetch(`/api/users/${id}`);
  return response.json();
};

После этого commit-msg проверяет:

feat(api): add user loading

Если обе проверки завершились успешно, Git создаёт коммит.


Проверки в CI

Локальные hooks можно обойти, поэтому основные проверки следует повторять в CI.

Минимальный набор команд:

npm ci
npm run lint
npm run format:check
npm test
npm run build

Для ESLint в CI полезно запретить предупреждения:

{
  "scripts": {
    "lint": "eslint . --max-warnings=0"
  }
}

Но такое правило имеет смысл только в том случае, если команда действительно считает каждое предупреждение блокирующей проблемой.

Обычно CI не должен выполнять:

prettier . --write

CI не сохраняет автоматические исправления в репозитории. Вместо этого используется проверка:

prettier . --check

Частые проблемы

ESLint и Prettier постоянно изменяют код друг после друга

Причина — конфликт правил форматирования.

Установите:

npm install --save-dev eslint-config-prettier

И подключите его последним:

export default [
  // Другие конфигурации
  eslintConfigPrettier
];

ESLint не распознаёт window или document

Не настроено браузерное окружение:

import globals from "globals";

export default [
  {
    languageOptions: {
      globals: {
        ...globals.browser
      }
    }
  }
];

ESLint не распознаёт process

Для соответствующих файлов добавьте Node.js globals:

{
  files: ["scripts/**/*.js"],

  languageOptions: {
    globals: {
      ...globals.node
    }
  }
}

Prettier форматирует ненужные файлы

Добавьте их в .prettierignore:

dist
coverage
public/vendor

Hook не запускается

Проверьте:

Повторная настройка:

npm run prepare

Проверка lint-staged:

npx lint-staged

lint-staged ничего не проверяет

lint-staged работает только с файлами, добавленными в индекс:

git add src/app.js
npx lint-staged

Проверьте состояние:

git status

Коммит отклоняется из-за сообщения

Проверьте формат:

type(scope): description

Правильно:

fix(auth): handle expired token

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

Fixed expired token

После форматирования коммит содержит неожиданные изменения

Проверьте индекс:

git diff --staged

При необходимости отмените добавление файла в индекс:

git restore --staged src/app.js

Команда убирает файл из staged-состояния, но обычно сохраняет изменения в рабочем каталоге.


Рекомендации по организации

Конфигурационные файлы следует хранить в репозитории:

.editorconfig
.prettierrc.json
.prettierignore
eslint.config.mjs
commitlint.config.mjs
.husky/pre-commit
.husky/commit-msg
package.json
package-lock.json

Версия Node.js также должна быть зафиксирована или задокументирована, например через:

.nvmrc
.node-version
engines в package.json

Пример:

{
  "engines": {
    "node": ">=22"
  }
}

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

Не следует добавлять в pre-commit слишком долгие операции. Оптимальное разделение:

pre-commit — быстрые проверки изменённых файлов
commit-msg — проверка сообщения коммита
pre-push — более длительные локальные проверки при необходимости
CI        — полный обязательный набор проверок

Краткая памятка

Установка инструментов:

npm install --save-dev \
  eslint \
  @eslint/js \
  globals \
  prettier \
  eslint-config-prettier \
  husky \
  lint-staged \
  @commitlint/cli \
  @commitlint/config-conventional

Основные команды:

npx eslint .
npx eslint . --fix

npx prettier . --check
npx prettier . --write

npx lint-staged

Скрипты package.json:

{
  "scripts": {
    "lint": "eslint .",
    "lint:fix": "eslint . --fix",
    "format": "prettier . --write",
    "format:check": "prettier . --check",
    "check": "npm run lint && npm run format:check",
    "prepare": "husky"
  }
}

Hook pre-commit:

npx lint-staged

Hook commit-msg:

npx --no -- commitlint --edit "$1"

Формат Conventional Commits:

type(scope): description

Примеры:

feat(auth): add password reset
fix(api): handle empty response
docs: update installation guide
refactor(profile): extract avatar service
test(cart): add empty cart test
chore: update development dependencies

Основные правила: