From 841595526174df82de60a1048ae4c32e7e21ebf4 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 2 Mar 2026 15:24:58 +0900 Subject: [PATCH 1/8] fix(vite): skip full reload for server only modules watched by client --- packages/@tailwindcss-vite/src/index.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index 3ab7c3a0f2f8..42b32099f5d6 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -208,9 +208,22 @@ 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. + for (const environment of Object.values(server.environments)) { + if (environment.name === this.environment.name) continue + + const modules = environment.moduleGraph.getModulesByFile(file) + if (modules && [...modules].some(m => m.type !== 'asset')) { + return; + } + } + for (let env of new Set([this.environment.name, 'client'])) { let roots = rootsByEnv.get(env) if (roots.size === 0) continue From b37c59367e69ae1cbc2b43a3bcc8e9444c335e98 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 2 Mar 2026 17:06:54 +0900 Subject: [PATCH 2/8] test: add e2e --- integrations/vite/react-router.test.ts | 95 ++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/integrations/vite/react-router.test.ts b/integrations/vite/react-router.test.ts index 5b9574fd658b..4ea92a613021 100644 --- a/integrations/vite/react-router.test.ts +++ b/integrations/vite/react-router.test.ts @@ -102,6 +102,101 @@ 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 ( +
+

{loaderData.message}

+ +
+ ) + } + `, + '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') From b42e65cf9a88de29ce5f4136e4cb2cb86bbd9395 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 2 Mar 2026 17:08:39 +0900 Subject: [PATCH 3/8] chore: lint --- integrations/vite/react-router.test.ts | 11 ++--------- packages/@tailwindcss-vite/src/index.ts | 8 +++++--- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/integrations/vite/react-router.test.ts b/integrations/vite/react-router.test.ts index 4ea92a613021..9d8fae9b8d5e 100644 --- a/integrations/vite/react-router.test.ts +++ b/integrations/vite/react-router.test.ts @@ -145,9 +145,7 @@ test( ) } `, - 'app/direct-hdr-dep.ts': ts` - export const direct = 'HDR: 0' - `, + 'app/direct-hdr-dep.ts': ts` export const direct = 'HDR: 0' `, }, }, async ({ fs, spawn, expect }) => { @@ -176,12 +174,7 @@ test( // 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' - `, - ) + await fs.write('app/direct-hdr-dep.ts', ts` export const direct = 'HDR: 1' `) // check update await retryAssertion(async () => { diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index 42b32099f5d6..3273ea9609a7 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -210,7 +210,9 @@ export default function tailwindcss(opts: PluginOptions = {}): Plugin[] { // do a full page reload already. // // 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) + 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. @@ -219,8 +221,8 @@ export default function tailwindcss(opts: PluginOptions = {}): Plugin[] { if (environment.name === this.environment.name) continue const modules = environment.moduleGraph.getModulesByFile(file) - if (modules && [...modules].some(m => m.type !== 'asset')) { - return; + if (modules && [...modules].some((m) => m.type !== 'asset')) { + return } } From 93ca94ca679739dd2fc03d40a9acb2bcf551d214 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 2 Mar 2026 17:15:28 +0900 Subject: [PATCH 4/8] chore: comment --- packages/@tailwindcss-vite/src/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index 3273ea9609a7..e3ed13dd1d2f 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -217,6 +217,8 @@ export default function tailwindcss(opts: PluginOptions = {}): Plugin[] { // 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 (const environment of Object.values(server.environments)) { if (environment.name === this.environment.name) continue From 30cd91d117d5b744b1603d8005bdd9803f4724ed Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 12 Mar 2026 17:43:02 +0100 Subject: [PATCH 5/8] use `let` --- packages/@tailwindcss-vite/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index e3ed13dd1d2f..7738c7d95dae 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -219,10 +219,10 @@ export default function tailwindcss(opts: PluginOptions = {}): Plugin[] { // 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 (const environment of Object.values(server.environments)) { + for (let environment of Object.values(server.environments)) { if (environment.name === this.environment.name) continue - const modules = environment.moduleGraph.getModulesByFile(file) + let modules = environment.moduleGraph.getModulesByFile(file) if (modules && [...modules].some((m) => m.type !== 'asset')) { return } From d2cf8dbfa2d9a96245ac8ef22407b8decc15e948 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 12 Mar 2026 19:35:25 +0100 Subject: [PATCH 6/8] re-print comments --- packages/@tailwindcss-vite/src/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index 7738c7d95dae..2fe9ced7d6ba 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -215,9 +215,9 @@ export default function tailwindcss(opts: PluginOptions = {}): Plugin[] { 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 + // 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 From e767c0723e9873c2399ec9859d8ce42644335496 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 12 Mar 2026 19:40:54 +0100 Subject: [PATCH 7/8] loop over set directly --- packages/@tailwindcss-vite/src/index.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index 2fe9ced7d6ba..6dfc7b227832 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -223,8 +223,12 @@ export default function tailwindcss(opts: PluginOptions = {}): Plugin[] { if (environment.name === this.environment.name) continue let modules = environment.moduleGraph.getModulesByFile(file) - if (modules && [...modules].some((m) => m.type !== 'asset')) { - return + if (modules) { + for (let module of modules) { + if (module.type !== 'asset') { + return + } + } } } From cf44ddfc384b3f84d3b1508296c4ce8a4e5d8faa Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 12 Mar 2026 19:42:57 +0100 Subject: [PATCH 8/8] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c30de00ef425..38cb0e3a74d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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