Do not force full page reloads when using @tailwindcss/vite (#20414)
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
This commit is contained in:
parent
b9286a7346
commit
00ef99df3d
5 changed files with 195 additions and 269 deletions
|
|
@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Ensure root `theme('…')` namespace lookups in JavaScript plugins and config files return the full namespace object instead of the value of its `DEFAULT` key ([#20399](https://github.com/tailwindlabs/tailwindcss/pull/20399))
|
||||
- Skip ignored directories entirely when computing watch globs (`scanner.globs`), instead of walking their full contents on every rebuild ([#20408](https://github.com/tailwindlabs/tailwindcss/pull/20408))
|
||||
- Oxide: drop invalid UTF-8 candidates ([#20389](https://github.com/tailwindlabs/tailwindcss/pull/20389))
|
||||
- `@tailwindcss/vite` no longer forces a full page reload for external files (e.g.: `.php` files) ([#20414](https://github.com/tailwindlabs/tailwindcss/issues/20414))
|
||||
|
||||
## [4.3.3] - 2026-07-16
|
||||
|
||||
|
|
|
|||
|
|
@ -580,11 +580,11 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => {
|
|||
},
|
||||
)
|
||||
|
||||
describe.sequential.each([['^6'], ['7.0.8'], ['7.1.12'], ['7.3.1'], ['8.0.0']])(
|
||||
describe.each([['^6'], ['7.0.8'], ['7.1.12'], ['7.3.1'], ['8.0.0']])(
|
||||
'Using Vite %s',
|
||||
(version) => {
|
||||
test(
|
||||
'external source file changes trigger a full reload',
|
||||
'external source file changes update the CSS',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`{}`,
|
||||
|
|
@ -661,26 +661,33 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => {
|
|||
expect(styles).toContain(candidate`content-['project-b/src/index.php']`)
|
||||
})
|
||||
|
||||
// Flush all messages so that we can be sure the next messages are from
|
||||
// the file changes we're about to make
|
||||
// Flush all messages so that we can be sure the next messages are
|
||||
// from the file changes we're about to make
|
||||
process.flush()
|
||||
|
||||
// Changing an external .php file should trigger a full reload
|
||||
// Changing an external .php file hot-updates the generated CSS
|
||||
{
|
||||
await fs.write(
|
||||
'project-b/src/index.php',
|
||||
txt`<div class="content-['updated:project-b/src/index.php']"></div>`,
|
||||
)
|
||||
|
||||
// Ensure the page reloaded
|
||||
// On Vite < 7.1, Vite itself hard-invalidates watched files that
|
||||
// aren't part of the module graph and reloads the page.
|
||||
//
|
||||
// On newer versions nothing reloads the page: the CSS hot-updates
|
||||
// through the regular pipeline because the changed file is a
|
||||
// watch dependency of the CSS root.
|
||||
//
|
||||
// Reloading the page for external template changes is the
|
||||
// responsibility of the backend integration (e.g. `laravel-vite-plugin`'s `refresh` option, or `vite-plugin-full-reload`).
|
||||
//
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/20411
|
||||
if (version === '^6' || version === '7.0.8') {
|
||||
await process.onStdout((m) => m.includes('page reload') && m.includes('index.php'))
|
||||
} else {
|
||||
await process.onStderr(
|
||||
(m) => m.includes('vite:hmr (client)') && m.includes('index.php'),
|
||||
)
|
||||
await process.onStdout((m) => m.includes('hmr update') && m.includes('index.css'))
|
||||
}
|
||||
await process.onStderr((m) => m.includes('vite:hmr (ssr)') && m.includes('index.php'))
|
||||
|
||||
// Ensure the styles were regenerated with the new content
|
||||
let styles = await fetchStyles(url, '/index.html')
|
||||
|
|
@ -853,7 +860,6 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => {
|
|||
let styles = await fetchStyles(url, '/index.html')
|
||||
expect(styles).toContain(candidate`content-['updated:src/lazy.tsx']`)
|
||||
})
|
||||
expect(await fs.read('project-a/hmr.log')).not.toContain('full-reload')
|
||||
}
|
||||
|
||||
// The same holds for a custom file type as long as some file of the
|
||||
|
|
@ -868,7 +874,6 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => {
|
|||
let styles = await fetchStyles(url, '/index.html')
|
||||
expect(styles).toContain(candidate`content-['updated:src/comp-b.custom']`)
|
||||
})
|
||||
expect(await fs.read('project-a/hmr.log')).not.toContain('full-reload')
|
||||
}
|
||||
|
||||
// Changing a scanned stylesheet that is not part of the module graph
|
||||
|
|
@ -893,23 +898,26 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => {
|
|||
let log = await fs.read('project-a/hmr.log')
|
||||
expect(log.split('"type":"update"').length).toBeGreaterThan(updates)
|
||||
})
|
||||
expect(await fs.read('project-a/hmr.log')).not.toContain('full-reload')
|
||||
}
|
||||
|
||||
// Changing an external file (e.g. a PHP template) should still trigger
|
||||
// a full reload. This must work even though `snippet.php` is part of
|
||||
// the module graph via the `?raw` import: a query import only pulls
|
||||
// the file's contents into the graph (and creates an untransformed
|
||||
// module node for the underlying file), it is not evidence that Vite
|
||||
// processes `.php` files as modules.
|
||||
// Changing an external file (e.g. a PHP template) hot-updates the
|
||||
// generated CSS but does not trigger a full reload either. Reloading the
|
||||
// page for external template changes is the responsibility of the backend
|
||||
// integration (e.g. `laravel-vite-plugin`'s `refresh` option, or
|
||||
// `vite-plugin-full-reload`).
|
||||
//
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/20411
|
||||
{
|
||||
let updates = (await fs.read('project-a/hmr.log')).split('"type":"update"').length
|
||||
|
||||
await fs.write(
|
||||
'project-b/src/index.php',
|
||||
html`<div class="content-['updated:project-b/src/index.php']"></div>`,
|
||||
)
|
||||
|
||||
await retryAssertion(async () => {
|
||||
expect(await fs.read('project-a/hmr.log')).toContain('full-reload')
|
||||
let log = await fs.read('project-a/hmr.log')
|
||||
expect(log.split('"type":"update"').length).toBeGreaterThan(updates)
|
||||
})
|
||||
|
||||
let styles = await fetchStyles(url, '/index.html')
|
||||
|
|
|
|||
165
integrations/vite/preact.test.ts
Normal file
165
integrations/vite/preact.test.ts
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
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')
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import { expect, test } from 'vitest'
|
||||
import tailwindcss from './index'
|
||||
|
||||
// Vite's experimental `bundledDev` mode calls `hotUpdate` without a `server`,
|
||||
// so the handler must not dereference it.
|
||||
//
|
||||
// - https://github.com/vitejs/vite/discussions/22746
|
||||
// - https://github.com/tailwindlabs/tailwindcss/issues/20378
|
||||
// - https://vite.dev/blog/announcing-vite8-1#experimental-bundled-dev-mode
|
||||
test('hotUpdate does not crash when Vite omits the server (bundledDev)', () => {
|
||||
let plugin = tailwindcss().find((plugin) => plugin.name === '@tailwindcss/vite:generate:serve')!
|
||||
|
||||
let hotUpdate = plugin.hotUpdate as unknown as (options: {
|
||||
file: string
|
||||
modules: unknown[]
|
||||
timestamp: number
|
||||
server: undefined
|
||||
}) => unknown
|
||||
|
||||
expect(() =>
|
||||
hotUpdate.call(plugin, {
|
||||
file: '/app/template.html',
|
||||
modules: [{ type: 'asset', id: undefined }],
|
||||
timestamp: Date.now(),
|
||||
server: undefined,
|
||||
}),
|
||||
).not.toThrow()
|
||||
})
|
||||
|
|
@ -9,23 +9,15 @@ import {
|
|||
} from '@tailwindcss/node'
|
||||
import { clearRequireCache } from '@tailwindcss/node/require-cache'
|
||||
import { Scanner } from '@tailwindcss/oxide'
|
||||
import { realpathSync } from 'node:fs'
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import type {
|
||||
Environment,
|
||||
InternalResolveOptions,
|
||||
Plugin,
|
||||
ResolvedConfig,
|
||||
ViteDevServer,
|
||||
} from 'vite'
|
||||
import type { Environment, InternalResolveOptions, Plugin, ResolvedConfig } from 'vite'
|
||||
import * as vite from 'vite'
|
||||
|
||||
const DEBUG = env.DEBUG
|
||||
const SPECIAL_QUERY_RE = /[?&](?:worker|sharedworker|raw|url)\b/
|
||||
const COMMON_JS_PROXY_RE = /\?commonjs-proxy/
|
||||
const INLINE_STYLE_ID_RE = /[?&]index=\d+\.css$/
|
||||
const JS_EXTENSIONS_RE = /^\.[cm]?[jt]sx?$/
|
||||
|
||||
export type PluginOptions = {
|
||||
/**
|
||||
|
|
@ -73,17 +65,9 @@ function createCustomResolver(
|
|||
}
|
||||
|
||||
export default function tailwindcss(opts: PluginOptions = {}): Plugin[] {
|
||||
let servers: ViteDevServer[] = []
|
||||
let config: ResolvedConfig | null = null
|
||||
let rootsByEnv = new DefaultMap<string, Map<string, Root>>((env: string) => new Map())
|
||||
|
||||
// File extensions that Vite (or one of its plugins) has been seen to process
|
||||
// as a module. Plugins don't get added or removed while the dev server is
|
||||
// running (changing the Vite config restarts the server), so once we've seen
|
||||
// evidence for a file type we don't need to scan the module graphs for it
|
||||
// again.
|
||||
let viteProcessedExtensions = new Set<string>()
|
||||
|
||||
let isSSR = false
|
||||
let shouldOptimize = true
|
||||
let minify = true
|
||||
|
|
@ -196,10 +180,6 @@ export default function tailwindcss(opts: PluginOptions = {}): Plugin[] {
|
|||
name: '@tailwindcss/vite:scan',
|
||||
enforce: 'pre',
|
||||
|
||||
configureServer(server) {
|
||||
servers.push(server)
|
||||
},
|
||||
|
||||
async configResolved(_config) {
|
||||
config = _config
|
||||
isSSR = config.build.ssr !== false && config.build.ssr !== undefined
|
||||
|
|
@ -256,151 +236,6 @@ export default function tailwindcss(opts: PluginOptions = {}): Plugin[] {
|
|||
return result
|
||||
},
|
||||
},
|
||||
|
||||
hotUpdate({ file, modules, timestamp, server }) {
|
||||
// Vite's experimental `bundledDev` mode invokes `hotUpdate` without a
|
||||
// `server`, so there are no sibling environments to inspect and no
|
||||
// server-level `hot`/`ws` channel to reload through. Bail out early
|
||||
// rather than dereferencing `undefined`.
|
||||
//
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/20378
|
||||
if (!server) return
|
||||
|
||||
// Ensure full-reloads are triggered for files that are being watched by
|
||||
// Tailwind but aren't part of the module graph (like PHP or HTML
|
||||
// files). If we don't do this, then changes to those files won't
|
||||
// trigger a reload at all since Vite doesn't know about them.
|
||||
{
|
||||
// It's a little bit confusing, because due to the `addWatchFile`
|
||||
// calls, it _is_ part of the module graph but nothing is really
|
||||
// handling those files. These modules typically have an id of
|
||||
// undefined and/or have a type of 'asset'.
|
||||
//
|
||||
// If we call `addWatchFile` on a file that is part of the actual
|
||||
// module graph, then we will see a module for it with a type of `js`
|
||||
// and a type of `asset`. We are only interested if _all_ of them are
|
||||
// missing an id and/or have a type of 'asset', which is a strong
|
||||
// signal that the changed file is not being handled by Vite or any of
|
||||
// the plugins.
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
// 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 files that Vite (or one of its plugins) processes as a
|
||||
// module — in this environment (e.g. a lazily-loaded route that
|
||||
// hasn't been visited yet) or in another one (e.g. an SSR-only
|
||||
// module). Such a file can only affect the page through Vite's own
|
||||
// pipeline, so a full reload would only destroy client state. Any
|
||||
// changes to the generated CSS still go through the regular
|
||||
// `css-update` flow because the file is registered via
|
||||
// `addWatchFile`.
|
||||
//
|
||||
// If the file exists as a real module in another environment, then
|
||||
// that environment is responsible for it. E.g. an 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 mod of modules) {
|
||||
if (mod.type !== 'asset') {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise the file is not loaded as a module anywhere, so
|
||||
// determine whether its file _type_ would be processed by Vite
|
||||
// when requested by the browser (in which case the file just isn't
|
||||
// loaded yet, e.g. a lazily-loaded route that hasn't been visited).
|
||||
// Vite has no API to answer this without actually running the
|
||||
// plugin pipeline, so instead:
|
||||
//
|
||||
// Files Vite handles natively (the JS/TS and CSS families) are always
|
||||
// processed by Vite. This includes stylesheets that never show up as
|
||||
// their own module because a framework plugin compiles them into a
|
||||
// component (e.g. Angular), in which case that plugin owns their HMR.
|
||||
let extension = path.extname(file)
|
||||
if (JS_EXTENSIONS_RE.test(extension) || vite.isCSSRequest(file)) return
|
||||
|
||||
// For any other file type (e.g. `.vue`, `.svelte`, or `.md` with an
|
||||
// SSG plugin), if a file with the same extension exists as a real
|
||||
// module in any environment's module graph, then a plugin evidently
|
||||
// handles this file type and the changed file just isn't loaded
|
||||
// (yet).
|
||||
if (extension !== '') {
|
||||
if (viteProcessedExtensions.has(extension)) return
|
||||
|
||||
for (let environment of Object.values(server.environments)) {
|
||||
for (let mod of environment.moduleGraph.idToModuleMap.values()) {
|
||||
if (!mod.file?.endsWith(extension)) continue
|
||||
if (mod.type === 'asset') continue
|
||||
|
||||
// Only count modules that the plugin pipeline actually
|
||||
// transformed. Vite also creates untransformed placeholder
|
||||
// nodes (e.g. for the file underlying a `?raw` import) that
|
||||
// are not evidence that a plugin handles this file type.
|
||||
if (mod.transformResult == null) continue
|
||||
|
||||
// Similarly, ignore query imports (e.g. `./template.html?raw`,
|
||||
// or the `?html-proxy` modules Vite creates for inline
|
||||
// scripts): they pull a file's _contents_ into the graph
|
||||
// without a plugin processing the file type. A scanned
|
||||
// `.html` template must still trigger a full reload even if
|
||||
// some other `.html` file is imported with `?raw`.
|
||||
if (!mod.id || mod.id.includes('?')) continue
|
||||
|
||||
viteProcessedExtensions.add(extension)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let env of new Set([this.environment.name, 'client'])) {
|
||||
let roots = rootsByEnv.get(env)
|
||||
if (roots.size === 0) continue
|
||||
|
||||
// If the file is not being watched by any of the roots, then we can
|
||||
// skip the reload since it's not relevant to Tailwind CSS.
|
||||
if (!isScannedFile(file, modules, roots)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// https://vite.dev/changes/hotupdate-hook#migration-guide
|
||||
let invalidatedModules = new Set<vite.EnvironmentModuleNode>()
|
||||
for (let mod of modules) {
|
||||
this.environment.moduleGraph.invalidateModule(
|
||||
mod,
|
||||
invalidatedModules,
|
||||
timestamp,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
if (env === this.environment.name) {
|
||||
this.environment.hot.send({ type: 'full-reload' })
|
||||
} else if (server.hot.send) {
|
||||
server.hot.send({ type: 'full-reload' })
|
||||
} else if (server.ws.send) {
|
||||
server.ws.send({ type: 'full-reload' })
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
|
|
@ -521,10 +356,6 @@ class Root {
|
|||
private customJsResolver: (id: string, base: string) => Promise<string | false | undefined>,
|
||||
) {}
|
||||
|
||||
get scannedFiles() {
|
||||
return this.scanner?.files ?? []
|
||||
}
|
||||
|
||||
// Generate the CSS for the root file. This can return false if the file is
|
||||
// not considered a Tailwind root. When this happened, the root can be GCed.
|
||||
public async generate(
|
||||
|
|
@ -710,54 +541,3 @@ class Root {
|
|||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function isScannedFile(
|
||||
file: string,
|
||||
modules: vite.EnvironmentModuleNode[],
|
||||
roots: Map<string, Root>,
|
||||
) {
|
||||
let seen = new Set()
|
||||
let q = [...modules]
|
||||
let checks = {
|
||||
file,
|
||||
get realpath() {
|
||||
try {
|
||||
let realpath = realpathSync(file)
|
||||
Object.defineProperty(checks, 'realpath', { value: realpath })
|
||||
return realpath
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
while (q.length > 0) {
|
||||
let module = q.shift()!
|
||||
if (seen.has(module)) continue
|
||||
seen.add(module)
|
||||
|
||||
if (module.id) {
|
||||
let root = roots.get(module.id)
|
||||
|
||||
if (root) {
|
||||
// If the file is part of the scanned files for this root, then we know
|
||||
// for sure that it's being watched by any of the Tailwind CSS roots. It
|
||||
// doesn't matter which root it is since it's only used to know whether
|
||||
// we should trigger a full reload or not.
|
||||
if (
|
||||
root.scannedFiles.includes(checks.file) ||
|
||||
(checks.realpath && root.scannedFiles.includes(checks.realpath))
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Keep walking up the tree until we find a root.
|
||||
for (let importer of module.importers) {
|
||||
q.push(importer)
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue