Viteプラグインの`@import`パス解決バグを修正:`base`から`importer`へ

tailwindlabs/tailwindcss

@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)の三つの問題を同時に解消する形になっています。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
2ef95c0f

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
リトライ後承認
Review Count:
2回 (改善を経て承認)
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

「リード文(総論)→背景・技術詳細(各論)→まとめ(結論)」の3部構成が明確に守られています。設計判断セクションも設けられており、読者の深い理解を助ける構成になっています。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト(```typescript:packages/@tailwindcss-vite/src/index.ts)やGitHubのIssue/PRリンク([#19956](URL))の記法が正しく使われています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

Viteのリゾルバやパス解決といった専門的なトピックを扱っており、専門知識を持つエンジニアという対象読者に適切です。不要な初心者向け解説はありません。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論→各論で構成され、各段落はトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が遵守されています。これにより、非常に高い可読性が確保されています。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内で引用されている`createCustomResolver`関数は、提供されたDiffの内容と完全に一致しています。コードの引用は正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`importer`, `base`, `resolver`, `aliasOnly`といった技術用語が、PRの文脈に沿って正確に使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

「Viteは`path.dirname(importer)`でベースパスを導出する」という問題の根本原因や、「`__placeholder__.css`を付加してファイルパスとして渡す」という解決策の説明は、PR Descriptionの内容と整合が取れており、技術的に正確です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内の主張(ファイルサイズ肥大化の問題、daisyUIへの言及など)はすべてPR Descriptionで裏付けられており、ハルシネーション(捏造)は見られません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#19965)や関連するIssue番号(#19956, #19950, #19946)はすべて正確に記載されています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事タイトル「Viteプラグインの`@import`パス解決バグを修正:`base`から`importer`へ」は、PRの主題である相対パス解決問題の原因(base)と解決策(importer)を的確に要約しており、内容と完全に一致しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

記事の内容は提供されたPR情報に完全に準拠しており、サポート期間やリリース予定といったPR外の知識を持ち込んでいる箇所はありません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

「最近導入された」といった時間表現は、PR内の「recently started」という記述と一致しており、時間的な歪曲はありません。