樣式契約要用工具強制,逃生門要明示且有 scope
遷移完不等於結束。Linear 用兩層工具守住慣例,同時刻意保留出口。
兩層檢查
- Oxlint:快速的本地、單檔檢查——常見寫法錯誤、design token 使用、React props 蓋掉產生出來的樣式。
- 型別感知的 repository checker:有些規則需要的脈絡超過單檔 linter。它跨檔案追蹤元件、import 與樣式,驗證樣式 API,並偵測 property-level 合併靜默移除 base 或 conditional 樣式的情況。
五類規則:
- 遷移與遺留清理 — 禁止 import 舊函式庫;找出仍暴露但沒用到的
className/styleprops。 - 元件樣式 API 與跨檔案傳遞 — 讓
sx成為元件之間的標準介面。 - 樣式組合與優先序 — 抓
className、style、JSX spread、sx、stylex.props()、mergedSx()互相覆蓋;也抓 shorthand / longhand 讓 property priority 默默改變結果的組合。 - 設計與互動一致性 — 寫死的視覺值換成共用變數;hover / press / link 行為標準化(例如
highlightMixin在指標裝置給:hover、觸控裝置給:active)。 - 編譯器、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,不能靠紀律、最難遷移的模式,往往正是讓舊系統難以推理的那些