Как автоматизировать использование дизайн токенов с помощью Stylelint и PostCSS
Сегодня я хотел бы поделиться своим небольшим успехом в автоматизации. В какой-то момент я понял, что во время код ревью указываю разработчикам на одни и те же ошибки. Но, что ещё хуже, я сам время от времени допускаю эти ошибки. Сегодня расскажу об одной из таких проблем, которую я решил с помощью PostCSS + Stylelint, и о том, как я это сделал.
Статья будет полезна для разработчиков, которые уже используют или собираются использовать дизайн токены.
“Автоматизируй всё, что можешь автоматизировать”. Я уже не помню, где услышал эту фразу, но теперь она меня повсюду преследует и не даёт спокойно спать по ночам. Бесит вся рутина, и хочется, чтобы инструменты делали всю работу за меня.
До этого я автоматизировал:
- Именование файлов и папок с помощью плагина eslint-plugin-import
- Контроль импортов с помощью плагина import/no-restricted-paths
- Использование именованных импортов в JS
С автоматизацией использования дизайн токенов всё немного сложнее, и в пост не поместится, поэтому я решил написать статью.
Наш продукт является self-hosted решением, которое компании могут разворачивать самостоятельно на своих серверах, ну что-то типа на self-hosted гитлаба. Несколько лет назад, мы с командой подумали и решили, что пользователям было бы полезно, брендировать внешний вид нашего продукта.
Мы реализовали простенький UI, а наш дизайнер сел за чашечкой тёмного нефильтрованного кофе и унифицировал цвета, типографику и иконки. Для каждого свойства мы создали CSS переменные и, положа руку на сердце, поклялись больше не хардкодить эти значения. Все эти переменные называются дизайн-токенами и являются самыми маленькими, атомарными правилами дизайн системы.
Но время от времени, к сожалению, мы нарушаем нашу клятву. Иногда, мы не глядя копируем стили из фигмы, думаем, что исправим перед мёржем изменений и забываем, а новые разработчики и вовсе не в курсе, что нужно использовать дизайн-токены. А потом приходят клиенты и жалуются, что в каком-то месте не могут изменить цвет или шрифт. Поэтому я решил посмотреть, что можно с этим сделать и как это автоматизировать.
ℹ️ Важное замечание: в статье мы не будем говорить о том, как правильно именовать дизайн токены, это отдельная большая тема. Я выбрал самый простой вариант именования в качестве примера, и для боевого проекта он не подойдёт. Если хотите разобраться как именовать и использовать дизайн токены, вот несколько статей:
- Что такое дизайн-токены простыми словами
- Дизайн токены: как они могут помочь вам сэкономить время и деньги
- Atlassian Design System: How to read design token names
Если вам, как и мне нравится фронтенд и автоматизация — то давайте начнём. Обозначим проблему и как хотим её решить:
- Проблема: использование значений напрямую в коде, вместо дизайн токенов
- Решение: добавить линтер, который будет уведомлять нас об упомянутых проблемах
- Идеально: линтер сам заменяет все значения на переменные
Все исходники можно найти здесь.
Подключение плагина stylelint-declaration-strict-value
Для демо я буду использовать vite шаблон для реакта на тайпскрипте
pnpm create vite design-tokens-demo --template react-ts
Теперь нужно установить stylelint и товарища, из-за которого мы сегодня, собственно, собрались — плагин stylelint-declaration-strict-value.
pnpm install -D stylelint stylelint-declaration-strict-value
Именно этот плагин решает нашу проблему — он будет оповещать нас, если мы вдруг где-то не используем переменные, а жёстко задали значение.
Конечно, просто подключить плагин не достаточно, нам нужно также ему объяснить, какие свойства он должен валидировать, в нашем случае это:
- color
- background-color
- font-size
- font-weight
- font-family
- filter
Но как всегда существуют исключения, линтер не всегда должен ругаться, когда мы используем жёстко заданные значения. Например, мы хотим позволить использовать: 'currentColor', 'unset', 'inherit', 'initial', 'transparent'
, потому что создание переменных для этих значений лишено всякого смысла.
Конфигурация будет выглядеть так:
Теперь давайте запустим команду pnpx stylelint "**/*.css"
и проверим, что линтер работает.
Консоль это, конечно, хорошо, но всем нам нравится видеть ошибки прямо в редакторе кода. Если вы, как и я используете VS Code — установитеплагин для StyleLint
Можно было бы сказать, что всё готово, мы добавили литер — расходимся, но нет, давайте автоматизируем это безобразие.
Автоматизация использования переменных
Прежде чем что-то автоматизировать, нам нужно создать дизайн токены, которые мы в итоге будем использовать в наших стилях. Для этого создадим файл variables.css и для начала создадим переменные для насыщенности шрифта.
Теперь нам нужно научиться заменять значения на созданные токены. К счастью, мы можем передать в конфигурацию плагина функцию autoFixFunc, которая будет вызываться, когда styleling запущен с опцией --fix
.
Теперь добавим скрипт в package.json
для автоматического исправления ошибок и запустим его.
"lint:css": "pnpx stylelint --fix \"**/*.css\""
Тут вы должны воскликнуть, что я вас обманул и вообще посмотри на эту функцию, поддерживать такое вручную невозможно. Немного терпения, в ход вступает postcss.
Если присмотреться к реализации функции автозамены, то легко заметить, что её задача очень простая — заменить найденное значение на переменную, с точно таким же значением.
То есть, когда плагин запускает функцию autoFixFunc
мы должны найти соответствующую переменную в файле variables.css
и вернуть эту переменную из функции. Элементарно, но есть небольшая загвоздка, JavaScript не разговаривает на языке CSS, поэтому нашим переводчиком будет PostCSS — установим его pnpm install -D postcss
.
PostCSS позволяет писать плагины, с помощью которых можно работать со стилями из JavaScript. Мы напишем простой плагин для конвертации CSS переменных в JSON.
Теперь, когда мы запустим этот скрипт, он создаст вот такой variables.json
файл.
Остаётся только использовать этот файл внутри функции autoFixFunc
.
Теперь каждый раз, прежде чем запускать npx stylelint --fix \"**/*.css\"
, нужно заново сгенерировать файл variables.css
, поэтому давайте обновим скрипт lint:css
.
"lint:css": "node scripts/styles.js && pnpx stylelint --fix \"**/*.css\"",
С насыщенностью шрифта всё хорошо, теперь давайте создадим дизайн токены для цветов и добавим их в variables.css
.
Далее запустим pnpm run lint:css
и вуаля, теперь все свойства используют css переменные, ну почти.
Хочется закричать “Виват!” и сказать, что всё готово, но рано. Если присмотреться — цвет, который задан с помощью rgba
, не заменился, хотя мы создали для него переменную white-opacity
.
Это можно решить настройкой ignoreFunctions
плагина, которой необходимо задать значение false
, по-умолчанию все функции игнорируются.
Но есть у плагина и ограничения — он не умеет заменять значения внутри функций. После того как мы добавили настройку ignoreFunctions: false
stylelint ругается, что мы не используем переменные для свойств filter. К сожалению,тут мы ничего поделать не можем, и нам придётся вручную заменить значения на дизайн токены.
А чтобы stylelint перестал ругаться, нужно добавить регулярку "/^drop-shadow/"
для drop-shadow
в ignoreValues
.
ignoreValues: ["/^drop-shadow/", 'currentColor', 'unset', 'inherit', 'initial', 'transparent'],
В общем и целом всю нашу проделанную работу можно отобразить с помощью вот такой схемы.
Автоматизируем запуск линтера
Вроде всё хорошо, но мы же не хотим запускать этот скрипт всё время вручную, верно? Если уж автоматизировать, то автоматизировать до конца, чтобы переменные заменялись по мере написания кода. В этом нам поможет vite-plugin-stylelint:
Также добавим stylelint({ fix: true })
в vite.config.ts
Теперь, когда мы запустим дев сервер pnpm run dev
, замена значений на дизайн токены будет происходить автоматически (если автозамена не срабатывает — значит вам просто нужно открыть ваше приложение в браузере).
Что можно улучшить?
Конечно, это решение не идеально и есть как минимум 2 проблемы, с которыми вы можете столкнуться
Семантика
Самое главное ограничение решения — этот подход совсем ничего не знает о семантике. Он может подхватить не те переменные и, например, применить border-radius от кнопки к чекбоксу.
Допустим, что у нас уже есть кнопка и теперь нам нужно реализовать чекбокс, и сейчас у него такое же значение скругления углов (border-radius) как и у кнопки. В таком случае, может произойти неприятная ситуация, и наше решение автоматически применит токен от кнопки для чекбокса.
В этом нет ничего страшного до тех пор, пока мы не решили сильнее скруглить края для кнопки, и чекбокс не стал похож на радио кнопку. Поэтому важно доработать решение, чтобы таких конфузов не возникало. Но тут сложно предложить какое-то универсальное решение, которое подойдёт для любого проекта.
Формат значений
В нашем примере есть токен white
со значением #ffffff
, но если в коде мы будем использовать другие форматы белого цвета: #fff
, rgb(255, 255, 255)
или hsl(0, 0%, 100%)
— то ничего не получится, ведь мы используем простое сравнение строк.
Аналогично с насыщенностью — значение bold
не будет заменено на var(-font-weight-bold)
, потому что текущее решение не умеет сравнивать 700
и bold
. С размером шрифта тоже самое.
Если вам необходимо — вы можете решить эту проблему самостоятельно. Для font-weight
довольно просто написать функцию конвертации, а для сравнения цветов можно использовать библиотеку color.
Заключение
Конечно подобная автоматизация не решила все проблемы, но можно точно сказать, что она позволяет сэкономить время. Теперь я банально стал меньше обращать внимание на дизайн токены во время код ревью, и у меня появилась лишняя минутка выпить чаю (как будто до этого у меня не было на это времени).
Будет классно, если вы поделитесь решениями, которыми пользуетесь в своих проектах или как живёте без них.
Спасибо, что дочитали статью, надеюсь вы узнали для себя что-то новое. Если вам понравилось — можете поставить звёздочку репозиторию с демо-проектом или подписаться на мой телеграмм канал, где я рассказываю о разработке, вам мелочь а мне приятно.
Я собираюсь написать ещё одну статью об отображении дизайн токенов в storybook, если тема интересна можете подписаться на меня на хабре или втелеграмме, чтобы не пропустить.