webpackローダーのキャッシュキーにリソースクエリを含めてキャッシュ汚染を修正
@tailwindcss/webpack のローダーが、同一CSSファイルを異なる resourceQuery でインポートした際にキャッシュエントリを共有してしまうバグを修正しました。キャッシュキーを this.resourcePath(パスのみ)から this.resource(パス+クエリ)に変更することで、各エントリが独立したキャッシュを持つようになります。
背景
同一CSSファイルを異なるクエリパラメータ付きでインポートすると、全インポートが単一の CacheEntry を共有してしまう問題がありました。Webpackのローダーは this.resourcePath(ファイルパスのみ)と this.resource(パス+クエリ文字列)の両方を提供しますが、@tailwindcss/webpack はキャッシュキーの構築に this.resourcePath のみを使用していました。そのため、styles.css?entryA と styles.css?entryB のように同じファイルを異なるクエリで参照すると、同一の CacheEntry にマッピングされてしまいます。
この結果、あるエントリ向けに検出されたユーティリティクラスが別エントリのCSS出力に「漏れ出す」キャッシュ汚染が発生していました。マルチエントリ構成のwebpackプロジェクトでCSSの分離が保証されない、深刻な問題です。
技術的な変更
キャッシュキーの生成ロジックを getCacheKey 関数として切り出し、キーに使用するリソース識別子を resourceId(this.resource)に統一しました。
変更前:
function getContextFromCache(inputFile: string, opts: LoaderOptions): CacheEntry {
let key = `${inputFile}:${opts.base ?? ''}:${JSON.stringify(opts.optimize)}`
// ...
}
変更後:
function getCacheKey(resourceId: string, opts: LoaderOptions): string {
return `${resourceId}:${opts.base ?? ''}:${JSON.stringify(opts.optimize)}`
}
function getContextFromCache(resourceId: string, opts: LoaderOptions): CacheEntry {
let key = getCacheKey(resourceId, opts)
// ...
}
ローダー本体では、this.resourcePath を inputFile として保持しつつ、キャッシュ参照には this.resource を resourceId として使い分けます。ファイルシステム操作(path.dirname(path.resolve(inputFile)) など)は引き続き inputFile を使用するため、クエリ文字列がパス解決に混入する副作用はありません。
let inputFile = this.resourcePath // ファイルシステム操作用
let resourceId = this.resource // キャッシュキー用(パス+クエリ)
エラー発生時のキャッシュ削除パスも getCacheKey(resourceId, options) を使うよう修正され、エラー時の再ビルド処理でも一貫したキーが使われるようになりました。
設計判断
this.resource をキャッシュキーに使い、ファイルI/Oには this.resourcePath を使い続ける という責務の分離が採用されています。
この設計により、styles.css?entryA と styles.css?entryB はそれぞれ独立したキャッシュエントリを持ちながら、両者のファイル読み込みは同一のパス(styles.css)を正しく参照します。キャッシュキーを変えるだけで、ファイルシステム周りのロジックは一切変更不要という、最小限の修正で問題を解決しています。
また、キャッシュキー生成を getCacheKey 関数に切り出したことで、通常パスとエラーパスで同じキー文字列を使う保証が得られます。変更前はキー文字列をインラインで2箇所に記述していたため、将来的な変更時に片方を更新し忘れるリスクがありました。
まとめ
この修正は、webpackの resourcePath と resource の違いを正確に把握した上で、キャッシュキーの責務を正しいプロパティに割り当てた変更です。変更規模は小さいものの、マルチエントリ構成でのCSS出力の正確性を保証する上で重要な修正であり、キャッシュ設計における「キーは一意性を担保すべき識別子で構成する」という原則の好例といえます。