В этом посте мы рассмотрим настройку lint-staged и husky для запуска pre-commit проверок. В этом посте дается много контекста, но фактические изменения кода очень незначительны!

Что такое проверки перед фиксацией?

Проверки перед фиксацией выполняются после постановки ваших изменений и выполнения git commit, но до завершения фиксации. Если проверки не проходят, то фиксация не выполняется и отображается ошибка, а если все проверки проходят, фиксация выполняется как обычно.

Зачем выполнять проверки перед фиксацией?

Проверки перед фиксацией обычно используются для запуска скриптов и тестов линтинга, что позволяет сделать каждую фиксацию максимально чистой. Как указано в документации, созданной с помощью линта, они предотвращают «попадание в вашу базу кода!».

Я работал с репозиториями как с проверками перед фиксацией, так и без них, и только недавно я оценил их ценность. Они часто казались раздражающими, прерывая фиксацию «wip» путем выхода из правила стиля и предотвращая выполнение фиксации без исправления ошибки или использования флага фиксации --no-verify.

Сочетание двух вещей изменило мое мнение по этому поводу.

Во-первых, работа с репозиторием, который приближается к 20-минутному конвейеру непрерывной интеграции. Этот конвейер запускается при каждом запросе push и pull и содержит этапы установки, сборки, lint и несколько этапов тестирования. Простой отказ от ворса (например, неиспользованный импорт) через 15 минут на удивление отличается по своему психологическому эффекту, чем через 5 минут.

Это получило дополнительную ясность после того, как мне порекомендовали выступление Пола Армстронга на тему React Europe 2019 Двигайтесь быстро с уверенностью. Я очень рекомендую весь доклад, но связанная временная метка охватывает те моменты, которые он высказывает в отношении негативного эффекта переключения контекста. Он подчеркивает, что за время, необходимое для запуска и отказа CI, разработчик часто переходит к размышлениям о другой работе и несет расходы на переключение контекста с нее в случае отказа CI. Это также влияет на рецензента PR, если он начал проверку, только чтобы затем CI потерпел неудачу во время проверки.

Поэтому следует приложить усилия к тому, чтобы выполнить простые проверки как можно быстрее, а перехватчики перед фиксацией могут использоваться для обнаружения сбоев еще до того, как они будут зафиксированы. Дополнительные несколько секунд на фиксацию намного перевешивают потерянное время сбоя CI через 15 минут, определение и устранение причины, а затем наблюдение за следующим 20-минутным запуском, чтобы убедиться, что он не потерпит неудачу снова.

Также подчеркивается, что мы должны доверять нашим инструментам. Если они могут исправить ошибки линтинга за нас, тогда мы должны опираться на это (в конце концов, PR все равно будет проверяться людьми) и сэкономить как можно больше времени.

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

Настраивать

Мы рассмотрим две разные структуры репозитория. Один имеет один пакет с package.json на верхнем уровне, а другой - несколько пакетов с корневым package.json, а затем по одному в каждом пакете под /packages/package-name/package.json. В репозитории с несколькими пакетами у нас есть одна конфигурация линтинга, настроенная в корневом пакете, с ее правилами, применяемыми ко всем подпакетам.

Делается предположение, что любые линтеры и тесты уже настроены. Здесь мы будем ссылаться на eslint (рекомендуется добавить более красивый плагин) и stylelint, но любые команды могут быть заменены линтерами по вашему выбору. Мы будем использовать npm, но опять же, если хотите, можно использовать yarn.

Для обеих структур репозитория мы захотим установить lint-staged и husky в наш корневой пакет.

npm install lint-staged husky --save-dev

На момент написания версии были [email protected] и [email protected].

Единый пакет

Примером структуры репо может быть:

|-- package.json
|-- README.md
|-- prettierrc.yml
|-- eslintrc
|-- stylelintrc.json
|-- .gitignore
|-- src
   |-- scripts.js
   |-- styles.scss

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

"scripts": {
    "lint:scss": "stylelint 'src/**/*.scss' --syntax scss",
    "lint:scss:fix": "stylelint 'src/**/*.scss' --syntax scss --fix",
    "lint:js": "eslint . --ext .js,.jsx",
    "lint:js:fix": "npm run lint:js -- --fix",
}

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

Требуемая здесь конфигурация проста. Это добавлено в корень package.json.

"lint-staged": {
  "src/**/*.{js,jsx}": [
    "eslint . --fix", "git add"
  ],
  "src/**/*.scss": [
    "stylelint --syntax scss --fix", "git add"
  ],
},
"husky": {
  "hooks": {
    "pre-commit": "lint-staged"
  }
},

Объект husky используется, чтобы указать, какой хук использовать, и что lint-staged должно быть запущено на нем.

Объект lint-staged используется для поиска поэтапных файлов, которые соответствуют шаблону micromatch в своем ключе. Затем для этих файлов запускается массив команд.

В этом примере мы смотрим только на файлы в каталоге /src, хотя при желании можем просмотреть все файлы.

Обратите внимание, что в сценарии NPM шаблон соответствия передается как флаг CLI, тогда как в конфигурации lint-staged он является ключом объекта.

Затем мы запускаем :fix версию каждой команды, что означает, что линтер попытается исправить любые найденные ошибки. Если это так, он внесет эти изменения, но они не будут поэтапными. git add затем поэтапно вносит изменения, что означает, что завершение lint-staged будет напрямую фиксировать любые исправления, внесенные в текущую фиксацию.

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

Мульти-пакет

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

|-- package.json
|-- README.md
|-- prettierrc.yml
|-- eslintrc
|-- stylelintrc.json
|-- .gitignore
|-- packages
   |-- package-one
     |-- src
       |-- package.json
       |-- scripts.js
       |-- styles.scss
   |-- package-two
     |-- src
       |-- package.json
       |-- scripts.js
       |-- styles.scss

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

Мы рассмотрим здесь только запуск eslint, чтобы код был более читабельным, но можно использовать те же команды stylelint, что и в предыдущем примере с одним пакетом.

Это можно настроить двумя способами: с помощью lerna или вручную. Мы не будем вдаваться в подробности, как lerna работает здесь, просто знайте, что это инструмент для работы в репозиториях с несколькими пакетами, где пакеты могут зависеть друг от друга. Это позволяет легко связывать и публиковать их. Одна из особенностей заключается в том, что запуск сценария

"lerna:lint:js": "lerna run lint:js:fix"

из корневого пакета приведет к запуску любого lint:js:fix сценария в любом подпакете.

Альтернативой достижению того же самого является более ручной, используя приведенный выше пример для того, что lerna делает в одном наборе, потребуются два сценария:

"lint:one:js": "npm run lint:js:fix --prefix packages/package-one",
"lint:two:js": "npm run lint:js:fix --prefix packages/package-two"

Для обоих вариантов для каждого подпакета потребуется следующее в своих индивидуальных package.json 'для lerna или--prefix для таргетинга.

"scripts": {
    "lint:js:fix": "npm run eslint . --ext .js,.jsx --fix",
}

Стандартная установка

per package

"scripts": {
    "lint:js:fix": "npm run eslint . --ext .js,.jsx --fix",
}

root

"scripts": {
  "lint:one:js": "npm run lint:js:fix --prefix packages/package-one",
  "lint:two:js": "npm run lint:js:fix --prefix packages/package-two"
}
"lint-staged": {
  "packages/package-one/**/*.{js,jsx}": [
    "npm run --silent lint:one:js", 
    "git add",
  ],
  "packages/package-two/**/*.{js,jsx}": [
    "npm run --silent lint:two:js",
    "git add",
  ],
},
"husky": {
  "hooks": {
    "pre-commit": "lint-staged"
  }
},

Лерна установка

per package

"scripts": {
    "lint:js:fix": "npm run eslint . --ext .js,.jsx --fix",
}

root

"scripts": {
  "lerna:lint:js": "lerna run lint:js:fix"
}
"lint-staged": {
  "packages/**/*.{js,jsx}": [
    "npm run --silent lerna:lint:js", 
    "git add",
  ]
},
"husky": {
  "hooks": {
    "pre-commit": "lint-staged"
  }
},

Отрицание файлов

Если есть определенные файлы, которые вы не хотите, чтобы линтер перебирал, то для этого можно использовать шаблон отрицания. Об этом стоит упомянуть, поскольку синтаксис, кажется, немного отличается от стандартного синтаксиса micromatch, как отмечено в этой проблеме отрицания.

Скажем, у нас есть flow-typed файл на корневом уровне, который мы не хотели включать, а также некоторые другие файлы сценариев. Этот шаблон можно использовать:

"!(packages|flow-typed)/**/*.{js}": [
  "npm run --silent lint:js -- --fix", "git add"
]

Здесь мы бы выделили отдельный шаг, чтобы просмотреть любые js файлы, не в /packages или /flow-typed.

Запуск тестов

В этих примерах мы рассмотрели только запуск линтеров, но также можно запускать тесты в том же хуке, ориентируясь только на файлы, которые изменили тесты с помощью jest --bail —-findRelatedTests. Вам решать, хотите ли вы это включить.

Это можно увидеть в шапке экрана, приведенной в упомянутом ранее выступлении Пол Армстронг« Двигайся быстро и уверенно ».

Последние мысли

Добавив объекты husky и lint-staged в свой package.json, вы можете быстро интегрировать pre-commit проверки в свой рабочий процесс, настроить их в соответствии со своими индивидуальными предпочтениями и сэкономить время для всех разработчиков, работающих в этом репозитории.

Перехватчики позволят находить, исправлять и добавлять общие ошибки без добавления какого-либо дополнительного взаимодействия - и все это до того, как будет запущен CI.

Спасибо за прочтение! 😀

Другие сообщения, которые я написал, включают: