Ви прибрали з 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, а в дизайн-системах він виглядає так:

  1. Розширюємо. Додаєте новий проп variant, старий type залишається робочим і просто мапиться на новий. Це minor, оновлюються всі й одразу.
  2. Чекаємо. Один-два релізи старий проп живе з поміткою deprecated. Ви бачите по коду споживачів, скільки місць лишилось.
  3. Звужуємо. Коли використань нуль або майже нуль, видаляєте старий проп мажором. Для більшості команд цей мажор уже нічого не ламає.

Друге, що суттєво зменшує кількість мажорів: не виносьте в публічний API те, у чому не впевнені. Кожен проп, який ви віддали назовні, це обіцянка підтримувати його роками. Пʼять пропсів, які точно потрібні, краще за пʼятнадцять "про всяк випадок", бо кожен зайвий - це майбутній major.

І третє: візуальні тести. Chromatic або Playwright зі скріншотами ловлять саме той тип змін, який компілятор пропускає. Без них ви дізнаєтесь про візуальний breaking change від продуктової команди, а не від CI.

Один пакет чи пакет на компонент

Це друге питання, на якому команди зависають надовго.

Один пакет (@company/ui) - одна версія на все. Просто пояснити, просто релізити. Мінус: мажор через один Table змушує всіх думати про весь пакет, навіть тих, хто Table не використовує.

Пакет на компонент (@company/ui-button) - точкові оновлення. Мінус чесніший, ніж здається: вам потрібна матриця сумісності версій і людина, яка її тримає в голові. Для команди з трьох людей це смерть.

Практичний орієнтир: до 40 компонентів і до пʼяти команд-споживачів тримайте один пакет. Дробіть, тільки коли доведено болить, і бажано на два-три пакети за природними межами (core, icons, charts), а не на сорок.

Changesets: реліз без ручного changelog

Ручний CHANGELOG.md завжди відстає, бо його пишуть в кінці, коли вже нічого не памʼятаєш. Changesets переносить опис зміни в той момент, коли ви цю зміну робите.

Виглядає це так:

  1. Зробили зміну, запускаєте npx changeset. Питає: який пакет, який рівень (patch / minor / major), опис своїми словами.
  2. Створюється маленький md-файл у теці .changeset/. Він їде в PR разом з кодом.
  3. Перед релізом npx changeset version бампає версії і збирає всі описи в CHANGELOG.md.
  4. npx changeset publish публікує в npm.

Головна цінність тут не в автоматизації, а в кроці 2. Ревʼюер бачить у PR не тільки діф, а й заявлений рівень зміни - і може сказати "стоп, це не minor". Це найдешевший момент, щоб зловити пропущений мажор: до релізу, а не після. Кілька таких прогонів я показував на YouTube-каналі UX Hero.

Одне правило до цього: PR без changeset не мержиться. Ставиться перевіркою в CI за пʼять хвилин.

Deprecation: як прибирати, не ламаючи

Правило двох релізів: спочатку мінор з попередженням, потім мажор з видаленням. Ніколи не мінор з видаленням.

  • Позначка в коді. Додайте @deprecated у JSDoc над пропом чи компонентом. IDE малює перекреслення прямо в редакторі, і розробник побачить це, навіть не відкривши ваш changelog.
  • Попередження в консолі. Один console.warn тільки в dev-режимі: "Button prop type is deprecated, use variant. 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", писати списком комітів, писати для себе.

Робочий формат одного запису - три рядки:

  1. Що змінилось. Одне речення людською мовою.
  2. Кого стосується. "Тих, хто використовує Table з пропом sortable".
  3. Що зробити. Конкретна дія або посилання на codemod.

Секції: Breaking, Added, Fixed. Breaking завжди зверху, навіть якщо там один рядок.

І одне неочевидне правило, яке не про формат: анонсуйте мажор у спільному каналі за тиждень до релізу, а не в день релізу. Не з ввічливості, а тому що спринти планують заздалегідь, і в день релізу у команди вже немає вільних рук.

З чого почати цього тижня

Якщо версій зараз немає взагалі:

  1. Поставте 1.0.0 сьогодні. Не чекайте "готовності".
  2. Заведіть CHANGELOG.md з трьома секціями: Breaking, Added, Fixed.
  3. Підключіть Changesets. Це пів години, включно з перевіркою в CI.
  4. Домовтесь про визначення breaking change і запишіть його в README одним реченням. Те саме, що вище: якщо споживач мусить зупинитись і прийняти рішення.
  5. На найближчий мажор: анонс за тиждень плюс codemod, якщо зміна механічна.

Жоден з цих пунктів не вимагає великої команди чи бюджету. Вони вимагають домовленості на пʼять рядків.


Дизайн-система без версій - це не система, це папка з компонентами. Версія - найдешевший спосіб сказати команді правду про власні зміни до того, як вона дізнається її з проду.