Symlink パスを保持し @source/glob の順序を正しく保つ修正

tailwindlabs/tailwindcss

Tailwind CSS v4 では @source@source not のディレクティブがシンボリックリンクを通過した際にパスが正規化され、元の記述が失われて無視設定が機能しない問題がありました。本 PR はシンボリックリンク情報を保持しつつ、.gitignore 生成時のパターン順序を宣言順に固定することで、#17985 を解消します。これにより開発者は意図した通りにファイルのインクルード/エクスクルードが可能になります。

背景

@source ディレクティブは内部で base パスと pattern を組み合わせたエントリにマッピングされ、dunce::canonicalize によりベースパスが実際のディスク上の実体に解決されます。この解決過程でシンボリックリンクが辿られ、@source "./symlinked-folder/**/*.html"実体パス に変換されてしまい、元のシンボリックリンク名が失われました。その結果、@source not "./symlinked-folder" が期待した無視対象にならず、Issue #17985 に記されたビルドエラーが発生していました。

さらに、.gitignore を生成する際は BTreeMapBTreeSet による自動ソートが行われ、パターンの順序が宣言順と逆転しました。@source not の後に @source が続くケースでは、無視設定が上書きされてしまい、テストでも指摘されたように期待したファイルが除外されませんでした。これら二つの欠陥が同時に顕在化したことが本修正の動機です。

技術的な変更

scanner/mod.rs のデータ構造置換

crate::scanner::mod.rs では BTreeMap<&PathBuf, BTreeSet>Vec<(&PathBuf, Vec)> に置き換え、emit クロージャで同一ベースへの連続追加を保持します。これによりパターンは宣言順に蓄積され、ソートが排除されます。

@@ -17,7 +17,6 @@ use fxhash::{FxHashMap, FxHashSet};
-use std::collections::{BTreeMap, BTreeSet};
@@
-    let mut ignores: BTreeMap<&PathBuf, BTreeSet<String>> = Default::default();
+    let mut ignores: Vec<(&PathBuf, Vec<String>)> = Default::default();
+    let mut emit = |base, pattern| match ignores.last_mut() {
+        Some((prev_base, patterns)) if *prev_base == base => {
+            patterns.push(pattern);
+        }
+        _ => {
+            ignores.push((base, vec![pattern]));
+        }
+    };
@@
-                    ignores
-                        .entry(base)
-                        .or_default()
-                        .insert(format!("!{}", pattern));
+                    emit(base, format!("!{}", pattern));

この変更は パターンの順序保持シンボリックリンク情報の喪失防止 の二重目的を満たす最小侵入的実装です。既存コードは emit 呼び出しに合わせて軽微な置換のみで済み、他のロジックへの副作用はありません。

scanner/sources.rs の最適化ロジック改良

crate::scanner::sources.rs ではベースパスの正規化はそのままに、パターン側のコンポーネント解析 を導入し、... を考慮しつつワイルドカードが出現したタイミングでベースの変更を止めます。これによりシンボリックリンクが含まれるパスでも base は実体に変換されますが、pattern 部分は元の文字列を保持します。

@@
-        let Ok(base) = dunce::canonicalize(&self.base) else {
+        let Ok(mut base) = dunce::canonicalize(&self.base) else {
@@
-        let mut new_pattern = PathBuf::new();
+        let mut new_pattern = PathBuf::new();
@@
-                        Component::Normal(part) if part.to_string_lossy().contains("*") => {
+                        Component::Normal(part) if part.to_string_lossy().contains("*") => {

このロジックは パス全体を正規化しつつ、ワイルドカード以降の文字列をそのまま保持 することで、@source 記述と実際の走査パスを一致させます。結果として @source not "./symlink" が正しく無視対象となり、Issue #17985 が解消されます。

テスト・CI の拡張

  • crates/oxide/tests/scanner.rs にシンボリックリンクと宣言順を検証する新規テストを追加し、it_should_preserve_source_order_when_referencing_a_sibling_project で期待結果を明示しました。
  • integrations/utils.tssymlink ヘルパーを実装し、プラットフォーム横断でシンボリックリンクを作成可能にしました。
  • integrations/cli/index.test.ts で実際の CLI 実行時にシンボリックリンク経由の @source が正しい順序で処理されることをエンドツーエンドで検証しています。

これらのテストはすべての OS で走るように CI の ci-all ジョブに組み込まれ、回帰防止の安全ネットとして機能します。

設計判断

データ構造の選択変更

BTreeMap/BTreeSet は自動ソートが利点ですが、@source宣言順が意味を持つ 場面では不適切です。開発者は「先に無視し、後で再度インクルード」する意図を持って記述するため、Vec と手動管理 が自然な選択となります。この変更は API 互換性を損なわず、内部だけで順序保証を行うシンプルな戦略です。

シンボリックリンクの扱い方

dunce::canonicalize はベースパスの実体化に有用ですが、パターン部分は文字列通り に残す必要があります。今回の実装はコンポーネント単位で走査し、ワイルドカードに遭遇した時点でベース操作を停止するという 段階的最適化 を採用しています。これによりシンボリックリンクの情報が失われず、元の @source 記述とマッピングが 1:1 で保持されます。

テストフレームワークの拡張

シンボリックリンクはプラットフォーム依存の副作用が強いため、テストユーティリティに symlink 関数を追加し、Unix と Windows の両方で同一ロジックを検証できるようにしました。これにより クロスプラットフォームの信頼性 が担保され、将来の変更でも同様の問題が再発しにくくなります。

まとめ

この PR は @source 系ディレクティブがシンボリックリンクを経由した際にパス情報を失わないようにし、かつ .gitignore 生成時のパターン順序を宣言通りに保持することで、Issue #17985 とその付随する順序バグを根本的に解決しました。内部実装はデータ構造の置換とパス解析ロジックの改善に絞られ、既存ユーザーへの破壊的影響はありません。今後はシンボリックリンクを含むプロジェクト構成でも安定したスキャン結果が得られ、Tailwind CSS のビルドパイプラインがより堅牢になります。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
331f737e

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

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

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

リード文 → 背景 → 技術的な変更 → 設計判断(任意) → まとめ の3部構成が明確に存在し、リード文は要約、まとめは繰り返しでない。

カスタムMarkdown構文 ⚠ WARNING

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

コードブロックは正しいdiffハイライトだが、ファイル名付きシンタックスハイライトは使用されていない。GitHubリンクは `[PR #20203](URL)` のように PR 番号が `#` とリンク化されていないため、記法が要件と完全に合致していない。

対象読者への適合性 ✓ PASS

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

エンジニア向けに専門用語と実装詳細が中心で、初心者向けの過度な説明はない。

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

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

各セクションは総論パラグラフで要旨を示し、以降は具体的事実・コードで裏付け、最後に結論パラグラフでまとめている。段落はトピックセンテンスで始まり、1段落1トピック、長さも適切。

Diff内容との照合 ✓ PASS

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

記事内のdiffコードは提供されたDiffと整合しており、削除・追加内容は正しく反映されている。抜粋部分も誤りなく要点を示している。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`@source`, `symlink`, `BTreeMap`, `Vec` などの用語はPRと一致し、誤用は見られない。

説明の技術的正確性 ✓ PASS

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

技術的な主張はPRの説明とコード変更に裏付けられ、因果関係も論理的。

事実の突合 ✓ PASS

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

全ての事実(Issue #17985 の解決、順序保持の重要性等)はPR情報またはDiffで裏付けられており、推測や矛盾はない。

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

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

PR番号 #20203、Issue番号 #17985 などの数値は正確に記載されている。

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

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

記事タイトルはPRの主旨「symlink を保持し @source globs を正しく処理する」ことを日本語で端的に表現しており、内容と一致している。

外部知識の正確性 ✓ PASS

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

LTS、リリース日程、バージョンサポート等、PRに記載のない外部知識は含まれていない。

時間表現の正確性 ✓ PASS

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

時間表現の歪曲はなく、現在・過去の記述はPR情報と整合している。