This PR removes all of the custom HMR handling we had in the `@tailwindcss/vite` plugin. When Vite 7.1 was introduced, Vite stopped performing a full page reload for unknown files and instead started performing normal `hmr` updates. This resulted in this issue: https://github.com/tailwindlabs/tailwindcss/issues/19637 At the time, it felt like something we could easily re-add: if a file is not covered by Vite, we can perform a `full-reload`. This meant that a `.php` file would trigger a full page reload as expected. The reason the `.php` file triggered Vite in the first place is because those files were scanned by us (`@tailwindcss/vite`) so it made sense. However, this then resulted in a plethora of issues, and it feels a bit like a game of whac-a-mole. - https://github.com/tailwindlabs/tailwindcss/issues/19744 - https://github.com/tailwindlabs/tailwindcss/issues/19903 - https://github.com/tailwindlabs/tailwindcss/issues/20320 - https://github.com/tailwindlabs/tailwindcss/issues/20378 - https://github.com/tailwindlabs/tailwindcss/issues/20411 Fixes: #19744 Fixes: #19903 Fixes: #20320 Fixes: #20378 Fixes: #20411 We kept updating the logic by safelisting certain extensions, checking different servers and/or environments, handling the fact that `server` in the callback could be absent in `experimental.bundledDev` mode, etc. etc. Now, when investigating the last issue (https://github.com/tailwindlabs/tailwindcss/issues/20411), I can trigger full reloads by changing `.json`, `.yaml` or `.svg` files. This makes sense since they aren't handled by default. So thinking about this more, I think it's just not Tailwind's responsibility to tell Vite to reload the browser or not. Yes, we use the `addWatchFile` API, so files are being watched because of us. However, our only goal is to update the `.css` file (and HMR that). This means that we can just drop all the custom HMR handling we have in `@tailwindcss/vite`. This also means that https://github.com/tailwindlabs/tailwindcss/issues/19637 would regress and won't trigger full page reloads. But this can be easily handled by a plugin responsible for this behavior: - https://github.com/ElMassimo/vite-plugin-full-reload ## Test plan 1. All tests pass 2. Manually tested and changing unknown files don't result in a full page reload
165 lines
5 KiB
TypeScript
165 lines
5 KiB
TypeScript
import { candidate, css, fetchStyles, html, json, retryAssertion, test, ts, txt } from '../utils'
|
|
|
|
test(
|
|
'dev mode',
|
|
{
|
|
fs: {
|
|
'package.json': json`
|
|
{
|
|
"type": "module",
|
|
"dependencies": {
|
|
"preact": "^10"
|
|
},
|
|
"devDependencies": {
|
|
"@preact/preset-vite": "^2",
|
|
"@tailwindcss/vite": "workspace:^",
|
|
"tailwindcss": "workspace:^",
|
|
"vite": "^8"
|
|
}
|
|
}
|
|
`,
|
|
'vite.config.ts': ts`
|
|
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
import preact from '@preact/preset-vite'
|
|
import tailwindcss from '@tailwindcss/vite'
|
|
import { defineConfig } from 'vite'
|
|
|
|
export default defineConfig({
|
|
plugins: [
|
|
tailwindcss(),
|
|
preact(),
|
|
{
|
|
// Log all HMR payloads to a file so the test can assert on them
|
|
name: 'hmr-wiretap',
|
|
configureServer(server) {
|
|
let logFile = path.resolve('hmr.log')
|
|
fs.writeFileSync(logFile, '')
|
|
for (let environment of Object.values(server.environments)) {
|
|
let send = environment.hot.send.bind(environment.hot)
|
|
environment.hot.send = (payload) => {
|
|
fs.appendFileSync(logFile, JSON.stringify(payload) + '\\n')
|
|
return send(payload)
|
|
}
|
|
}
|
|
},
|
|
},
|
|
],
|
|
})
|
|
`,
|
|
'index.html': html`
|
|
<html>
|
|
<head>
|
|
<link rel="stylesheet" href="./src/index.css" />
|
|
</head>
|
|
<body>
|
|
<div id="app"></div>
|
|
<script type="module" src="./src/main.tsx"></script>
|
|
</body>
|
|
</html>
|
|
`,
|
|
'src/main.tsx': ts`
|
|
import { render } from 'preact'
|
|
import { App } from './app'
|
|
|
|
render(<App />, document.getElementById('app')!)
|
|
`,
|
|
'src/app.tsx': ts`
|
|
import { useState } from 'preact/hooks'
|
|
|
|
export function App() {
|
|
const [count, setCount] = useState(0)
|
|
return (
|
|
<button className="underline" onClick={() => setCount((c) => c + 1)}>
|
|
Count: {count}
|
|
</button>
|
|
)
|
|
}
|
|
`,
|
|
'src/index.css': css`@import 'tailwindcss';`,
|
|
},
|
|
},
|
|
async ({ fs, spawn, expect }) => {
|
|
let process = await spawn('pnpm vite dev')
|
|
await process.onStdout((m) => m.includes('ready in'))
|
|
|
|
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 styles = await fetchStyles(url)
|
|
expect(styles).toContain(candidate`underline`)
|
|
})
|
|
|
|
// Load the component modules, like a browser visiting the page would
|
|
await fetch(`${url}/src/main.tsx`)
|
|
await fetch(`${url}/src/app.tsx`)
|
|
|
|
// Editing a component keeps HMR intact: new classes are delivered through
|
|
// a regular update, not a full page reload (which would lose all state)
|
|
{
|
|
await fs.write(
|
|
'src/app.tsx',
|
|
ts`
|
|
import { useState } from 'preact/hooks'
|
|
|
|
export function App() {
|
|
const [count, setCount] = useState(0)
|
|
return (
|
|
<button className="underline flex" onClick={() => setCount((c) => c + 1)}>
|
|
Count: {count}
|
|
</button>
|
|
)
|
|
}
|
|
`,
|
|
)
|
|
|
|
await retryAssertion(async () => {
|
|
let styles = await fetchStyles(url)
|
|
expect(styles).toContain(candidate`underline`)
|
|
expect(styles).toContain(candidate`flex`)
|
|
})
|
|
expect(await fs.read('hmr.log')).toContain('"type":"update"')
|
|
expect(await fs.read('hmr.log')).not.toContain('full-reload')
|
|
}
|
|
|
|
// Changing a scanned file that is not part of the module graph (e.g.
|
|
// `package.json`, which package managers and other tooling write to while
|
|
// the dev server is running) should not trigger a full reload either —
|
|
// that would destroy client state. New candidates should still be picked
|
|
// up because the file is a watch dependency of the CSS root, so the CSS
|
|
// hot-updates through Vite's regular pipeline.
|
|
//
|
|
// https://github.com/tailwindlabs/tailwindcss/issues/20411
|
|
{
|
|
await fs.write(
|
|
'package.json',
|
|
txt`
|
|
{
|
|
"type": "module",
|
|
"description": "content-['package.json']",
|
|
"dependencies": {
|
|
"preact": "^10"
|
|
},
|
|
"devDependencies": {
|
|
"@preact/preset-vite": "^2",
|
|
"@tailwindcss/vite": "workspace:^",
|
|
"tailwindcss": "workspace:^",
|
|
"vite": "^8"
|
|
}
|
|
}
|
|
`,
|
|
)
|
|
|
|
await retryAssertion(async () => {
|
|
let styles = await fetchStyles(url)
|
|
expect(styles).toContain(candidate`content-['package.json']`)
|
|
})
|
|
expect(await fs.read('hmr.log')).not.toContain('full-reload')
|
|
}
|
|
},
|
|
)
|