任意値における単位の正規化を抑制し、`-mt-[20in]` → `mt-[-20in]` に改善
キャノニカライゼーション処理において、任意値(arbitrary value)内の単位を基本単位へ変換しないよう修正されました。これにより、-mt-[20in] が mt-[-1920px] という意図不明な値に変換されてしまう問題が解消されます。
背景
Tailwind CSS のキャノニカライゼーション機能は、等価なクラスを正規の形式に統一するものです。たとえば -mt-[20in] と mt-[-20in] は意味的に同一であるため、後者の形式に正規化されます。この処理の一部として、シグネチャ計算のために単位を基本単位(px など)へ変換する処理が行われていました。
シグネチャ計算自体は正しく機能しており、たとえば .foo { margin-top: 20in; } と .bar { margin-top: 1920px; } が同一シグネチャを持つと判定されるのは意図通りの動作です。しかし、この単位正規化のロジックが任意値の変換処理にも漏れ出していました。
tailwindcss-intellisense#1573 では、印刷用ドキュメントで in(インチ)単位を意図的に使用しているにもかかわらず、Tailwind CSS IntelliSense が -mt-[0.04in] を mt-[-3.84px] と書き換えるよう提案するという問題が報告されました。px への変換はまったく「簡略化」になっておらず、かえって混乱を招く動作でした。
根本的な問題は、任意値内の - を値の中に移動する処理において、定数畳み込み(constant folding)と単位正規化が両方実行されていた点にあります。-mt-[20in] を mt-[calc(20in_*_-1)] に展開したあと、この calc 式を評価する際に 20in が 1920px に変換されてしまい、最終的に mt-[-1920px] という値が生成されていました。
技術的な変更
constantFoldDeclarationAst 関数に normalizeUnit フラグが追加され、呼び出し側が単位正規化の有無を制御できるようになりました。
packages/tailwindcss/src/constant-fold-declaration.ts にて、constantFoldDeclaration と constantFoldDeclarationAst の両関数のシグネチャが変更されました。
変更前:
export function constantFoldDeclaration(input: string, rem: number | null = null): string
export function constantFoldDeclarationAst(
ast: ValueParser.ValueAstNode[],
rem: number | null = null,
): [folded: boolean, ast: ValueParser.ValueAstNode[]]
変更後:
export function constantFoldDeclaration(
input: string,
rem: number | null = null,
normalizeUnit = true,
): string
export function constantFoldDeclarationAst(
ast: ValueParser.ValueAstNode[],
rem: number | null = null,
normalizeUnit = true,
): [folded: boolean, ast: ValueParser.ValueAstNode[]]
canonicalizeDimension 内部関数も同様に normalizeUnit パラメータを受け取るよう拡張され、normalizeUnit = false の場合は単位変換をスキップする分岐が追加されています。
任意値の最適化を担う packages/tailwindcss/src/canonicalize-candidates.ts の optimizeArbitraryValueExpressions 関数では、constantFoldDeclarationAst の呼び出し箇所2か所に false を渡すよう変更されました。
変更前:
let [folded, foldedValueAst] = constantFoldDeclarationAst(valueAst)
// ...
let [folded, foldedExpressionAst] = constantFoldDeclarationAst(expressionAst)
変更後:
let [folded, foldedValueAst] = constantFoldDeclarationAst(valueAst, null, false)
// ...
let [folded, foldedExpressionAst] = constantFoldDeclarationAst(expressionAst, null, false)
これにより、任意値の処理では定数畳み込みのみが実行され、単位の変換は行われません。変換の結果は以下のようになります:
-
-mt-[20in]→mt-[-20in](従来はmt-[-1920px]) -
-mt-[0.04in]→mt-[-0.04in](従来はmt-[-3.84px])
デフォルト値は normalizeUnit = true のまま維持されており、シグネチャ計算など単位正規化が必要な既存の呼び出し箇所への影響はありません。
設計判断
normalizeUnit フラグによる関数の拡張という方式が採用されました。
単位正規化を行う処理と行わない処理を別関数として切り出す設計も考えられますが、今回は既存の constantFoldDeclarationAst を拡張するアプローチが選ばれています。デフォルト値を true にすることで、既存の呼び出し箇所への影響をゼロに抑えつつ、任意値処理の呼び出し側だけが false を明示的に渡す形になっています。
修正の対象はあくまで「任意値のキャノニカライゼーション」という文脈に限定されています。シグネチャ計算において .foo { margin-top: 20in; } と .bar { margin-top: 1920px; } を同一とみなす動作は変更されておらず、単位正規化が必要な箇所と不要な箇所を関数の引数レベルで分離することで、それぞれの正しい動作を保っています。
まとめ
本PRは、定数畳み込みと単位正規化という2つの独立した処理が意図せず結合していたことで生じたバグを、normalizeUnit フラグによる制御の導入で解消したものです。任意値の文脈では単位をそのまま保持するという直感的な動作が回復され、in や cm など非px単位を意図的に使用するユースケースでも正しいキャノニカライゼーションが行われるようになりました。