Viteプラグインがサーバー専用モジュールで誤って全リロードを引き起こす問題を修正
SSRフレームワークが管理するサーバー専用モジュールの変更時に、@tailwindcss/viteプラグインが誤ってクライアントの全リロードを強制していた問題を修正しました。これにより、React RouterのHDR(Hot Data Revalidation)やWakuなどのSSRフレームワークにおけるHMRが正常に動作するようになります。
背景
#19670で導入されたhandleHotUpdateフックが、SSRフレームワーク固有のサーバー専用モジュールを考慮していませんでした。このフックはVite 7.1+での外部ファイル変更時に全リロードを正しくトリガーするために実装されましたが、SSRフレームワークが管理するサーバー専用モジュールまで誤って全リロードの対象としていました。
具体的には、クライアント側のCSSスキャン時にサーバー専用モジュールが走査された場合、そのモジュールへの変更がクライアントの全リロードを引き起こしていました。React RouterのHDRのように、サーバー側のローダー依存ファイルを変更した際にクライアント状態を保ちながらデータのみを再取得する仕組みが、全リロードによって破壊される状況でした(#19744)。同様の問題は@vitejs/plugin-rscやwakuでも確認されており、複数のSSRフレームワークに影響を与えていました。
この問題の核心は、ViteのEnvironment APIが提供する複数の環境(client、ssrなど)にまたがるモジュールグラフの扱いにあります。SSRフレームワークはサーバー専用モジュールに対して独自のHMR・リロード機構を持っており、クライアント側が介入すべきではありません。
技術的な変更
packages/@tailwindcss-vite/src/index.tsのhandleHotUpdateフック内に、他の環境のモジュールグラフを参照するチェックが追加されました。
変更前は、変更されたファイルに対応するモジュールがすべてasset型かid未定義であれば、外部ファイルと判断して全リロードを実行していました。
変更前:
let isExternalFile = modules.every((mod) => mod.type === 'asset' || mod.id === undefined)
if (!isExternalFile) return
for (let env of new Set([this.environment.name, 'client'])) {
// ... full reload logic
}
変更後:
let isExternalFile =
modules.length > 0 &&
modules.every((mod) => mod.type === 'asset' || mod.id === undefined)
if (!isExternalFile) return
// Skip if the module exists in other environments. SSR framework has
// its own server side hmr/reload mechanism when handling server
// only modules.
for (let environment of Object.values(server.environments)) {
if (environment.name === this.environment.name) continue
let modules = environment.moduleGraph.getModulesByFile(file)
if (modules) {
for (let module of modules) {
if (module.type !== 'asset') {
return
}
}
}
}
for (let env of new Set([this.environment.name, 'client'])) {
// ... full reload logic
}
変更は2点あります。第一に、modules.length > 0のチェックが追加されました。これにより、モジュールが空の場合(addWatchFile経由ではないファイル)は外部ファイル判定をスキップします。第二に、現在の環境以外の全環境のmoduleGraph.getModulesByFileを参照し、変更ファイルが他の環境でも非asset型モジュールとして管理されている場合は全リロードを中止します。
検証として、React RouterのHDR(サーバーローダーのHMR)を対象とした統合テストがintegrations/vite/react-router.test.tsに追加されています。サーバー専用のdirect-hdr-dep.tsを変更した際に、クライアント入力状態を保ちつつHDRが発火することを確認するテストで、修正前はmainブランチで失敗します。
設計判断
他環境のモジュールグラフを参照して全リロードをスキップするアプローチが採用されました。
ViteのEnvironment APIでは、server.environmentsに各環境(client、ssr、RSCフレームワーク固有の環境など)のモジュールグラフが独立して管理されています。あるファイルが現在の環境(クライアント)ではasset型として見えても、別の環境(SSR)では通常のJSモジュールとして管理されている場合、そのファイルの更新はSSR環境側のHMR機構に委ねるべきです。この判断はVite公式ドキュメントの「SSR専用モジュールへの更新はクライアントの全リロードをトリガーしない」という原則と一致しています。
このアプローチにより、Tailwindプラグインは自身が担当すべき範囲(TailwindのCSSに影響する外部ファイルの変更)に処理を限定し、SSRフレームワーク固有のモジュール管理には介入しない設計になっています。
まとめ
本修正は、マルチ環境構成を持つVite向けSSRフレームワークとの協調動作を改善する変更です。全リロードを実行する前に他環境のモジュールグラフを参照するという1つの判断が、React Router、@vitejs/plugin-rsc、Wakuといった複数のSSRフレームワークで発生していたHMR破壊を解消しています。