デザイントークンを用意したのに、急ぎのPRで #3b82f6 が直書きされる
--color-primary を定義して、チームにも「色はトークン経由で」と伝えてある。それでも色は直書きされる。締め切り前のPRで、どこかから貼り付けたコンポーネントの中で、className="text-[#3b82f6]" の中で。私自身、何度もやった。
止める手段が無いわけではない。stylelint-declaration-strict-value(週48万ダウンロード)を入れれば、色のベタ書きはCIで弾ける。でも返ってくるのは pass か fail だけだ。「今どれくらいトークン化できているのか」「このPRで負債が増えたのか」——私が見たい数字は出てこない。
だから作った。ベタ書きの色を数えて、トークン網羅率をスコアカードとバッジにして、毎PRで「増えたら落とす」リンタ、tokenlint だ。
まず、数字が出る
npx @hyuga/tokenlint を叩くと、こう出る。
$ npx @hyuga/tokenlint
tokenlint · color token coverage
Coverage 28.6% · tokenized 4 / hardcoded 10
Hardcoded 10 colors across 3 files
Palette 4 tokens defined
#3b82f6 Card.jsx:3 → --color-primary (Δ0)
rgb(107,114,128) Card.jsx:4 → --color-primary (Δ209)
#1a1a1a legacy.css:3 → --color-ink (Δ0)
hsl(217,91%,60%) legacy.css:10 → --color-primary (Δ2)
…
やっていることは3つだ。
- 網羅率 = トークン経由の色 ÷(トークン経由 + ベタ書き)を出す
- ベタ書きの色を file:line 付きで並べ、
--color-primaryのような近傍トークンを色距離で提案する(Δが小さいほど「そのトークンで置き換えられる」) -
--reportを付けると、同じ内容を色チップのスコアカードHTMLで吐く(色の四角が並ぶスコアカード。GitHubのREADMEに実物を貼ってある)
設定ファイルは要らない。:root の --color-* を勝手にトークン源として読むからだ。Figmaもトークンファイルも要らない。フォルダに向けて3秒で数字が出る。ここが入口になる。
でも、見えるだけのツールは続かない
スクショが映えるツールは拡散はするが、毎日は使われない。だから定着の本体は別に置いた。GitHub Action だ。
# .github/workflows/tokenlint.yml
on: [pull_request]
jobs:
tokens:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: hyuga611/tokenlint@v0
with:
max-new: 0 # このPRで色を新しくベタ書きしたら落とす
max-new: 0 の意味はこうだ。「既にある負債は責めない。でもこのPRで増やしたら落とす。」git の共通祖先(merge-base)と比べて、このPRで追加されたベタ書きの実数だけを見る。size-limit がバンドルのバイト増を見張るのと同じ発想を、色でやる。
これが効くのは、既存の巨大なレガシーCSSで詰まらないからだ。網羅率が今3割でも、「これ以上増やさない」なら明日から入れられる。人が意識しなくても毎PRで走る——ここが定着の本体だ。
正直に言う。色の検出そのものは、レッドオーシャンだ
ここは正直に書きたい。「色のベタ書きを検出する」だけなら、さっきの stylelint-declaration-strict-value が既にデファクトだ。近傍トークンの提案も、ds-bridge というツールが先にやっている。まっさらな空き地ではなかった。
だから tokenlint は「検出」で殴り合うのをやめた。空いていたのは見せ方だった。
- READMEに常駐する網羅率バッジを出すツールは無かった
- 「このPRで新規ベタ書き +N」という差分の実数を主役にするツールも無かった
-
設定ゼロで、CSS変数と Tailwind の
text-[#..]を一本で両対応するものも無かった
検出は誰でも書ける。ccusage のドル、size-limit のバイトのように、盛れない数字を見せる——そこに賭けた。
公開する前に自分のツールを敵対的にレビューさせたら、launch即死バグが出てきた
ここが一番書きたいところだ。
見えるwowで拡散を狙うツールは、最初に触った人が誤検知を踏んだら終わりだ。スクショで叩かれて信用が飛ぶ。だから npm に publish する直前、自分のコードを6つの観点(スキャナ・色パース・git差分・CLI・HTMLのXSS・ReDoS)で敵対的にレビューさせ、各指摘を独立に検証した。17件、実バグが出た。
一番まずかったのはこれだ。hsl(var(--background)) を「ベタ書きの色」だと誤検知していた。
shadcn/ui や最近の Tailwind は、色を hsl(var(--foo)) の形で持つ。つまりいちばん模範的にトークン化されたコードだ。それを私のツールは「網羅率50%、ベタ書き3件」と報告していた。色関数の中の括弧を雑に閉じていたのが原因だ。もしこのまま公開して shadcn ユーザーが最初に試していたら、「こいつ何も分かってない」で即終了だったと思う。
もう一つ、地味に怖いやつがあった。閉じ括弧の無い [color:#fff を含む巨大なファイルを食わせると、正規表現が壊滅的バックトラッキングを起こして、20万文字で16秒ハングした。悪意あるPRファイル1個でCIを溶かせる。
両方とも公開前に直した。hsl(var()) は入れ子対応とトークン参照の除外で網羅率100%に戻し、ReDoS は正規表現を lookahead で書き直して16秒→5ミリ秒にした。テストは11件から24件に増やして、17件すべてを回帰テストで固定した。
教訓はひとつ。見えるツールほど、公開する前に自分を疑え。自作のテストが通ることは、正しさの証明にはならない。
まとめ
色のベタ書きは、「数えて、見せて、毎PRで増やさない」でCIに載る。
-
npx @hyuga/tokenlintで今すぐ試せる - GitHub Action なら
uses: hyuga611/tokenlint@v0の一行 - 依存ゼロ・設定ゼロ・CSS変数と Tailwind 両対応
リポジトリはこちら。 https://github.com/hyuga611/tokenlint