Skip to content

fix: restore full page reload for watched external files on Vite 7.1+#19670

Open
Arxsher wants to merge 3 commits intotailwindlabs:mainfrom
Arxsher:fix/vite-7-1-reload-regression
Open

fix: restore full page reload for watched external files on Vite 7.1+#19670
Arxsher wants to merge 3 commits intotailwindlabs:mainfrom
Arxsher:fix/vite-7-1-reload-regression

Conversation

@Arxsher
Copy link

@Arxsher Arxsher commented Feb 13, 2026

PR: Fix @source file changes not triggering full page reload on Vite 7.1+

Description

This PR addresses issue #19637 where template files (PHP, HTML, Blade, etc.) watched via the @source directive fail to trigger a full page reload when using Vite 7.1 or newer.

Root Cause

Vite 7.1 introduced the Environment API, which supersedes the legacy WebSocket API for HMR. Specifically:

  • server.ws.send is deprecated/ignored for certain external file updates in favor of server.hot.send.
  • The @tailwindcss/vite plugin currently collects ViteDevServer instances but lacks a handleHotUpdate hook to explicitly trigger reloads for non-module files added via addWatchFile.

Changes

  • Implemented a handleHotUpdate hook in the @tailwindcss/vite plugin.
  • The hook identifies changes to files that are not part of the standard Vite module graph (e.g., .php, .html) but are watched by Tailwind.
  • Triggers a full-reload using the new server.hot.send API if available (Vite 7.1+), with a fallback to server.ws.send for backward compatibility.

Verification

  • Reproduced the issue in a standalone Vite 7.1.0 project using a mock plugin with the legacy API.
  • Confirmed that the browser fails to reload upon editing a watched .php file.
  • Verified that migrating to server.hot.send restores the expected reload behavior.

@Arxsher Arxsher requested a review from a team as a code owner February 13, 2026 01:18
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 13, 2026

Walkthrough

A new handleHotUpdate hook was added to the Tailwind Vite plugin. When a hot-updated file is not in the module graph, the hook checks whether the file resides in any watched directory and, if so, sends a full-reload payload ({ type: 'full-reload', path: '*' }) via server.hot or server.ws. The hook returns an empty array after initiating the reload. No changes were made to config resolution or other plugin steps.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: adding a handleHotUpdate hook to restore full page reload for watched external files when using Vite 7.1+, which directly matches the changeset.
Description check ✅ Passed The description provides comprehensive context about the issue, root cause, implementation details, and verification steps, all directly related to the changeset and its objectives.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


No actionable comments were generated in the recent review. 🎉

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@packages/`@tailwindcss-vite/src/index.ts:
- Around line 98-115: The current handleHotUpdate logic uses
files.includes(path.basename(file)) which can false-positive across different
watched directories; update the check to compare full resolved paths instead —
for the server.watcher.getWatched() loop, replace the basename check with
something like files.some(f => path.resolve(dir, f) === path.resolve(file)) so
the code only matches when the exact watched file path equals the changed file;
keep the rest of handleHotUpdate (including server.moduleGraph.getModulesByFile
and the full-reload payload send) unchanged.
- Around line 104-106: The reload check uses
Array.from(server.watcher.getWatched()) which returns [] because getWatched()
returns a plain object; replace Array.from(...) with
Object.entries(server.watcher.getWatched()) and keep the .some(...) logic over
([dir, files]) to locate the watched directory; also tighten the directory check
in the callback by preventing prefix false-matches—change file.startsWith(dir)
to ensure a directory boundary (e.g., file.startsWith(dir + path.sep) or
comparable check) while still using path.basename(file) to check the file name.
🧹 Nitpick comments (1)
packages/@tailwindcss-vite/src/index.ts (1)

98-115: Consider returning an empty array to suppress duplicate default HMR handling.

When handleHotUpdate doesn't return a value, Vite continues its default HMR pipeline. For non-module files this is typically harmless, but explicitly returning [] (empty modules array) after sending the full-reload makes intent clear and avoids potential double-processing.

Suggested change
         if (server.hot) {
           server.hot.send(payload)
         } else {
           server.ws.send(payload)
         }
+        return []
       }

Comment on lines 98 to 115
handleHotUpdate({ server, file }) {
// If the changed file is being watched by Tailwind but isn't part of the
// module graph (like a PHP or HTML file), we need to trigger a full
// reload manually.
if (
!server.moduleGraph.getModulesByFile(file) &&
Array.from(server.watcher.getWatched()).some(([dir, files]) => {
return file.startsWith(dir) && files.includes(path.basename(file))
})
) {
const payload = { type: 'full-reload' as const, path: '*' }
if (server.hot) {
server.hot.send(payload)
} else {
server.ws.send(payload)
}
}
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

path.basename match can false-positive across directories.

files.includes(path.basename(file)) only checks the filename, so two different watched directories each containing e.g. index.php would both match. This is minor when the consequence is just a full-reload, but it means reloads fire for unrelated file changes. Consider comparing the full resolved path instead of just the basename.

🤖 Prompt for AI Agents
In `@packages/`@tailwindcss-vite/src/index.ts around lines 98 - 115, The current
handleHotUpdate logic uses files.includes(path.basename(file)) which can
false-positive across different watched directories; update the check to compare
full resolved paths instead — for the server.watcher.getWatched() loop, replace
the basename check with something like files.some(f => path.resolve(dir, f) ===
path.resolve(file)) so the code only matches when the exact watched file path
equals the changed file; keep the rest of handleHotUpdate (including
server.moduleGraph.getModulesByFile and the full-reload payload send) unchanged.

@Arxsher
Copy link
Author

Arxsher commented Feb 13, 2026

Thanks for the cleanup @RobinMalfait, Appreciate the help getting this ready.

@RobinMalfait
Copy link
Member

@Arxsher thank you for the PR! Just doing a few more tests here and then I think we can get it merged!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants