Безболезненный линтинг CSS с помощью stylelint


Некоторые приемы, описанные в статье, ориентированы на использование PostCSS (в связке с css-modules и webpack), однако они с легкостью адаптируются под LESS / SASS / ванильный CSS.

Часть I. Кодстайл vs личные предпочтения. Делаем одинаково

Рано или поздно перед любой командой разработчиков встаёт вопрос о внедрении стандартов написания кода и подключении линтеров на проект. Стандартизация кода позволяет:

  • быстрее проводить code review, так как полностью убирается необходимость следить за общим форматированием и распространёнными ошибками (благодаря --fix флагу);
  • упростить работу с компонентами других членов команды — за счёт одинакового порядка свойств/селекторов вы всегда знаете, в каком именно месте искать проблемное свойство/селектор при рефакторинге/поддержке;
  • быстрее вводить в проект новых разработчиков, так как полностью пропадет необходимость объяснять все договоренности о кодстайле. Здесь стоит отметить, что в идеале все ваши стандарты написания кода должны проверяться линтером, если линтер не может проверить какой-либо кейс — не вводите такое требование к коду.

С другой стороны, у каждого разработчика есть свой собственный стиль, к которому он привык. Вспомните, как вы обычно пишете стили — скорее всего, правила добавляются в рандомном порядке. И если стандарты на проекте отличаются от личных предпочтений, а линтер настроен так, что даже локальная сборка не будет запускаться/пересобираться при наличии ошибок линтера — эффективность разработки падает в разы, огромное количество времени уходит на то, чтобы привести код в соответствие стандартам, и это больно. Очень больно.

Часть II. Общее форматирование. Делаем красиво

Под общим форматированием я имею в виду все правила stylelint, отвечающие непосредственно за внешний вид кода и никак не влияющие на его работу. Разделять ли селекторы пустой строкой, переносить каждое правило на новую строку или писать все правила в одну линию? Отделять ли открывающую фигурную скобку селектора пробелом, писать hex-цвета в сокращённом или длинном виде, оставлять или опускать ведущие нули у чисел? Всё это (и еще многое другое) относится только к форматированию и на работу ваших стилей никак не влияет.

Все пишут по-разному

Взгляните на примеры ниже — на каждом из них реальный, работающий код, хотя манеры его написания очень различаются:

.main-info{
    padding: 0;
    padding-top: 21px;
    margin: 0;
    border: none;
    margin-bottom: 50px;
    &__field{
        display: flex;
        align-items: flex-end;
        margin-bottom: 19px;
    }
    &__field-name{
        color: $grey;
        font-size: 22px;
        font-weight: 700;
        margin-right: 18px;
    }
}
.root {
  padding-top: 60px;
}

.title {
  margin: 0 auto 40px;
  width: 170px;
  height: 70px;
  color: transparent;
  background-image: url('./images/title.svg');
  background-size: contain;
  user-select: none;

  @media (--tablet-large) {
    margin: 0 auto 60px;
    width: 328px;
    height: 136px;
  }
}
body
	font-family PTSansBold
.wrap
	width 1000px
	margin 0 auto
	background-image url(../images/background.png)
.wrap__header
	display flex
	justify-content space-between
.wrap__logo
	padding 62px 0 0 112px
.wrap__secret
	padding 48px 54px 0 0
.wrap__content
	padding 0 112px 73px 112px

Абсолютно разные подходы к форматированию стилевых файлов

Конечно же, использование определённых пре- или постпроцессоров накладывает свои ограничения, но некоторые из нюансов форматирования остаются всегда. Но представьте себе, что творилось бы на проекте без линтера, если бы на него одновременно пришли авторы трёх кодов выше. Боль.

Линтим форматирование

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

Вы разрабатываете свой собственный кодстайл и прописываете правила линтера в соответствии с этими требованиями. В этом случае ваш .stylelintrc может выглядеть примерно так:

{
    "rules": {
        "indentation": 2,
        "string-quotes": "single",
        "declaration-colon-space-before": "never",
        "declaration-colon-space-after": "never",
        "rule-empty-line-before": "never",
        "media-feature-range-operator-space-before": "never",
        "media-feature-range-operator-space-after": "never",
        "media-feature-colon-space-before": "never",
        "media-feature-colon-space-after": "never"
    }
}

Либо вы можете взять уже готовый кодстайл, написанный какой-либо известной компанией,

npm install stylelint-config-airbnb --save-dev

и использовать его в виде расширения конфигурации линтера. .stylelintrc в таком случае приобретёт следующий вид:

{
  "extends": "stylelint-config-airbnb"
}

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

image

Результат работы линтера

Prettier

В моей команде выбор был сделан в пользу кодстайла Prettier. На самом деле его конфиг для stylelint не добавляет никаких правил, а наоборот, отключает бОльшую их часть. Сделано это для того, чтобы отключить все пересекающиеся правила из предыдущих конфигов (конфиг Prettier должен подключаться последним), дабы они не конфликтовали с логикой форматирования, заложенной в Prettier.

Как следствие отключения правил линтера — в текстовом редакторе пропадают предупреждения об ошибках форматирования, так как stylelint вообще перестаёт наблюдать за этими правилами. Это требует от разработчиков не забывать запускать Prettier либо через CLI, либо через плагин для текстового редактора (вот такой, к примеру, для VS Code).

Часть III. Порядок свойств внутри селектора. Делаем структурированно

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

Если все ваши свойства сгруппированы по логическому смыслу и отсортированы внутри этих групп, код становится более структурированным. Преимущества такой сортировки:

  • более быстрое нахождение определённых свойств (например, вы всегда уверены, что position идёт самым первым свойством, а margin всегда идут до padding);
  • внедрение новых фич не снижает качество кода (как бы вы ни были организованы в плане написания кода на первоначальном этапе разработки, чаще всего, к моменту выхода проекта на стадию поддержки эта организованность значительно снижается);
  • упрощается работа с кодом, который писали не вы, а другой разработчик команды.

Реализация сортировки

Для достижения поставленной цели в виде сгруппированных и отсортированных свойств следует воспользоваться плагином для stylelint.

npm install stylelint-order --save-dev

После установки плагина достаточно добавить его в список плагинов, а также указать нужный порядок свойств в .stylelintrc

{
  "plugins": [
    "stylelint-order",
  ],
  "rules": {
    "order/properties-order": [
      "position",
      "z-index",
      "top",
      "right",
      "bottom",
      "left",
    ],
  },
}

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

"order/properties-order": [
  [
    "position",
    "z-index",
  ],
  {
    unspecified: "bottom"
  }
]

image

Результат работы линтера

Часть IV. Очерёдность в «нестинге». Снижаем риски и повышаем читаемость

«Нестинг», или вложенность — крайне удобный и мощный инструмент, в который умеют все пре- и постпроцессоры. Однако неумелое его использование открывает возможность выстрелить себе в ногу (чаще всего, из-за незнания того, как работает специфичность селекторов).

Если же говорить про кодстайл, то при командной разработке вы наверняка хотели бы иметь возможность управлять очерёдностью в нестинге. Ведь код, в котором очерёдность ‘БЭМ модификатор — media — псевдоклассы — псевдоэлементы’, будет ОЧЕНЬ сильно отличаться от кода с обратной очерёдностью.

image

Различные варианты сортировки в нестинге

Так же как и одинаковый порядок свойств в селекторе, одинаковая очерёдность в нестинге позволяет облегчить работу с кодом, который писал другой разработчик команды, так как вы всегда уверены, в каком именно месте искать определенные вложенные правила. Плюс это страхует от ошибок при поддержке кода, когда, к примеру, к уже существующим media добавляется ещё одно, но в неправильной очерёдности. Добавьте к этому правило о максимальной глубине вложенности и запрет на любые селекторы внутри media (т.е. чтобы изменить эффект при наведении на планшетах, вы обязываете своих разработчиков вкладывать media в hover, а не наоборот) — и на выходе получите очень мощный инструмент стандартизации кода.

Реализация сортировки

Данная сортировка достигается за счёт использования упомянутого выше плагина stylelint-order, возможный конфиг .stylelintrc может выглядеть так:

{
  "plugins": [
    "stylelint-order",
  ],
  "rules": {
    "order/order": [
      "declarations",
      {
        "type": "at-rule",
        "name": "media"
      },
      {
        "type": "rule",
        "selector": "^&::(before|after)"
      },
      {
        "type": "rule",
        "selector": "^&:\\w"
      },
      {
        "type": "rule",
        "selector": "^&_"
      },
      {
        "type": "rule",
        "selector": "^."
      }
    ],
  },
}

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

image

Результат работы линтера

Часть V. Не форматированием единым. Заботимся о качестве

Как вы могли заметить, всё, о чём шла речь выше, касалось лишь форматирования кода, работал он при этом абсолютно одинаково. Но помимо соглашений о форматировании любой кодстайл включает себя также и соглашения о качестве кода.

Хотите запретить использование тегов или id в селекторах? Застраховаться от дублирования правил внутри селектора или дублирования самих селекторов? Запретить !important, ограничить максимальную глубину нестинга, запретить использование em или px? А может, запретить целые правила, вроде сокращённой записи margin/padding/flex? stylelint предоставляет огромное количество правил, отвечающих именно за такие ситуации. За счет их использования внутри команды достигается некое единообразие в самой логике написания стилевых файлов.

Помимо стандартных правил stylelint также стоит обратить внимание на плагины, расширяющие возможности проверки кода — например, можно запретить использование media без custom-media или запретить вкладывать селекторы внутрь media.

Использование таких правил очень сильно зависит от договоренностей внутри команды. Главный посыл здесь должен быть «не навреди» — обсуждайте внедрение правил с командой, ставьте их под сомнение, разбирайте все плюсы и минусы их использования. Хорошим маркером для подключения нового правила (или расширения уже подключенного) является момент, когда при прохождении code review ошибка встречается более двух раз, т.е. в первый раз разработчик поправил ошибку после замечания, но через некоторое время снова отправил на проверку код с такой же ошибкой.

image

«Качественные» ошибки линтера

Часть VI. Автоматизация процесса. Убираем боль

Итак, вы потратили достаточно времени на составление добротного конфига stylelint, который покрывает все требуемые вашим кодстайлом моменты — общее форматирование, различные сортировки правил, а также «качественные» нюансы. Следующий вопрос, который вам предстоит решить — «в какой момент запускать проверку линтера, и что должно происходить при наличии ошибок в этой проверке?». Этот вопрос имеет огромное значение.

Представьте себе такую ситуацию — ваша система сборки (gulp/webpack/etc.) настроена таким образом, что проверка линтера запускается на каждое сохранение файла и при наличии ошибок не даёт обновить локальный сервер, т.е. вы не сможете увидеть результат внесённых изменений. Неважно, на какой стадии находится проект — на начальном этапе разработки самых мелких реиспользуемых компонент или же на этапе поддержки, когда код вносится в уже функционирующую систему с огромным количеством файлов и строк кода. Вы не увидите ни одного изменения до тех пор, пока не расставите все написанные правила в правильном порядке и не соблюдёте все бест-практики, предписанные кодстайлом. Боль.

Настроив систему сборки и/или сам линтер так, чтобы ошибки общего форматирования и сортировки не препятствовали обновлению локального сервера (к примеру, за счёт смены отображения ошибок в правилах на предупреждения), вы снизите боль, но не уберёте её полностью. Ведь большинство разработчиков, которые имеют хоть какой-то опыт работы на проектах с линтерами, пользуются плагинами для своих текстовых редакторов/IDE, чтобы ошибки и предупреждения линтеров подсвечивались в коде. Все эти подчёркивания и всплывающие попапы знатно мозолят глаза и прилично отвлекают от основной задачи — написания кода. Слёз перфекционистов вообще не счесть.

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

Разделяй и властвуй

Скорее всего, вы заметили, что на приведённых выше в статье скриншотах, где был показан результат работы линтера, его запуск осуществлялся с флагом --fix. В результате этого все ошибки исправлялись автоматически. И напротив, ошибки правил, о которых шла речь в V части статьи, невозможно исправить автоматически, для их решения необходимо непосредственное вмешательство разработчика.

Помимо --fix флага существует ещё и такой флаг как --config, который позволяет указать путь к определённому конфигу, а не использовать для проверки дефолтный конфиг. Такая возможность позволяет разделить правила для нашего кодстайла на два конфига:

  • Первый (он же дефолтный, к примеру .stylelintrc) будет содержать только те правила, для исправления ошибок которых необходимо вмешательство разработчика (не работает автоисправление). Именно этот конфиг будет подгружаться в плагины текстовых редакторов / IDE для проверки кода в режиме реального времени. Это обеспечит следование «качественным» критериям кодстайла, не отвлекая разработчиков на вопросы общего форматирования/сортировок.
  • Второй конфиг (к примеру, .stylelintrc-extended) будет содержать все правила из первого, а также будет дополнен правилами для форматирования/сортировок. Вызов такого конфига будет всегда сопровождаться флагом --fix, что обеспечит следующую логику: линтер самостоятельно исправит все ошибки, которые сможет, а затем проверит код на соответствие качественным критериям кодстайла. Прохождение такой проверки без ошибок будет сигналом о том, что код полностью соответствует кодстайлу и его можно отправлять в репозиторий проекта.

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

Автоматизация запуска линтера

Как уже упоминалось выше, есть как минимум три способа запуска линтера, обеспечивающие автоматизацию процесса проверки.

Первый — запуск линтера в момент сохранения файлов. Этого можно достичь за счёт настройки вашей системы сборки (так — для webpack и вот так — для gulp). Лично мне не нравится этот подход по ряду причин:

  • из-за сортировок код перемешивается прямо у вас на глазах, что вызывает дискомфорт, т.к. теряется положение курсора на последнем месте изменения в редакторе, а также нарушается порядок действий для cmd/ctrl + z команды;
  • повышается задержка между сохранением файла и реальным отображением правок в браузере (hot reload), а ведь сохранений в течение рабочего дня может быть ОЧЕНЬ много;
  • нет гарантий того, что код с ошибками не попадёт в репозиторий.

Второй — запуск линтера перед созданием коммита (т.к. в репозитории хранится вся история коммитов, мы не можем полагаться именно на момент отправки кода в него). Преимущества подхода:

  • линтер не вмешивается в процесс активной разработки, автоисправления будут применены, только когда работа с кодом завершена;
  • количество запускаемых проверок снижается в разы, что повышает общую производительность процесса разработки;
  • код с ошибками линтера не может попасть в репозиторий, так как наличие ошибок при проверке просто не даст создать новый коммит (конечно же, флаг --no-verify никто не отменял, но за его использование следует бить по рукам).

Именно реализацию такого подхода мы и рассмотрим далее. Заключается она в использовании прекоммит хука, запускающего проверку линтера (с расширенным конфигом и флагом --fix) с помощью husky и lint-staged.

Третий способ заключается в использовании CI (Continuous integration), проверка линтером в таком случае может быть проведена автоматически для каждого отдельного Pull/Merge Request. Такой подход обеспечивает чистоту и порядок в стабильной ветке проекта без необходимости запуска проверки для каждого отдельного коммита в feature-ветках.

husky + lint-staged + prettier = ❤️❤️❤️

husky — инструмент, позволяющий использовать различные гит хуки, в том числе и прекоммит хук.

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

prettier — форматтер кода, который будет отвечать за следование правилам общего форматирования (о них шла речь во второй части статьи) нашего кодстайла.

Перейдём к настройке этих инструментов для работы в связке. Первым делом установим зависимости:

npm install prettier husky lint-staged stylelint-order stylelint-config-prettier stylelint-config-recommended --save-dev

Далее нужно настроить ваш .package.json следующим образом:

{
  "devDependencies": {
    "husky": "^1.2.0",
    "lint-staged": "^8.1.0",
    "prettier": "^1.15.2",
    "stylelint": "^9.8.0",
    "stylelint-config-prettier": "^4.0.0",
    "stylelint-config-recommended": "^2.1.0",
    "stylelint-order": "^2.0.0"
  },
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    "linters": {
      "*.css": [
        "prettier --write",
        "stylelint --fix --config ./.stylelintrc-format",
        "git add"
      ]
    }
  }
}

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

{
  "singleQuote": true,
  "tabWidth": 4
}

Также убедитесь в том, что вы передаете нужный конфиг в строке с вызовом линтера (скорее всего, это будет не дефолтный .stylelintrc).

Итоговый результат

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

  • базовый конфиг .stylelintrc включает в себя только «качественные» правила, плагины для текстовых редакторов / IDE используют именно его;
  • на этапе локальной работы с кодом проверки линтера не запускаются и не препятствуют разработке;
  • в момент создания коммита для файлов, которые находятся в «staged» состоянии, сначала отработает prettier, пофиксив общее форматирование. Затем запустится stylelint с расширенным конфигом и --fix флагом, пофиксив сортировки правил и нестинга. Далее проводится проверка «качественных» правил, и в случае наличия ошибок коммит не будет создан. Если же ошибок нет, то все изменения, внесённые в результате автоисправлений, будут автоматически добавлены в «staged» состояние и коммит будет создан.

Допустим, что ваш итоговый конфиг имеет следующий вид:

// .stylelintrc
{
  "rules": {
    "declaration-block-no-imortant": true,
    "property-blacklist": ["flex"],
    "unit-blacklist": ["em", "rem"],
  },
}
// .stylelint-format
{
  "extends": [
    "stylelint-config-recommended",
    "stylelint-config-prettier",
  ],
  "plugins": [
    "stylelint-order",
  ],
  "rules": {
    "declaration-block-no-imortant": true,
    "indentation": 2,
    "rule-empty-line-before": "always",
    "at-rule-empty-line-before": "always",
    "property-blacklist": ["flex"],
    "unit-blacklist": ["em", "rem"],
    "order/order": [
      "declarations",
      {
        "type": "at-rule",
        "name": "media",
      },
      {
        "type": "rule",
        "selector": "^&:\\w"
      },
      {
        "type": "rule",
        "selector": "^&_"
      },
    ],
    "order/properties-order": [
      [
        "position",
        "top",
        "right",
        "bottom",
        "left",
      ],
      {
        unspecified: "bottom",
      }
    ],
  },
}
// .package.json
{
  "devDependencies": {
    "husky": "^1.1.3",
    "lint-staged": "^8.0.4",
    "prettier": "^1.15.2",
    "stylelint": "^9.8.0",
    "stylelint-config-prettier": "^4.0.0",
    "stylelint-config-recommended": "^2.1.0",
    "stylelint-order": "^1.0.0"
  },
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    "linters": {
      "*.css": [
        "prettier --write",
        "stylelint --fix --config ./.stylelintrc-format",
        "git add"
      ]
    }
  }
}

Тогда работа линтера будет выглядеть так (обратите внимание, что в staged состоянии находится только один стилевой файл):

image

Попытка коммита вызывает прекоммит хук и автоисправление форматирования. Линтер падает с ошибками на «качественных» правилах, внесённые prettier изменения форматирования при этом сохраняются

image

Пример успешного коммита без ошибок в «качественных» правилах

Заключение

С каждым днем stylelint становится всё мощнее, дополняясь новыми правилами и возможностями для автоматических исправлений. Так что если вы ещё не используете данный инструмент, я очень надеюсь, что после прочтения этой статьи вы хотя бы попробуете внедрить его на свои проекты. Также некоторые подходы, описанные в статье, применимы к линтингу не только стилевых файлов, но и, к примеру, JSX файлов, но это уже совсем другая история…

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

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

Спасибо за внимание!

comments powered by Disqus