Skip to content

Commit bf441a7

Browse files
fix(vite): skip full reload for server only modules scanned by client css (#19745)
<!-- 👋 Hey, thanks for your interest in contributing to Tailwind! **Please ask first before starting work on any significant new features.** It's never a fun experience to have your pull request declined after investing a lot of time and effort into a new feature. To avoid this from happening, we request that contributors create a discussion to first discuss any significant new features. For more info, check out the contributing guide: https://github.com/tailwindlabs/tailwindcss/blob/main/.github/CONTRIBUTING.md --> ## Summary - Closes #19744 - Closes vitejs/vite-plugin-react#1118 - Closes wakujs/waku#1963 The change in #19670 didn't take account for server only modules managed by SSR framework. Forcing full reload for this path breaks server HMR. This PR added a check to determine whether the same modified file has associated modules in a different environment module graph to avoid this. ## Test plan <!-- Explain how you tested your changes. Include the exact commands that you used to verify the change works and include screenshots/screen recordings of the update behavior in the browser if applicable. --> Added an integration test for React router HDR (server loader hmr). This test fails on main. Also the local build is tested on `@vitejs/plugin-rsc` CI and confirmed the fix vitejs/vite-plugin-react#1132 --------- Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
1 parent 6b54dd8 commit bf441a7

File tree

3 files changed

+111
-1
lines changed

3 files changed

+111
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616
- Guard object lookups against inherited prototype properties ([#19725](https://github.com/tailwindlabs/tailwindcss/pull/19725))
1717
- Canonicalize `calc(var(--spacing)*…)` expressions into `--spacing(…)` ([#19769](https://github.com/tailwindlabs/tailwindcss/pull/19769))
1818
- Fix crash in canonicalization step when handling utilities with empty property maps ([#19727](https://github.com/tailwindlabs/tailwindcss/pull/19727))
19+
- Skip full reload for server only modules when using `@tailwindcss/vite` ([#19745](https://github.com/tailwindlabs/tailwindcss/pull/19745))
1920

2021
## [4.2.1] - 2026-02-23
2122

integrations/vite/react-router.test.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,94 @@ test('dev mode', { fs: WORKSPACE }, async ({ fs, spawn, expect }) => {
102102
})
103103
})
104104

105+
test(
106+
// cf. https://github.com/remix-run/react-router/blob/00cb4d7b310663b2e84152700c05d3b503005e83/integration/vite-hmr-hdr-test.ts#L311-L318
107+
'dev mode, editing a server-only loader dependency triggers HDR instead of a full reload',
108+
{
109+
fs: {
110+
...WORKSPACE,
111+
'package.json': json`
112+
{
113+
"type": "module",
114+
"dependencies": {
115+
"@react-router/dev": "^7",
116+
"@react-router/node": "^7",
117+
"@react-router/serve": "^7",
118+
"@tailwindcss/vite": "workspace:^",
119+
"@types/node": "^20",
120+
"@types/react-dom": "^19",
121+
"@types/react": "^19",
122+
"isbot": "^5",
123+
"react-dom": "^19",
124+
"react-router": "^7",
125+
"react": "^19",
126+
"tailwindcss": "workspace:^",
127+
"vite": "^7"
128+
}
129+
}
130+
`,
131+
'app/routes/home.tsx': ts`
132+
import type { Route } from './+types/home'
133+
import { direct } from '../direct-hdr-dep'
134+
135+
export async function loader() {
136+
return { message: direct }
137+
}
138+
139+
export default function Home({ loaderData }: Route.ComponentProps) {
140+
return (
141+
<div>
142+
<h1 className="font-bold">{loaderData.message}</h1>
143+
<input data-testinput />
144+
</div>
145+
)
146+
}
147+
`,
148+
'app/direct-hdr-dep.ts': ts` export const direct = 'HDR: 0' `,
149+
},
150+
},
151+
async ({ fs, spawn, expect }) => {
152+
let process = await spawn('pnpm react-router dev')
153+
154+
let url = ''
155+
await process.onStdout((m) => {
156+
let match = /Local:\s*(http.*)\//.exec(m)
157+
if (match) url = match[1]
158+
return Boolean(url)
159+
})
160+
161+
// check initial state
162+
await retryAssertion(async () => {
163+
let html = await (await fetch(url)).text()
164+
expect(html).toContain('HDR: 0')
165+
166+
let css = await fetchStyles(url)
167+
expect(css).toContain(candidate`font-bold`)
168+
})
169+
170+
// Flush stdout so we only see messages triggered by the edit below.
171+
process.flush()
172+
173+
// Edit the server-only module. The client environment watches this file
174+
// but it only exists in the server module graph. Without the fix, the
175+
// Tailwind CSS plugin would trigger a full page reload on the client
176+
// instead of letting react-router handle HDR.
177+
await fs.write('app/direct-hdr-dep.ts', ts` export const direct = 'HDR: 1' `)
178+
179+
// check update
180+
await retryAssertion(async () => {
181+
let html = await (await fetch(url)).text()
182+
expect(html).toContain('HDR: 1')
183+
184+
let css = await fetchStyles(url)
185+
expect(css).toContain(candidate`font-bold`)
186+
})
187+
188+
// Assert the client receives an HMR update (not a full page reload).
189+
await process.onStdout((m) => m.includes('(client) hmr update'))
190+
},
191+
)
192+
105193
test('build mode', { fs: WORKSPACE }, async ({ spawn, exec, expect }) => {
106194
await exec('pnpm react-router build')
107195
let process = await spawn('pnpm react-router-serve ./build/server/index.js')

packages/@tailwindcss-vite/src/index.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,9 +208,30 @@ export default function tailwindcss(opts: PluginOptions = {}): Plugin[] {
208208
// Note: in Vite v7.0.6 the modules here will have a type of `js`, not
209209
// 'asset'. But it will also have a `HARD_INVALIDATED` state and will
210210
// do a full page reload already.
211-
let isExternalFile = modules.every((mod) => mod.type === 'asset' || mod.id === undefined)
211+
//
212+
// Empty modules can be skipped since it means it's not `addWatchFile`d and thus irrelevant to Tailwind.
213+
let isExternalFile =
214+
modules.length > 0 &&
215+
modules.every((mod) => mod.type === 'asset' || mod.id === undefined)
212216
if (!isExternalFile) return
213217

218+
// Skip if the module exists in other environments. SSR framework has
219+
// its own server side hmr/reload mechanism when handling server
220+
// only modules. See https://v6.vite.dev/guide/migration.html
221+
// > Updates to an SSR-only module no longer triggers a full page reload in the client. ...
222+
for (let environment of Object.values(server.environments)) {
223+
if (environment.name === this.environment.name) continue
224+
225+
let modules = environment.moduleGraph.getModulesByFile(file)
226+
if (modules) {
227+
for (let module of modules) {
228+
if (module.type !== 'asset') {
229+
return
230+
}
231+
}
232+
}
233+
}
234+
214235
for (let env of new Set([this.environment.name, 'client'])) {
215236
let roots = rootsByEnv.get(env)
216237
if (roots.size === 0) continue

0 commit comments

Comments
 (0)