Canonicalization: 大きな任意値をスペーシングスケールへ変換しないよう制限
Tailwind CSS のキャノニカライゼーション処理において、任意値から bare value への変換が大きすぎるスペーシングスケール値を提案しないよう制限されました。left-[99999px] が left-24999.75 に変換される不自然な挙動が修正されています。
背景
Tailwind CSS の Canonicalization 機能は、left-[6px] のような任意値を left-1.5 のような bare value(名前付き値)へ変換することで、コードを簡潔に保つ仕組みです。--spacing 変数を内部で使うユーティリティについては、任意値の px 値を spacing の乗数に換算して bare value を求めます。
この変換は left-[6px] → left-1.5 のような小さい値では直感的に理解しやすいものの、left-[99999px] → left-24999.75 のようなケースでは明らかに不自然です。PR の説明では、.75 という小数が問題なのか、24999 という大きさが問題なのかを切り分けるため、left-[99996px] → left-24999(小数なし)のケースでも依然として違和感があることが確認されています。
デフォルトテーマで最大のブレークポイントは --breakpoint-2xl: 96rem(= 1536px)であることに着目し、変換後の bare value が実質的に 1536px を超えない範囲に限定することが合理的と判断されました。
技術的な変更
canonicalize-candidates.ts に isReasonableBareValue 関数が追加され、bare value への変換の可否を判断するロジックが導入されました。
新たに定義された定数と関数は以下のとおりです:
const MAX_BARE_VALUE_IN_PX = 1536
const MAX_BARE_VALUE_IN_REM = MAX_BARE_VALUE_IN_PX / 16
function isReasonableBareValue(value: number, designSystem: DesignSystem, rem: number | null) {
let spacingMultiplier = designSystem.resolveThemeValue('--spacing')
if (spacingMultiplier === undefined) return false
let parsed = dimensions.get(constantFoldDeclaration(spacingMultiplier, rem))
if (parsed === null) return false
let [spacingValue, spacingUnit] = parsed
let bareValueInPixels = value * spacingValue
if (spacingUnit === 'px') return bareValueInPixels <= MAX_BARE_VALUE_IN_PX
if (spacingUnit === 'rem') return bareValueInPixels <= MAX_BARE_VALUE_IN_REM
return false
}
変換の呼び出し箇所では、既存の isValidSpacingMultiplier チェックに isReasonableBareValue を AND 条件で追加することで、上限を超える bare value の提案を抑制しています:
変更前:
if (isValidSpacingMultiplier(bareValue)) {
yield Object.assign({}, candidate, {
value: { kind: 'named', value: bareValue, fraction: null },
})
}
変更後:
if (
isValidSpacingMultiplier(bareValue) &&
isReasonableBareValue(bareValue, designSystem, options.signatureOptions.rem)
) {
yield Object.assign({}, candidate, {
value: { kind: 'named', value: bareValue, fraction: null },
})
}
この制限が適用されるのは --spacing 変数を経由して bare value を算出するケースのみです。z-[9999999] のような --spacing 乗数を使わないユーティリティは引き続き z-9999999 へ変換されます。これはテストケースでも明示的に確認されています:
['left-[99999px]', 'left-[99999px]'], // This would otherwise result in `left-24999.75`
['left-[-99999px]', 'left-[-99999px]'], // This would otherwise result in `-left-24999.75`
['left-[96rem]', 'left-384'], // Within the limit
['left-[-96rem]', '-left-384'], // Within the limit
['left-[calc(96rem+1px)]', 'left-[calc(96rem+1px)]'], // Out of the positive limit
['z-[9999999]', 'z-9999999'], // `--spacing` multiplier is not used
設計判断
閾値を 1536px(--breakpoint-2xl の px 換算値)に設定するアプローチが採用されました。
PR では、特定パターン(繰り返し数値、1337 などのミーム的な値、720px や 1280px などの一般的な解像度)を検出して変換を回避するアイデアも言及されています。しかし、パターン列挙は網羅が困難で維持コストも高いため、「デフォルトテーマで実際に使われる最大値を上限とする」という単純かつ客観的な基準が選ばれています。
また、制限の単位を px と rem の両方に対応させた点も重要です。--spacing の解決値が rem 単位であれば MAX_BARE_VALUE_IN_REM(= 96)と比較し、px 単位であれば MAX_BARE_VALUE_IN_PX(= 1536)と比較することで、テーマのカスタマイズに対しても一貫した動作を保証しています。
まとめ
本変更は、閾値という単純な仕組みで「デフォルトテーマで意味を持つ範囲内だけを変換する」という明確な境界を設けた実用的な改善です。パターンマッチングのような複雑なヒューリスティックを避け、拡張性のある初手を打ちながら、将来的な改善の余地も明示的に残している点が特徴的です。