削除されたファイルが原因の Vite HMR クラッシュを防止
Tailwind CSS の @tailwindcss/vite プラグインが、ファイル削除時に Vite の Hot Module Replacement (HMR) がクラッシュする問題を解消し、削除されたパスが再度監視対象になることを防ぎました。
背景
Vite は addWatchFile API を通じて Tailwind のスキャナが検出したファイルを監視しますが、削除されたファイルがスキャナ内部の集合に残ると、次回の HMR サイクルでそのパスを再度開こうとして ENOENT エラーが発生し、開発サーバーが停止します。Issue #17532 で報告されたこの現象は、SVG 等のアセットを削除しただけで再現され、開発フローを著しく阻害していました。
この根本原因は、スキャナがファイルシステムの再走査時に削除されたエントリを除去しない点にありました。したがって、スキャナが最新のファイル集合を正しく反映できるように状態をリセットする必要がありました。
技術的な変更
Scanner の再走査ロジックに、走査開始前に内部コレクションをクリアする処理が追加されました。具体的には crates/oxide/src/scanner/mod.rs の discover_sources メソッド冒頭で以下のコードが挿入されています。
@@
- let mut seen_files: FxHashSet<PathBuf> = FxHashSet::default();
+ // Fresh state for each scan
+ self.files.clear();
+ self.dirs.clear();
+ self.extensions.clear();
+ self.globs = None;
同時に、重複除去の手段が seen_files から self.files への直接挿入へ置き換えられ、拡張子情報も同時に格納されるようになっています。
@@
- if !seen_files.insert(path.clone()) {
+ if !self.files.insert(path.clone()) {
continue;
}
+ self.extensions.insert(extension.clone());
さらに、変更検知のロジックが path.metadata().ok().and_then(|m| m.modified().ok()) を用いて mtime を取得し、スキャンが初回でない場合にのみ未変更ファイルをスキップするよう整理されています。これにより、削除されたファイルは再走査時にコレクションから除外され、addWatchFile に渡されることがなくなります。
設計判断
今回の実装は 状態リセット に重点を置き、スキャナが保持するファイル集合・ディレクトリ集合・拡張子集合・glob 設定を走査毎に完全に再構築する方針を採用しました。これにより、削除検知ロジックを個別に追跡・除去する複雑性を回避し、既存 API(addWatchFile への呼び出し)への影響を最小限に抑えています。
一方で candidates は新しいコンパイラインスタンスが生成されるまで append‑only のまま保持され、スキャンのリセット対象外としています。この設計は、ビルドパイプラインの安定性を保ちつつ、削除されたファイルが再度ウォッチされるリスクだけを解消するというトレードオフを明示しています。
まとめ
スキャナの内部集合を走査毎にクリアし、削除済みパスを除去する変更により、@tailwindcss/vite が Vite の HMR 中にクラッシュする問題が解消されました。変更は Scanner の状態管理 に限定され、既存の挙動や API への影響は最小限に抑えられています。