Ви прибрали з Button проп type, бо він дублював variant. Локально чисто, Storybook зелений, реліз пішов. Наступного дня у двох продуктових командах не збирається білд, а в третій кнопка мовчки стала на 4 пікселі нижча і поїхала шапка.
Окремо ніхто не винен. Зламався не компонент, зламався процес релізу: у команд не було способу дізнатися, наскільки страшно оновлюватись.
Версіонування - це не бюрократія для великих компаній. Це одна цифра, яка замінює листування в чаті.
Що взагалі вважати breaking change
Більшість команд думає про breaking change як про те, від чого падає TypeScript. Для дизайн-системи цього визначення замало. Ламати можна щонайменше чотирма способами:
- API компонента. Прибрали або перейменували проп, зробили обовʼязковим те, що було опційним, змінили дефолтне значення.
- Візуал. Змінилась висота, паддінг, товщина шрифту. Код збирається ідеально, а макет у споживача їде.
- Поведінка. Модалка тепер закривається по Esc. Кнопка всередині форми стала
type="submit". Тултіп зʼявляється по фокусу, а не по ховеру. - Токени. Перейменували
color.bg.subtleуcolor.surface.subtle. Для тих, хто звертався до токена напряму, це рівно те саме, що видалений проп.
Робоче правило: якщо команда-споживач мусить зупинитись і прийняти рішення, це breaking change. Навіть якщо компілятор мовчить.
Найпідступніші breaking changes не ламають збірку. Вони ламають макет через тиждень, і ніхто вже не звʼязує це з оновленням бібліотеки. Візуальні регресії теж мають їхати мажором.
Semver: три числа і одне правило
Формат MAJOR.MINOR.PATCH, наприклад 2.4.1:
- PATCH (2.4.1 → 2.4.2): фікс, який ніхто не помітить, крім того, хто на нього чекав.
- MINOR (2.4.1 → 2.5.0): щось нове і зворотно сумісне. Новий компонент, новий проп з дефолтом, новий токен. Оновлення можна ставити не читаючи.
- MAJOR (2.4.1 → 3.0.0): все, що вимагає дій від споживача.
Одне правило, яке закриває 90% суперечок: сумніваєтесь між minor і major - беріть major. Зайвий мажор коштує командам десяти хвилин на читання changelog. Пропущений мажор коштує зламаного релізу в проді і години на пошук причини.
Окремо про 0.x. Формально, доки ви в нулях, ламати можна будь-коли. Але якщо вашою системою вже користуються дві команди, ви де-факто в 1.0, просто без чесної цифри. Ставте 1.0.0 у той день, коли зʼявився другий споживач, а не тоді, коли система "буде готова". Готовою вона не буде ніколи.
Як не робити мажор кожні два тижні
Semver вирішує питання "як повідомити". Але найдешевший breaking change - той, якого не було. Тут працює патерн, який у бекенді називають expand and contract, а в дизайн-системах він виглядає так:
- Розширюємо. Додаєте новий проп
variant, старийtypeзалишається робочим і просто мапиться на новий. Це minor, оновлюються всі й одразу. - Чекаємо. Один-два релізи старий проп живе з поміткою deprecated. Ви бачите по коду споживачів, скільки місць лишилось.
- Звужуємо. Коли використань нуль або майже нуль, видаляєте старий проп мажором. Для більшості команд цей мажор уже нічого не ламає.
Друге, що суттєво зменшує кількість мажорів: не виносьте в публічний API те, у чому не впевнені. Кожен проп, який ви віддали назовні, це обіцянка підтримувати його роками. Пʼять пропсів, які точно потрібні, краще за пʼятнадцять "про всяк випадок", бо кожен зайвий - це майбутній major.
І третє: візуальні тести. Chromatic або Playwright зі скріншотами ловлять саме той тип змін, який компілятор пропускає. Без них ви дізнаєтесь про візуальний breaking change від продуктової команди, а не від CI.
Один пакет чи пакет на компонент
Це друге питання, на якому команди зависають надовго.
Один пакет (@company/ui) - одна версія на все. Просто пояснити, просто релізити. Мінус: мажор через один Table змушує всіх думати про весь пакет, навіть тих, хто Table не використовує.
Пакет на компонент (@company/ui-button) - точкові оновлення. Мінус чесніший, ніж здається: вам потрібна матриця сумісності версій і людина, яка її тримає в голові. Для команди з трьох людей це смерть.
Практичний орієнтир: до 40 компонентів і до пʼяти команд-споживачів тримайте один пакет. Дробіть, тільки коли доведено болить, і бажано на два-три пакети за природними межами (core, icons, charts), а не на сорок.
Changesets: реліз без ручного changelog
Ручний CHANGELOG.md завжди відстає, бо його пишуть в кінці, коли вже нічого не памʼятаєш. Changesets переносить опис зміни в той момент, коли ви цю зміну робите.
Виглядає це так:
- Зробили зміну, запускаєте
npx changeset. Питає: який пакет, який рівень (patch / minor / major), опис своїми словами. - Створюється маленький md-файл у теці
.changeset/. Він їде в PR разом з кодом. - Перед релізом
npx changeset versionбампає версії і збирає всі описи в CHANGELOG.md. npx changeset publishпублікує в npm.
Головна цінність тут не в автоматизації, а в кроці 2. Ревʼюер бачить у PR не тільки діф, а й заявлений рівень зміни - і може сказати "стоп, це не minor". Це найдешевший момент, щоб зловити пропущений мажор: до релізу, а не після. Кілька таких прогонів я показував на YouTube-каналі UX Hero.
Одне правило до цього: PR без changeset не мержиться. Ставиться перевіркою в CI за пʼять хвилин.
Deprecation: як прибирати, не ламаючи
Правило двох релізів: спочатку мінор з попередженням, потім мажор з видаленням. Ніколи не мінор з видаленням.
- Позначка в коді. Додайте
@deprecatedу JSDoc над пропом чи компонентом. IDE малює перекреслення прямо в редакторі, і розробник побачить це, навіть не відкривши ваш changelog. - Попередження в консолі. Один
console.warnтільки в dev-режимі: "Button proptypeis deprecated, usevariant. Removal in 3.0". - Дата, а не "колись". "Видаляємо в 3.0, орієнтовно листопад" - це те, що команда може покласти в спринт. "Плануємо прибрати" - це те, що ніхто не покладе нікуди.
- Codemod. Якщо перейменування механічне, напишіть скрипт на jscodeshift і покладіть його в репозиторій. Різниця між "мігруйте самі" і "запустіть одну команду" - це різниця між міграцією за квартал і за день.
Паралельно те саме в Figma: префікс [deprecated] у назві компонента, щоб він падав у кінець списку в Assets, і опис із посиланням на заміну.
Figma не має semver. Що з цим робити
Це головний розрив у процесі. У коді є версія, у бібліотеці Figma її немає: дизайнер бачить панель Library updates зі списком змінених компонентів і нуль інформації про те, ламає це щось чи ні.
Мінімум, який закриває проблему:
- Заведіть у файлі бібліотеки сторінку Releases: дата, номер версії (той самий, що в npm), список змін, помітка BREAKING зверху.
- Публікуйте бібліотеку в той самий день, що й npm-реліз, і пишіть номер версії в описі публікації. Цей опис - єдине поле, яке Figma реально показує споживачам. Не витрачайте його на слово "update".
- Ламаючі зміни робіть у гілці (branch), а не одразу в головному файлі. Ревʼю до того, як воно поїде у двадцять продуктових файлів.
Сама по собі синхронність номерів не магія. Але коли дизайнер каже "у мене 2.4" і розробник каже "у мене 2.4", розмова про баг займає хвилину замість години.
Changelog, який реально читають
Три класичні помилки: писати "fixes and improvements", писати списком комітів, писати для себе.
Робочий формат одного запису - три рядки:
- Що змінилось. Одне речення людською мовою.
- Кого стосується. "Тих, хто використовує Table з пропом sortable".
- Що зробити. Конкретна дія або посилання на codemod.
Секції: Breaking, Added, Fixed. Breaking завжди зверху, навіть якщо там один рядок.
І одне неочевидне правило, яке не про формат: анонсуйте мажор у спільному каналі за тиждень до релізу, а не в день релізу. Не з ввічливості, а тому що спринти планують заздалегідь, і в день релізу у команди вже немає вільних рук.
З чого почати цього тижня
Якщо версій зараз немає взагалі:
- Поставте
1.0.0сьогодні. Не чекайте "готовності". - Заведіть CHANGELOG.md з трьома секціями: Breaking, Added, Fixed.
- Підключіть Changesets. Це пів години, включно з перевіркою в CI.
- Домовтесь про визначення breaking change і запишіть його в README одним реченням. Те саме, що вище: якщо споживач мусить зупинитись і прийняти рішення.
- На найближчий мажор: анонс за тиждень плюс codemod, якщо зміна механічна.
Жоден з цих пунктів не вимагає великої команди чи бюджету. Вони вимагають домовленості на пʼять рядків.
Дизайн-система без версій - це не система, це папка з компонентами. Версія - найдешевший спосіб сказати команді правду про власні зміни до того, як вона дізнається її з проду.