樣式契約要用工具強制,逃生門要明示且有 scope

遷移完不等於結束。Linear 用兩層工具守住慣例,同時刻意保留出口。

兩層檢查

  • Oxlint:快速的本地、單檔檢查——常見寫法錯誤、design token 使用、React props 蓋掉產生出來的樣式。
  • 型別感知的 repository checker:有些規則需要的脈絡超過單檔 linter。它跨檔案追蹤元件、import 與樣式,驗證樣式 API,並偵測 property-level 合併靜默移除 base 或 conditional 樣式的情況。

五類規則:

  1. 遷移與遺留清理 — 禁止 import 舊函式庫;找出仍暴露但沒用到的 className / style props。
  2. 元件樣式 API 與跨檔案傳遞 — 讓 sx 成為元件之間的標準介面。
  3. 樣式組合與優先序 — 抓 className、style、JSX spread、sx、stylex.props()、mergedSx() 互相覆蓋;也抓 shorthand / longhand 讓 property priority 默默改變結果的組合。
  4. 設計與互動一致性 — 寫死的視覺值換成共用變數;hover / press / link 行為標準化(例如 highlightMixin 在指標裝置給 :hover、觸控裝置給 :active)。
  5. 編譯器、runtime 與 theme 安全 — 禁止對 defineConsts 做算術、對未定義的 dynamic 值設防、裸 CSS variable 換成型別化的 defineVars()。

sx 的契約有四條,合併順序是關鍵:

// 元件必須:暴露 sx、在本地樣式「之後」合併它、不默默丟掉、不宣告卻不套用
{...stylex.props(styles.base, isActive && styles.active, sx)}
//                └─ 本地樣式在前 ─┘                      └ caller 的 sx 在後

陷阱:呼叫端一個扁平的 override 會換掉元件所有 conditional state,所以跨元件邊界消失的 conditional 樣式要靠 checker 偵測。

逃生門

團隊沒有消滅逃生門,但是逃生門要明示且有 scope:

  • 本質上還是 CSS 形狀的問題(global selector、改第三方 DOM 樣式)→ 用 CSS Modules,它有 scope、而且是明寫的。
  • 其他地方 → 擴充 StyleX(自製 theming、Vite 建置期擴充)。
  • lint 的 suppression 允許,但範圍必須窄、必須有理由。

相關:把 token 當佔位符,由 ThemeProvider 在邊界注入一條規則、規格檢查要機械化進 CI,不能靠紀律、最難遷移的模式,往往正是讓舊系統難以推理的那些