Skip to content

Don't trigger a full page reload for scanned files that Vite processes as modules - #20336

Merged
RobinMalfait merged 3 commits into
mainfrom
fix/issue-20320
Jul 15, 2026
Merged

Don't trigger a full page reload for scanned files that Vite processes as modules#20336
RobinMalfait merged 3 commits into
mainfrom
fix/issue-20320

Conversation

@RobinMalfait

@RobinMalfait RobinMalfait commented Jul 15, 2026

Copy link
Copy Markdown
Member

This PR fixes an issue where editing a scanned file that Vite (or one of its plugins) can process as a module, but that isn't currently loaded, caused @tailwindcss/vite to force a full page reload, throwing away all client state.

The hotUpdate hook has a fallback that sends a full-reload for files that Tailwind scans but that Vite knows nothing about (e.g. .php or .blade.php templates rendered by a backend). Without it, edits to those files wouldn't refresh the page at all. To detect those files we check whether every module for the changed file is an asset and/or has no id, because the scanner's addWatchFile calls create exactly such placeholder nodes for every scanned file.

The problem is that a source file that Vite can process, but that isn't loaded yet, looks exactly the same. The realistic way to get into that state is code splitting: with route-level splitting (e.g. React.lazy, TanStack Router's autoCodeSplitting, lazy routes in vue-router), every component behind an un-visited split boundary only exists as a scan placeholder in the module graph. Editing any of them reloaded the whole app. The same happens for component stylesheets that a framework plugin compiles into the component (e.g. Angular via Analog), which never show up as their own module.

A full reload is never useful for these files: if the file is loaded, Vite's own HMR handles it, and if it isn't loaded, reloading the page won't load it either. Any new candidates still apply through the regular css-update flow because the file is registered via addWatchFile.

So instead, we now skip the fallback when the changed file is handled by Vite's module pipeline:

  • The file exists as a real module in another environment (e.g. an SSR-only module). This check already existed and is folded into the same code path.
  • The file is part of the JS/TS or CSS families, which Vite transforms natively.
  • For any other file type (e.g. .vue, .svelte, or .md with an SSG plugin), a file with the same extension exists as a real module in some environment's module graph, then a plugin does handle this file type and the changed file just isn't loaded (yet).

External templates like .php files still trigger a full reload exactly like before.

Fixes: #20320
Fixes: #19903
Closes: #20323

Test plan

  1. Added integration tests to ensure extensions handled by default rely on HMR
  2. Added integration tests to make sure that unknown extensions that have been handled already will also use HMR
  3. Manually tested that changing a .php file still triggers a full-reload
  4. Manually tested the reproduction where local client state isn't thrown away
file-14a86a90a1e4b810c2b80338ea688572 file-a5121da2ad77b95fdd1703560ef1ff41
Tailwind uses the `addWatchFile` API from Vite to make sure that Vite
triggers when any of these files change. However, we don't want to
process/transform these files, we just want the plugin to be triggered
so we can generate new CSS.

This caused some problems in the past where a change to an asset file
required the user to reload the page (e.g. a php file in case of
Laravel) instead of reloading automatically.

We made sure that if it's _not_ an asset file, aka a file that is
handled by Vite or any of its plugins that we don't use the
`full-reload` trigger, and instead rely on normal HMR.

Now another issue came up where a JS or TS file that is not part of the
Vite tree _yet_ is also considered an asset file since it's not handled
yet. This can happen if you use code splitting where a module isn't
loaded yet. Because it's an asset file, we will trigger a full-reload,
which is not what we want.

Instead we check whether it's a JS-like or CSS-like file because those
are always handled by Vite. If we detect those, we rely on normal HMR.

If it's a file such as an .astro file or .vue file, then we check if any
other file has the same extension, and if it does, then we know that
Vite would handle this file correctly, and we rely on HMR again.
@RobinMalfait
RobinMalfait requested a review from a team as a code owner July 15, 2026 15:36
@greptile-apps

greptile-apps Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Confidence Score: 5/5

The change is safe to merge. It only makes the hotUpdate hook return early in cases where a full reload was never useful, and the PHP/external-template path remains unchanged and is directly tested.

The heuristics are well-layered: hardcoded families (JS/TS, CSS) are checked cheaply before the module graph scan, the scan itself is bounded to one pass per new extension type via the viteProcessedExtensions cache, and the edge cases most likely to produce false positives (?raw query imports, asset-only modules, SSR-only modules) are each explicitly handled. Integration tests cover all four distinct file categories, and the full-reload assertion for PHP templates runs after the non-reload assertions so they remain independent.

No files require special attention. The implementation in packages/@tailwindcss-vite/src/index.ts is the only non-test change and is straightforward.

Reviews (1): Last reviewed commit: "update changelog" | Re-trigger Greptile

@RobinMalfait
RobinMalfait merged commit bdcd708 into main Jul 15, 2026
10 checks passed
@RobinMalfait
RobinMalfait deleted the fix/issue-20320 branch July 15, 2026 15:49
@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 75feda51-4169-48a4-83d1-218d1f9d9607

📥 Commits

Reviewing files that changed from the base of the PR and between bb6587e and 16276b5.

📒 Files selected for processing (1)
  • CHANGELOG.md

Walkthrough

The Vite plugin now detects files transformed as modules across Vite environments and caches confirmed extensions before deciding whether external changes require a full reload. New integration tests cover unloaded .tsx, custom, stylesheet, and Vue files, while preserving full reload behavior for external PHP templates. The tests also record HMR payloads and verify generated CSS updates.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main behavior change: skipping full reloads for scanned files Vite can process as modules.
Description check ✅ Passed The description matches the code changes and explains the bug, fix, and tests.
Linked Issues check ✅ Passed The changes address #20320, #19903, and #20323 by avoiding reloads for handled scanned files while preserving reloads for external templates.
Out of Scope Changes check ✅ Passed The added tests and hotUpdate logic stay focused on the reload-fallback fix with no unrelated feature work.

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

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

Labels

None yet

1 participant