Viteプラグインの`@import`パス解決バグを修正:`base`から`importer`へ
@tailwindcss/viteにおいて、ネストしたディレクトリ構造で同名のCSSファイルが存在する場合に@importが誤ったファイルを参照するバグが修正されました。原因は、Viteのリゾルバにbaseディレクトリを渡していたことで、正しくはimporterファイルのパスを渡す必要がありました。
背景
@tailwindcss/vite において、Viteのエイリアス対応として最近導入されたaliasOnly: trueを使ったリゾルバが、意図せずパス解決のバグを引き起こしていました。#19956で報告されたこの問題は、サブディレクトリ内のCSSファイルが親ディレクトリの同名ファイルを誤って参照するというものです。
具体的には、以下のような構成で問題が発生します。
src/entry.css ← 親ディレクトリにも同名ファイル
src/themes/glow.css ← @import './entry.css' を含む
src/themes/entry.css ← 本来解決されるべきファイル
src/themes/glow.css内の@import './entry.css'はsrc/themes/entry.cssを参照するべきところ、実際にはsrc/entry.cssが解決されていました。この誤解決は、誤ったCSSファイルがインクルードされることによる生成ファイルのサイズ肥大化という問題を引き起こしていました。
根本原因は、ViteのリゾルバAPIに渡すパラメータの意味の違いにあります。Viteはpath.dirname(importer)でベースパスを導出するため、importerにはディレクトリではなくファイルパスを渡さなければなりません。ディレクトリを渡した場合、path.dirnameが一段上のディレクトリを返してしまいます。
// ディレクトリを渡した場合(バグ)
path.dirname('/path/to/my-project') // => /path/to
// ファイルパスを渡した場合(正しい)
path.dirname('/path/to/my-project/index.css') // => /path/to/my-project
技術的な変更
packages/@tailwindcss-vite/src/index.tsにおいて、customCssResolverの実装が createCustomResolver 関数として切り出され、baseディレクトリの扱いが修正されました。
核心的な変更は、リゾルバに渡すimporterの生成方法です。baseディレクトリをそのまま渡すのではなく、プレースホルダーファイル名を付加することでViteが正しくベースパスを導出できるようにしています。
変更前(概念):
// base(ディレクトリ)をそのままimporterとして渡していた
let resolved = await resolver(id, base)
変更後:
function createCustomResolver(
resolvers: ((id: string, importer: string) => Promise<string | undefined>)[],
filter = (_path: string) => true,
) {
return async (id: string, base: string) => {
// ダミーのファイル名を付加してimporterを構築する
// Viteはpath.dirname(importer)でベースパスを導出するため、
// ディレクトリではなくファイルパスである必要がある
let importer = path.resolve(base, '__placeholder__.css')
for (let resolver of resolvers) {
let resolved = await resolver(id, importer)
if (!resolved) continue
if (resolved === id) continue
// 相対パスの場合は絶対パスに変換
if (resolved[0] === '.') resolved = path.resolve(base, resolved)
// 追加フィルタ(例:.cssファイルのみ)の検証
if (!filter(resolved)) continue
// 絶対パスでない場合はディスクから読み取れないためスキップ
if (!path.isAbsolute(resolved)) continue
return resolved
}
}
}
また、リゾルバの試行順序も整理されています。aliasOnly: true(Viteエイリアスのみ解決)を先に試み、失敗した場合にaliasOnly: false(通常解決)にフォールバックする構造になりました。CSSリゾルバは.cssファイルのみを受け入れ、JSリゾルバは.cssファイルを除外するフィルタがfilterパラメータで表現されています。これはdaisyUIのようにpackage.jsonの"browser"フィールドがCSSファイルを指している場合に、@pluginがJSエントリを正しく解決するための配慮です(#19950)。
設計判断
プレースホルダーファイルによるimporter構築という手法が採用されました。
実際にはインポート元ファイルが何であるかをこの時点では特定できないため、__placeholder__.cssというダミーファイル名を使用しています。Viteはpath.dirname(importer)でディレクトリを導出するだけなので、ファイル名自体はダミーで問題ありません。重要なのは「ファイルパスであること」であり、この制約を__placeholder__という名前で明示することで、コードの意図を保ちつつ問題を回避しています。
また、createCustomResolverでCSSリゾルバとJSリゾルバのロジックを共通化しつつ、filterパラメータで拡張子ポリシーを分離したことで、今後のリゾルバ追加時の見通しも改善されています。
まとめ
この修正は、Viteのリゾルバに渡すパラメータの意味を正確に理解することで、__placeholder__.cssという単純な手法でパス解決の不整合を根本解決しています。aliasOnlyの試行順序整理とfilterによるCSS/JS分離を組み合わせることで、エイリアス対応(#19946)、daisyUI対応(#19950)、相対パス解決(#19956)の三つの問題を同時に解消する形になっています。