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 https://github.com/tailwindlabs/tailwindcss/issues/19744 - Closes https://github.com/vitejs/vite-plugin-react/issues/1118 - Closes https://github.com/wakujs/waku/issues/1963 The change in https://github.com/tailwindlabs/tailwindcss/pull/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 https://github.com/vitejs/vite-plugin-react/pull/1132 --------- Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This commit is contained in:
parent
6b54dd8630
commit
bf441a799f
3 changed files with 111 additions and 1 deletions
|
|
@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Guard object lookups against inherited prototype properties ([#19725](https://github.com/tailwindlabs/tailwindcss/pull/19725))
|
||||
- Canonicalize `calc(var(--spacing)*…)` expressions into `--spacing(…)` ([#19769](https://github.com/tailwindlabs/tailwindcss/pull/19769))
|
||||
- Fix crash in canonicalization step when handling utilities with empty property maps ([#19727](https://github.com/tailwindlabs/tailwindcss/pull/19727))
|
||||
- Skip full reload for server only modules when using `@tailwindcss/vite` ([#19745](https://github.com/tailwindlabs/tailwindcss/pull/19745))
|
||||
|
||||
## [4.2.1] - 2026-02-23
|
||||
|
||||
|
|
|
|||
|
|
@ -102,6 +102,94 @@ test('dev mode', { fs: WORKSPACE }, async ({ fs, spawn, expect }) => {
|
|||
})
|
||||
})
|
||||
|
||||
test(
|
||||
// cf. https://github.com/remix-run/react-router/blob/00cb4d7b310663b2e84152700c05d3b503005e83/integration/vite-hmr-hdr-test.ts#L311-L318
|
||||
'dev mode, editing a server-only loader dependency triggers HDR instead of a full reload',
|
||||
{
|
||||
fs: {
|
||||
...WORKSPACE,
|
||||
'package.json': json`
|
||||
{
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@react-router/dev": "^7",
|
||||
"@react-router/node": "^7",
|
||||
"@react-router/serve": "^7",
|
||||
"@tailwindcss/vite": "workspace:^",
|
||||
"@types/node": "^20",
|
||||
"@types/react-dom": "^19",
|
||||
"@types/react": "^19",
|
||||
"isbot": "^5",
|
||||
"react-dom": "^19",
|
||||
"react-router": "^7",
|
||||
"react": "^19",
|
||||
"tailwindcss": "workspace:^",
|
||||
"vite": "^7"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'app/routes/home.tsx': ts`
|
||||
import type { Route } from './+types/home'
|
||||
import { direct } from '../direct-hdr-dep'
|
||||
|
||||
export async function loader() {
|
||||
return { message: direct }
|
||||
}
|
||||
|
||||
export default function Home({ loaderData }: Route.ComponentProps) {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="font-bold">{loaderData.message}</h1>
|
||||
<input data-testinput />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
`,
|
||||
'app/direct-hdr-dep.ts': ts` export const direct = 'HDR: 0' `,
|
||||
},
|
||||
},
|
||||
async ({ fs, spawn, expect }) => {
|
||||
let process = await spawn('pnpm react-router dev')
|
||||
|
||||
let url = ''
|
||||
await process.onStdout((m) => {
|
||||
let match = /Local:\s*(http.*)\//.exec(m)
|
||||
if (match) url = match[1]
|
||||
return Boolean(url)
|
||||
})
|
||||
|
||||
// check initial state
|
||||
await retryAssertion(async () => {
|
||||
let html = await (await fetch(url)).text()
|
||||
expect(html).toContain('HDR: 0')
|
||||
|
||||
let css = await fetchStyles(url)
|
||||
expect(css).toContain(candidate`font-bold`)
|
||||
})
|
||||
|
||||
// Flush stdout so we only see messages triggered by the edit below.
|
||||
process.flush()
|
||||
|
||||
// Edit the server-only module. The client environment watches this file
|
||||
// but it only exists in the server module graph. Without the fix, the
|
||||
// Tailwind CSS plugin would trigger a full page reload on the client
|
||||
// instead of letting react-router handle HDR.
|
||||
await fs.write('app/direct-hdr-dep.ts', ts` export const direct = 'HDR: 1' `)
|
||||
|
||||
// check update
|
||||
await retryAssertion(async () => {
|
||||
let html = await (await fetch(url)).text()
|
||||
expect(html).toContain('HDR: 1')
|
||||
|
||||
let css = await fetchStyles(url)
|
||||
expect(css).toContain(candidate`font-bold`)
|
||||
})
|
||||
|
||||
// Assert the client receives an HMR update (not a full page reload).
|
||||
await process.onStdout((m) => m.includes('(client) hmr update'))
|
||||
},
|
||||
)
|
||||
|
||||
test('build mode', { fs: WORKSPACE }, async ({ spawn, exec, expect }) => {
|
||||
await exec('pnpm react-router build')
|
||||
let process = await spawn('pnpm react-router-serve ./build/server/index.js')
|
||||
|
|
|
|||
|
|
@ -208,9 +208,30 @@ export default function tailwindcss(opts: PluginOptions = {}): Plugin[] {
|
|||
// Note: in Vite v7.0.6 the modules here will have a type of `js`, not
|
||||
// 'asset'. But it will also have a `HARD_INVALIDATED` state and will
|
||||
// do a full page reload already.
|
||||
let isExternalFile = modules.every((mod) => mod.type === 'asset' || mod.id === undefined)
|
||||
//
|
||||
// Empty modules can be skipped since it means it's not `addWatchFile`d and thus irrelevant to Tailwind.
|
||||
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. See https://v6.vite.dev/guide/migration.html
|
||||
// > Updates to an SSR-only module no longer triggers a full page reload in the client. ...
|
||||
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'])) {
|
||||
let roots = rootsByEnv.get(env)
|
||||
if (roots.size === 0) continue
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue