Object.prototypeプロパティによるクラッシュを防ぐガード処理の追加
Tailwind CSS v4で、ソースファイルに row-constructor などの文字列が含まれると「V.map is not a function」エラーでビルドが失敗する問題が修正されました。ユーザー入力値をキーとしたオブジェクト参照時に Object.hasOwn() によるチェックを追加することで、継承されたプロトタイプのプロパティとの衝突を防いでいます。
背景
Tailwind CSS v4のユーティリティスキャン処理では、ソースコード中の文字列を候補値として抽出し、設定オブジェクトやプラグインの値定義から対応するスタイルを検索します。#19721 で報告されたように、row-constructor という文字列がソースに含まれると、row-* ユーティリティの候補値として constructor が抽出され、この値をキーにしたオブジェクト参照が Object.prototype.constructor にマッチしてしまいました。
Object.prototype.constructor は関数オブジェクトであるため、配列として扱おうとした後続処理で「V.map is not a function」というエラーが発生します。同様に hasOwnProperty、toString、valueOf、__proto__ などのプロトタイプチェーン上のプロパティも、ユーザー入力値として使用された場合に予期しない値を返す可能性がありました。
この問題は、JavaScriptのオブジェクトが通常 {} リテラルや new Object() で作成され、デフォルトで Object.prototype を継承することに起因します。プロパティの存在チェックに in 演算子や obj[key] !== undefined を使用すると、継承されたプロパティも検出されてしまいます。
技術的な変更
修正は、ユーザー入力値をキーとしたオブジェクト参照が行われる3つのファイルに Object.hasOwn() チェックを追加する形で実装されました。
utilities.ts: 静的値の参照
utilities.ts の functionalUtility 関数では、ユーティリティ定義の staticValues オブジェクトを Object.create(null) で再作成することで、プロトタイプを持たないオブジェクトに変換しています。
function functionalUtility(classRoot: string, desc: UtilityDescription) {
+ if (desc.staticValues) desc.staticValues = Object.assign(Object.create(null), desc.staticValues)
+
function handleFunctionalUtility({ negative }: { negative: boolean }) {
Object.create(null) で作成されたオブジェクトは Object.prototype を継承しないため、constructor などのプロパティが存在しません。既存の staticValues のプロパティを Object.assign() でコピーすることで、元の値定義を保持したままプロトタイプチェーンを排除しています。
plugin-api.ts: プラグイン値とモディファイアの参照
plugin-api.ts では、matchVariant、matchUtilities の各所で Object.hasOwn() による明示的なチェックを追加しています。
variant値の参照(named variantの処理):
} else if (variant.value.kind === 'named' && options?.values) {
+ if (!Object.hasOwn(options.values, variant.value.value)) {
+ return null
+ }
let defaultValue = options.values[variant.value.value]
utility値の参照:
-} else if (values[candidate.value.value]) {
+} else if (Object.hasOwn(values, candidate.value.value)) {
value = values[candidate.value.value]
modifier値の参照:
-} else if (modifiers?.[candidate.modifier.value]) {
+} else if (modifiers && Object.hasOwn(modifiers, candidate.modifier.value)) {
modifier = modifiers[candidate.modifier.value]
これらの変更により、ユーザー入力の候補値が options.values や modifiers に実際に定義されているかを確認してから参照するようになりました。継承されたプロパティは Object.hasOwn() で false と判定されるため、undefined を返すか処理をスキップします。
plugin-functions.ts: config() 関数のパス走査
設定値を取得する get() 関数では、オブジェクトのパスを順に辿る際の条件判定を強化しています。
-// The key does not exist so concatenate it with the next key
-if (obj?.[key] === undefined) {
+// The key does not exist so concatenate it with the next key.
+// We use Object.hasOwn to avoid matching inherited prototype properties
+// (e.g. "constructor", "toString") when traversing config objects.
+if (obj === null || obj === undefined || typeof obj !== 'object' || !Object.hasOwn(obj, key)) {
変更前は obj?.[key] === undefined で存在チェックを行っていましたが、この条件では obj['constructor'] が Object.prototype.constructor を返すため、undefined にならず次の処理に進んでしまいます。修正後は、Object.hasOwn(obj, key) で自身のプロパティであることを明示的に確認するようになりました。
設計判断
このPRでは、既存のオブジェクト構造を変更せず、参照時のガード処理を追加する アプローチが採用されています。
2つの防御戦略が使い分けられています。utilities.ts では Object.create(null) による根本的な解決(プロトタイプチェーンの排除)を行う一方、plugin-api.ts と plugin-functions.ts では Object.hasOwn() による防御的なチェックを追加しています。後者のアプローチは、外部から渡されるオブジェクト(プラグインのオプションなど)のプロトタイプを制御できない場合でも機能します。
テストコードでは、constructor、hasOwnProperty、toString、valueOf の4つのプロトタイプメソッド名を候補値として使用した場合に、クラッシュせず出力も生成されないことを確認しています。これらは JavaScript のすべてのオブジェクトが継承する代表的なプロパティであり、ユーザー入力として十分に想定される文字列です。
まとめ
本PRは、ユーザー入力値をオブジェクトのキーとして使用する際の型安全性を向上させる修正です。Object.hasOwn() による明示的な所有プロパティチェックと、Object.create(null) によるプロトタイプレスオブジェクトの活用により、候補値が JavaScript の予約語やプロトタイプメソッド名と一致してもクラッシュしないようになりました。この変更は、Tailwind CSS v4 のスキャン処理の堅牢性を高め、任意の文字列を含むソースコードに対しても安定した動作を保証します。