Skip to content

Avoid full reload in @tailwindcss/vite when editing a scanned file that is not a loaded module - #20323

Closed
UditDewan wants to merge 2 commits into
tailwindlabs:mainfrom
UditDewan:fix/vite-full-reload-unloaded-modules
Closed

Avoid full reload in @tailwindcss/vite when editing a scanned file that is not a loaded module#20323
UditDewan wants to merge 2 commits into
tailwindlabs:mainfrom
UditDewan:fix/vite-full-reload-unloaded-modules

Conversation

@UditDewan

Copy link
Copy Markdown
Contributor

Summary

Fixes #20320

In dev mode, @tailwindcss/vite's hotUpdate hook has a fallback that sends a full-reload for files that Tailwind scans but that aren't handled by Vite or any plugin (e.g. .php or .html templates). Without it, edits to those files wouldn't refresh the page at all.

However, the check ("every module for this file is an asset and/or has no id") also matches JS/TS source files that Tailwind has scanned but that Vite hasn't loaded as a module yet — lazy routes, unvisited chunks, and so on. The scanner's addWatchFile call creates an asset-type module-graph node with id === undefined for every scanned file, and for a not-yet-loaded .tsx there is no real JS node next to it, so the fallback fires and the whole page reloads, throwing away all client state. In apps with route-level code splitting this happens on almost every edit to a not-currently-loaded component.

This change exempts JS/TS module files (.js, .jsx, .ts, .tsx, .mjs, .cjs, .mts, .cts) from the fallback. Vite handles these files itself whenever they are actually loaded, and for scanned-but-unloaded files any new candidates still flow through the regular CSS update, so nothing is lost by skipping the reload:

  • Editing a scanned-but-unloaded .ts file now results in a targeted {"type":"update"} for the Tailwind stylesheet instead of {"type":"full-reload"}, and the generated CSS includes the new candidates.
  • Editing a scanned external .php file still triggers a full reload as before.

Test plan

  • Added an integration test that boots a Vite 8 dev server with a src/unloaded.ts file that is scanned but never imported. A small fixture plugin wraps environment.hot.send to log every HMR payload. The test edits unloaded.ts and asserts that an update payload is sent (not a full-reload) and that the stylesheet regenerates with the new candidate.
  • Verified both directions manually against a minimal reproduction (Vite 8.1.4): before the change, editing the unloaded file logs HOT_SEND {"type":"full-reload"}; after the change it logs HOT_SEND {"type":"update", ...} for /src/index.css and the CSS contains the new candidate. Editing a scanned .php file still logs HOT_SEND {"type":"full-reload"}.
pnpm exec vitest run --root=./integrations vite/index.test.ts -t "scanned module files that are not loaded do not trigger a full reload"
@UditDewan
UditDewan requested a review from a team as a code owner July 12, 2026 04:19
@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 4e2c5e27-7811-44f2-8a6c-7ac58a75019f

📥 Commits

Reviewing files that changed from the base of the PR and between 35a3e9c and 70c4643.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • integrations/vite/index.test.ts
  • packages/@tailwindcss-vite/src/index.ts

Walkthrough

The Vite plugin now detects external JavaScript and TypeScript files during hot updates and skips the full-reload path for them. A new integration test covers a scanned but unloaded module, captures HMR payloads, verifies an update instead of a full reload, and checks regenerated CSS. The changelog documents the fix under the unreleased changes.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main fix: avoiding full reloads for scanned files that are not yet loaded modules.
Description check ✅ Passed The description matches the code changes and test coverage for the unloaded JS/TS hot-update behavior.
Linked Issues check ✅ Passed The PR addresses #20320 by exempting scanned-but-unloaded JS/TS modules from the full-reload fallback while keeping template files on reload.
Out of Scope Changes check ✅ Passed The changes stay focused on the hot-update fix, its integration test, and a changelog note with no unrelated additions.

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.

@greptile-apps

greptile-apps Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

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

@RobinMalfait

Copy link
Copy Markdown
Member

Hey! Closed this in favor of #20336 because while the regex does solve some things, it doesn't solve the full picture (e.g. a file with a custom extension that isn't loaded yet)

pull Bot pushed a commit to steinsi/Tailwind that referenced this pull request Jul 15, 2026
…s as modules (tailwindlabs#20336)

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: tailwindlabs#20320
Fixes: tailwindlabs#19903
Closes: tailwindlabs#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


<img width="594" height="100"
alt="file-14a86a90a1e4b810c2b80338ea688572"
src="https://github.com/user-attachments/assets/a4507502-4a5d-43ee-93b9-14c793a25891"
/>

<img width="1122" height="1376"
alt="file-a5121da2ad77b95fdd1703560ef1ff41"
src="https://github.com/user-attachments/assets/cc5c67b0-b5ad-481f-8823-a3266d75357d"
/>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants