`@plugin` がブラウザCSS エントリを誤って解決するリグレッションを修正
@tailwindcss/vite において、@plugin "daisyui" のようなパッケージ指定が JavaScript エントリではなく browser フィールドのCSS ファイルに解決される不具合を修正しました。2段階のリゾルブ戦略により、Vite エイリアスと tsconfig パス解決の両立を維持しながら、このリグレッションを解消しています。
背景
#19803 で aliasOnly フラグを true から false に変更したことで、resolve.tsconfigPaths: true が @plugin 解決に効くようになりました。しかしこの変更が、意図しない副作用を生みました。
aliasOnly: false の状態では Vite のフルリゾルバーパイプラインが走るため、daisyUI のように package.json の browser フィールドにCSS ファイルを指定しているパッケージでは、Vite がそのCSS エントリを優先して返してしまいます。結果として Tailwind が .css ファイルを JavaScript プラグインとして読み込もうとし、#19950 で報告された次のエラーが発生していました。
Unknown file extension ".css"
技術的な変更
customJsResolver を2段階のリゾルブ戦略に書き直し、エイリアス優先解決とフルパイプライン解決を順に試みる構成にしました。
変更前:
customJsResolver = async (id: string, base: string) => {
let resolved = await jsResolver(id, base, false, isSSR)
if (!resolved) return
if (resolved === id) return
if (!path.isAbsolute(resolved)) return
return resolved
}
変更後:
customJsResolver = async (id: string, base: string) => {
// Resolve Vite aliases first so `@plugin "@/foo"` keeps working, but
// let bare package specifiers fall through to Node-style resolution.
let resolved = await jsResolver(id, base, true, isSSR)
if (resolved && resolved !== id) {
if (path.isAbsolute(resolved)) return resolved
if (resolved[0] === '.') return path.resolve(base, resolved)
}
// Fall back to Vite's full resolver for features like tsconfigPaths,
// but reject CSS results since plugins must resolve to executable code.
resolved = await jsResolver(id, base, false, isSSR)
if (!resolved) return
if (resolved === id) return
if (!path.isAbsolute(resolved)) return
if (resolved.endsWith('.css')) return
return resolved
}
第1フェーズでは aliasOnly: true でリゾルバーを呼び出します。@/foo のような Vite エイリアスが絶対パスや相対パスに解決された場合はその結果をそのまま返し、処理を完了します。エイリアス解決に失敗したベアパッケージ指定(daisyui 等)は第1フェーズをスルーし、第2フェーズへ進みます。
第2フェーズでは aliasOnly: false でフルリゾルバーパイプラインを実行し、tsconfig パス解決などの高度な機能を有効化します。ここで得た解決結果が .css で終わる場合は undefined を返し、Tailwind 内部のフォールバックリゾルバーに処理を委ねます。この同じロジックが、Vite 4系向けの env を受け取る実装にも対称的に適用されています。
設計判断
ファイル拡張子チェック(.css ガード)を第2フェーズにのみ適用する設計が採用されました。
PR の説明では、ハードコードしたJS拡張子リストで許可する方式ではなく、.css を明示的に拒否する方式が選ばれています。JavaScript の拡張子は .js / .mjs / .ts / .mts など多様であり、ホワイトリスト管理は漏れが生じやすいためです。一方でCSS を browser エントリに置くパターンは daisyUI のような実際のパッケージで確認されたアンチパターンであり、明確に拒否すべき対象として扱っています。
第1フェーズでエイリアスを先に解決することで、@plugin "@/foo" のような Vite エイリアスが意図しくフルパイプラインに流れ込むことも防いでいます。2段階構成にすることで、「エイリアス解決」と「tsconfig パス解決」という異なる機能を独立したフェーズに分離し、それぞれの副作用を制御可能にしている点が本変更の核心です。
まとめ
本PRは、aliasOnly フラグの変更で生じたリグレッションを、リゾルブ処理を2フェーズに分割することで解決しています。エイリアス解決とフルパイプライン解決を段階的に試みつつ、CSS エントリへの誤解決を明示的に拒否することで、daisyUI のような browser フィールドにCSS を持つパッケージと tsconfig パス解決の双方に対応できる堅牢な設計を実現しました。