昨天用 VRT 把畫面的變化攔了下來,但攔下來之後,真正的問題才開始。那張 diff 你看過、也點頭核可了,接下來要怎麼讓幾十個使用端知道「從這一版起,你的畫面會不一樣」。設計系統的發布從來不是把 package 推上 npm 就結束,版號本身就是一段訊息,它告訴下游「你可以閉著眼睛升,還是得排一個 sprint 來處理」。今天要把這段訊息講清楚:它怎麼定義、怎麼自動化產出、怎麼在移除舊東西時不傷人,以及怎麼寫得讓設計師也看得懂。

📖 學

語意化版本(Semantic Versioning)的三段式規則本身很簡單:MAJOR.MINOR.PATCH,不相容的 API 變更進 major,向後相容的新功能進 minor,向後相容的修正進 patch。難的是套到設計系統上時,你得先回答:設計系統的「API」到底包含什麼。一般套件的 API 就是函式簽名與型別;元件庫的使用者消費的卻不只是 props,還包括渲染出來的視覺結果、token 的名稱、以及元件在版面中佔據的空間。這三樣任一改變,下游畫面都可能壞掉,而 TypeScript 一個錯都不會報。所以設計系統的 semver,本質上是「把視覺契約也納入 API 契約」的一種延伸解釋。

那麼視覺變動到底算不算 breaking change?這是最容易吵起來的地方,而 Nathan Curtis 在〈Visual Breaking Change in Design Systems〉給的判準很好用:看這個變化有沒有溢出系統所擁有的那塊空間。如果你調的是 Menu 內部項目之間的 padding,那塊區域完全由系統定義與負責,外面的版面不會因此位移,它就不是 breaking change,即使畫面確實變了。反過來,如果變更讓使用端的版面產生位移、文字被截斷、元素互相重疊、或視覺層級整個翻轉,那不管你動的是幾個像素,它都是 breaking——因為壞掉的是別人的頁面,而別人沒有能力預期這件事。

從這個判準可以整理出一組實用的對照表,團隊在 PR 上吵不出結果時直接拿來對:

變更內容版號判斷理由
移除或重新命名 prop、移除元件major使用端的程式碼會直接壞掉
重新命名或刪除語意 token(如 color.bg.subtle)majortoken 名稱就是 API,主題檔、Figma、文件會一起斷
元件外框尺寸變大、行高改變導致版面位移major溢出系統擁有的空間,弄壞了使用端版面
新增 prop、新增變體、新增 tokenminor純增加,舊用法完全不受影響
標記某個 prop 為 deprecated(但仍可用)minor只是加了警告,行為沒變
元件內部間距微調、陰影層次調整patch變化被包在系統負責的區塊裡
色票微調以符合對比度標準、修正 hover 狀態失效patch修 bug,且不改變佔位

design token 的版本語意值得單獨拿出來講,因為它的影響半徑大得嚇人。一個 spacing token 改值,可能連動幾十個畫面,而過程中沒有任何一個元件的 props 有變動;一個語意色票被重新命名,會同時打斷程式碼、主題檔、Figma variable 與文件引用。實務上的規則是:token 的名稱與語意屬於公開 API,值不一定是。所以刪掉或改名 token 一律 major,新增 token 是 minor,而改值要看它落在哪一格——把 #0066CC 微調成 #0B66C3 以通過對比度是 patch,把整套 spacing scale 從 4px 基準改成 8px 基準,那是 major,因為每一個使用端的版面都會動。這也是為什麼 token 應該和元件庫分成不同的 package 各自版本化:讓下游可以只升 token 而不動元件,或反過來。

自動化的部分,現在的主流答案是 Changesets。它的設計精神是把「這次改動該升什麼版號、要跟使用者說什麼」這件事,從發布當下往前搬到 PR 當下——由最清楚脈絡的作者本人來寫。做法是每個 PR 跑一次 npx changeset,它會問你動了哪些 package、各自要升 major/minor/patch、以及一句描述,然後在 .changeset/ 底下留一個 markdown 檔跟著 PR 一起 review。這一步的價值在於版號決定被攤到 code review 上,reviewer 可以直接說「這個我覺得是 breaking,不是 patch」,而不是等發布前一刻才由某人拍腦袋決定。發版時 CI 把累積的 changeset 讀進來、算出各 package 的最終版號、產生 CHANGELOG、刪掉這些檔案,然後開一個「Version Packages」的 PR;merge 它就等於發布。monorepo 可選 fixed(共用版號)或 independent(各自獨立),設計系統通常選 independent,並用 linked 把 token 與主題包綁在一起。

有兩個誤解會反覆出現。第一個是「只要畫面有變就是 major」,聽起來很負責,實際上會造成版號通膨:一年發十幾個 major,下游看到 major 就自動延後升級,最後大家全都卡在兩年前的版本,你的設計系統就死了。major 應該是稀有事件,一年一到兩次,而且要有配套。第二個誤解相反:「props 沒改就是 patch」,這會讓你在某個週四下午靜靜地把三十個產品頁面弄歪,而且因為是 patch,很多專案的自動更新機制會直接吞下去,沒人有機會攔截。判準永遠回到那句——壞掉的是系統內部,還是別人的版面。

移除東西的正確姿勢是棄用流程,而不是直接刪。Primer 的元件生命週期把這件事定義得很清楚:元件會經過 experimental、beta、stable,最後才是 deprecated,而 deprecated 不等於消失。一個健康的棄用有三個階段:第一階段(minor 版)標記棄用,元件照常運作,但加上 JSDoc 的 @deprecated 標記、開發環境的 console 警告、文件上的狀態標籤、以及一條 ESLint 規則讓使用端在自己的 CI 就看得到;同時最重要的是明確寫出替代方案,「請改用 X」比「此元件已棄用」有用一百倍。第二階段是等待期,Primer 要求移除日期提前公告、且至少距離移除版本一個月以上,實務上給到一到兩個季度更合理,期間主動追蹤還有多少 repo 在用。第三階段才是在下一個 major 版真的刪掉。跳過任何一階段,你換來的都是下游對升級的恐懼。

而讓 major 版真的有人願意升的關鍵,是你有沒有把遷移成本自己吃下來,這就是 codemod 的用途。jscodeshift 這類工具把程式碼解析成 AST 再做結構化轉換,所以它能精準處理「只改 import 自我們套件的那個 Button,不要動到別人的 Button」這種搜尋取代做不到的事。改名元件、改名 prop、調整 import 路徑、把布林 prop 換成字串 enum,都是 codemod 的甜蜜區,應該連同 major 版一起發出去,讓使用端 npx @your-ds/codemod v3 ./src 一行解決。但要誠實面對極限:語意層重構(一個元件被拆成兩個、需要人判斷用哪個)、透過 spread 傳進去的動態 props、被包了一層自家 wrapper 的用法,codemod 都處理不了。標準做法是 codemod 掃八成、剩下兩成寫成清楚的手動清單,並提醒使用端把 codemod 產出獨立成一個 commit,方便 review 時把機器改的和手改的分開看。

最後是最常被做壞的一環:變更日誌。Changesets 自動產生的 CHANGELOG.md 對工程師夠用,但對設計師幾乎是無效資訊——fix(Button): update padding token reference 這行字沒有回答任何一個設計師會問的問題。設計師想知道的是:哪個元件在視覺上變了、變前變後長什麼樣、Figma 檔要不要跟著改、需不需要我動手。所以成熟的設計系統維護兩層:一層是機器產生的技術 changelog,一層是人工整理的 release note,後者按「影響範圍」分組而非按 package 分組,每個視覺變更配一張 before / after 對照圖(正好可以從昨天的 VRT diff 直接撈出來),並且一定要有「你需要做什麼」區塊,明確標成「不需動作」、「需要更新 Figma library」或「需要跑 codemod」。想達到這個品質,最省力的施力點其實在前面:把 changeset 的描述規定成「一句非工程師讀得懂的話」,並在 code review 時當成一項檢查。

🧠 記

  • 設計系統的 API 不只是 props,還包含視覺輸出、token 名稱、以及元件在版面中佔據的空間,三者任一改變都可能是 breaking。
  • 視覺變動算不算 breaking 的判準:變化有沒有溢出系統自己擁有的區塊。壞掉的是內部間距 → 不算;壞掉的是使用端版面 → 算。
  • token 的名稱與語意是 API,值不一定是:改名/刪除是 major,新增是 minor,微調色值是 patch,整套 scale 換基準是 major。
  • Changesets 的核心價值是把「版號決定 + 使用者說明」搬到 PR 當下,由最懂脈絡的作者寫,並讓 reviewer 有機會反對。
  • major 版號通膨會讓下游乾脆不升級;反過來把視覺破壞塞進 patch,則會讓自動更新機制無聲地弄歪產品畫面。
  • 棄用是三階段流程:minor 標記 + 明確替代方案 → 公告過的等待期 → 下一個 major 才移除;major 要附 codemod,它掃改名與結構轉換這八成,剩下兩成寫成手動清單。
  • 給設計師看的 release note 要按影響範圍分組、附 before/after 圖、並明確寫出「你需要做什麼」。

✍️ 實踐

今天找出你最近一次被 VRT 抓到、而且已經核可的視覺變更(如果手邊沒有,就翻最近三個 merge 進主線的 PR,挑一個有動到樣式的)。第一步先用「溢出判準」問三個問題:這個變化會不會讓使用端的版面位移?會不會讓文字被截斷或元素重疊?有沒有動到任何 token 的名稱?三題全否就是 patch,只有新增就是 minor,任何一題是就是 major。第二步把判定結果寫成一則 changeset:專案裝了 Changesets 就跑 npx changeset,沒裝的話手寫一個 markdown 也完全可以,重點是逼自己寫出那句描述。第三步是今天真正的自我檢查——把這句描述傳給團隊裡一位設計師或 PM,問他能不能單憑這句話回答「我需不需要做什麼」。答不出來就回頭檢查,是不是塞了只有工程師懂的東西(token 變數名、prop 名、檔案路徑、commit hash),改寫成「什麼元件、看起來哪裡不一樣、你要不要動手」的三段式。十五分鐘內做得完,而你會立刻感覺到,難的從來不是決定版號,是把改動翻譯成使用端的語言。

🔗 延伸學習

💬 問 AI

我在維護一套內部設計系統(monorepo:tokens、react 元件庫、圖示三個 package),下游約有 30 個使用端專案。
目前發版是手動改 package.json 版號 + 人工寫 changelog,已經開始出問題:視覺破壞被當成 patch 發出去、下游不敢升 major。
 
請幫我:
1. 定義一份「什麼算 major / minor / patch」的判準表,要同時涵蓋元件 API 變更、視覺變更、以及 design token 變更三類,每一格附一個具體例子與判斷理由。
2. 設計一套 Changesets 導入方案:三個 package 該用 fixed、linked 還是 independent?PR 模板要怎麼強制寫 changeset?CI(GitHub Actions)自動發版的流程長什麼樣?
3. 制定一份元件與 token 的棄用流程,含各階段的時間長度、要發出的警告形式(JSDoc、console、ESLint 規則、文件標籤)、以及如何追蹤下游殘留使用量。
4. 給我一份「給設計師看的 release note」範本,說明該分成哪些區塊、視覺變更要附什麼素材、以及「需要你做什麼」該怎麼標示。
5. 舉一個實際例子:我要把 <Button variant="primary"> 改成 <Button tone="brand">,請寫出對應的 jscodeshift codemod 草稿,並列出這個 codemod 處理不到、必須手動遷移的情況。
 
請用繁體中文、台灣用語回答,程式碼與設定檔用實際可跑的片段。