<!-- 👋 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>
266 lines
7.6 KiB
TypeScript
266 lines
7.6 KiB
TypeScript
import { candidate, css, fetchStyles, json, retryAssertion, test, ts, txt } from '../utils'
|
|
|
|
const 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": "^5"
|
|
}
|
|
}
|
|
`,
|
|
'react-router.config.ts': ts`
|
|
import type { Config } from '@react-router/dev/config'
|
|
export default { ssr: true } satisfies Config
|
|
`,
|
|
'vite.config.ts': ts`
|
|
import { defineConfig } from 'vite'
|
|
import { reactRouter } from '@react-router/dev/vite'
|
|
import tailwindcss from '@tailwindcss/vite'
|
|
|
|
export default defineConfig({
|
|
plugins: [tailwindcss(), reactRouter()],
|
|
})
|
|
`,
|
|
'app/routes/home.tsx': ts`
|
|
export default function Home() {
|
|
return <h1 className="font-bold">Welcome to React Router</h1>
|
|
}
|
|
`,
|
|
'app/app.css': css`@import 'tailwindcss';`,
|
|
'app/routes.ts': ts`
|
|
import { type RouteConfig, index } from '@react-router/dev/routes'
|
|
export default [index('routes/home.tsx')] satisfies RouteConfig
|
|
`,
|
|
'app/root.tsx': ts`
|
|
import { Links, Meta, Outlet, Scripts, ScrollRestoration } from 'react-router'
|
|
import './app.css'
|
|
export function Layout({ children }: { children: React.ReactNode }) {
|
|
return (
|
|
<html lang="en">
|
|
<head>
|
|
<meta charSet="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<Meta />
|
|
<Links />
|
|
</head>
|
|
<body>
|
|
{children}
|
|
<ScrollRestoration />
|
|
<Scripts />
|
|
</body>
|
|
</html>
|
|
)
|
|
}
|
|
|
|
export default function App() {
|
|
return <Outlet />
|
|
}
|
|
`,
|
|
}
|
|
|
|
test('dev mode', { fs: WORKSPACE }, 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)
|
|
})
|
|
|
|
await retryAssertion(async () => {
|
|
let css = await fetchStyles(url)
|
|
expect(css).toContain(candidate`font-bold`)
|
|
})
|
|
|
|
await retryAssertion(async () => {
|
|
await fs.write(
|
|
'app/routes/home.tsx',
|
|
ts`
|
|
export default function Home() {
|
|
return <h1 className="font-bold underline">Welcome to React Router</h1>
|
|
}
|
|
`,
|
|
)
|
|
|
|
let css = await fetchStyles(url)
|
|
expect(css).toContain(candidate`underline`)
|
|
expect(css).toContain(candidate`font-bold`)
|
|
})
|
|
})
|
|
|
|
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')
|
|
|
|
let url = ''
|
|
await process.onStdout((m) => {
|
|
let match = /\[react-router-serve\]\s*(http.*)\ \/?/.exec(m)
|
|
if (match) url = match[1]
|
|
return url != ''
|
|
})
|
|
|
|
await retryAssertion(async () => {
|
|
let css = await fetchStyles(url)
|
|
expect(css).toContain(candidate`font-bold`)
|
|
})
|
|
})
|
|
|
|
test(
|
|
'build mode using ?url stylesheet imports should only build one stylesheet (requires `file-system` scanner)',
|
|
{
|
|
fs: {
|
|
...WORKSPACE,
|
|
'app/root.tsx': ts`
|
|
import { Links, Meta, Outlet, Scripts, ScrollRestoration } from 'react-router'
|
|
import styles from './app.css?url'
|
|
export const links: Route.LinksFunction = () => [{ rel: 'stylesheet', href: styles }]
|
|
export function Layout({ children }: { children: React.ReactNode }) {
|
|
return (
|
|
<html lang="en">
|
|
<head>
|
|
<meta charSet="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<Meta />
|
|
<Links />
|
|
</head>
|
|
<body class="dark">
|
|
{children}
|
|
<ScrollRestoration />
|
|
<Scripts />
|
|
</body>
|
|
</html>
|
|
)
|
|
}
|
|
|
|
export default function App() {
|
|
return <Outlet />
|
|
}
|
|
`,
|
|
'vite.config.ts': ts`
|
|
import { defineConfig } from 'vite'
|
|
import { reactRouter } from '@react-router/dev/vite'
|
|
import tailwindcss from '@tailwindcss/vite'
|
|
|
|
export default defineConfig({
|
|
plugins: [tailwindcss(), reactRouter()],
|
|
})
|
|
`,
|
|
'.gitignore': txt`
|
|
node_modules/
|
|
build/
|
|
`,
|
|
},
|
|
},
|
|
async ({ fs, exec, expect }) => {
|
|
await exec('pnpm react-router build')
|
|
|
|
let files = await fs.glob('build/client/assets/**/*.css')
|
|
|
|
expect(files).toHaveLength(1)
|
|
let [filename] = files[0]
|
|
|
|
await fs.expectFileToContain(filename, [candidate`font-bold`])
|
|
},
|
|
)
|