Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 1 addition & 15 deletions packages/vite/src/node/plugins/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@ import { addToHTMLProxyTransformResult } from './html'
import {
assetUrlRE,
cssEntriesMap,
fileToDevUrl,
fileToUrl,
publicAssetUrlCache,
publicAssetUrlRE,
Expand Down Expand Up @@ -1096,20 +1095,7 @@ export function cssAnalysisPlugin(config: ResolvedConfig): Plugin {
// main import to hot update
const depModules = new Set<string | EnvironmentModuleNode>()
for (const file of pluginImports) {
if (isCSSRequest(file)) {
depModules.add(moduleGraph.createFileOnlyEntry(file))
} else {
const url = await fileToDevUrl(
this.environment,
file,
/* skipBase */ true,
)
if (url.startsWith('data:')) {
depModules.add(moduleGraph.createFileOnlyEntry(file))
} else {
depModules.add(await moduleGraph.ensureEntryFromUrl(url))
Copy link
Member Author

@sapphi-red sapphi-red Jul 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this line is removed, #19563 and #19786 will be fixed.

}
}
depModules.add(moduleGraph.createFileOnlyEntry(file))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great! I just explored the case in vitejs/vite-plugin-react#616, so let me share what I found. Client hot update looks like this when changing server component:

before

🔷 [hotUpdate] client [
  {
    environment: 'client',
    url: '/src/routes/root.tsx',
    id: '/home/hiroshi/code/others/vite-plugin-react/packages/plugin-rsc/examples/basic/src/routes/root.tsx',
    file: '/home/hiroshi/code/others/vite-plugin-react/packages/plugin-rsc/examples/basic/src/routes/root.tsx',
    type: 'js',
    info: undefined,
    meta: undefined,
    importers: [ '/src/styles.css?direct', '/src/styles.css' ],

after

🔷 [hotUpdate] client [
  {
    environment: 'client',
    url: '/@fs//home/hiroshi/code/others/vite-plugin-react/packages/plugin-rsc/examples/basic/src/routes/root.tsx',
    id: null,
    file: '/home/hiroshi/code/others/vite-plugin-react/packages/plugin-rsc/examples/basic/src/routes/root.tsx',
    type: 'asset',
    info: undefined,
    meta: undefined,
    importers: [ '/src/styles.css?direct', '/src/styles.css' ],

In my case, I already had filter based on id ctx.modules.map((mod) => mod.id).filter((v) => v !== null), so id: null also helped not hitting my custom css hot update handling.

The double slash in url: '/@fs//...' looks odd, but I'd assume it's essentially a placeholder value, so it shouldn't matter much.

}
moduleGraph.updateModuleInfo(
thisModule,
Expand Down
76 changes: 31 additions & 45 deletions packages/vite/src/node/server/hmr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,7 @@ import type {
InvokeSendData,
} from '../../shared/invokeMethods'
import { CLIENT_DIR } from '../constants'
import {
createDebugger,
isCSSRequest,
monotonicDateNow,
normalizePath,
} from '../utils'
import { createDebugger, monotonicDateNow, normalizePath } from '../utils'
import type { InferCustomEventPayload, ViteDevServer } from '..'
import { getHookHandler } from '../plugins'
import { isExplicitImportRequired } from '../plugins/importAnalysis'
Expand Down Expand Up @@ -73,7 +68,7 @@ export interface HmrContext {
}

interface PropagationBoundary {
boundary: EnvironmentModuleNode
boundary: EnvironmentModuleNode & { type: 'js' | 'css' }
acceptedVia: EnvironmentModuleNode
isWithinCircularImport: boolean
}
Expand Down Expand Up @@ -693,7 +688,16 @@ export function updateModules(
)
}

if (needFullReload) {
// html file cannot be hot updated
const isClientHtmlChange =
file.endsWith('.html') &&
environment.name === 'client' &&
// if the html file is imported as a module, we assume that this file is
// not used as the template for top-level request response
// (i.e. not used by the middleware).
modules.every((mod) => mod.type !== 'js')

if (needFullReload || isClientHtmlChange) {
const reason =
typeof needFullReload === 'string'
? colors.dim(` (${needFullReload})`)
Expand All @@ -705,6 +709,12 @@ export function updateModules(
hot.send({
type: 'full-reload',
triggeredBy: path.resolve(environment.config.root, file),
path:
!isClientHtmlChange ||
environment.config.server.middlewareMode ||
updates.length > 0 // if there's an update, other URLs may be affected
? '*'
: '/' + file,
})
return
}
Expand Down Expand Up @@ -761,25 +771,13 @@ function propagateUpdate(
}

if (node.isSelfAccepting) {
// isSelfAccepting is only true for js and css
const boundary = node as EnvironmentModuleNode & { type: 'js' | 'css' }
boundaries.push({
boundary: node,
acceptedVia: node,
boundary,
acceptedVia: boundary,
isWithinCircularImport: isNodeWithinCircularImports(node, currentChain),
})

// additionally check for CSS importers, since a PostCSS plugin like
// Tailwind JIT may register any file as a dependency to a CSS file.
for (const importer of node.importers) {
if (isCSSRequest(importer.url) && !currentChain.includes(importer)) {
propagateUpdate(
importer,
traversedModules,
boundaries,
currentChain.concat(importer),
)
}
}
Comment on lines -770 to -781
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part is no longer needed. type=asset module is a separate module from type=js, so updateModules is called separately for CSS files.


return false
}

Expand All @@ -789,36 +787,29 @@ function propagateUpdate(
// Also, the imported module (this one) must be updated before the importers,
// so that they do get the fresh imported module when/if they are reloaded.
if (node.acceptedHmrExports) {
// acceptedHmrExports is only true for js and css
const boundary = node as EnvironmentModuleNode & { type: 'js' | 'css' }
boundaries.push({
boundary: node,
acceptedVia: node,
boundary,
acceptedVia: boundary,
isWithinCircularImport: isNodeWithinCircularImports(node, currentChain),
})
} else {
if (!node.importers.size) {
return true
}

// #3716, #3913
// For a non-CSS file, if all of its importers are CSS files (registered via
// PostCSS plugins) it should be considered a dead end and force full reload.
if (
!isCSSRequest(node.url) &&
// we assume .svg is never an entrypoint and does not need a full reload
// to avoid frequent full reloads when an SVG file is referenced in CSS files (#18979)
!node.file?.endsWith('.svg') &&
[...node.importers].every((i) => isCSSRequest(i.url))
) {
return true
}
}

for (const importer of node.importers) {
const subChain = currentChain.concat(importer)

if (importer.acceptedHmrDeps.has(node)) {
// acceptedHmrDeps has value only for js and css
const boundary = importer as EnvironmentModuleNode & {
type: 'js' | 'css'
}
boundaries.push({
boundary: importer,
boundary,
acceptedVia: node,
isWithinCircularImport: isNodeWithinCircularImports(importer, subChain),
})
Expand Down Expand Up @@ -886,11 +877,6 @@ function isNodeWithinCircularImports(
// Node may import itself which is safe
if (importer === node) continue

// a PostCSS plugin like Tailwind JIT may register
// any file as a dependency to a CSS file.
// But in that case, the actual dependency chain is separate.
if (isCSSRequest(importer.url)) continue

// Check circular imports
const importerIndex = nodeChain.indexOf(importer)
if (importerIndex > -1) {
Expand Down
2 changes: 1 addition & 1 deletion packages/vite/src/node/server/mixedModuleGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ export class ModuleNode {
set file(value: string | null) {
this._set('file', value)
}
get type(): 'js' | 'css' {
get type(): 'js' | 'css' | 'asset' {
return this._get('type')
}
// `info` needs special care as it's defined as a proxy in `pluginContainer`,
Expand Down
7 changes: 4 additions & 3 deletions packages/vite/src/node/server/moduleGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export class EnvironmentModuleNode {
*/
id: string | null = null
file: string | null = null
type: 'js' | 'css'
type: 'js' | 'css' | 'asset'
info?: ModuleInfo
meta?: Record<string, any>
importers = new Set<EnvironmentModuleNode>()
Expand Down Expand Up @@ -219,7 +219,7 @@ export class EnvironmentModuleGraph {
// But we exclude direct CSS files as those cannot be soft invalidated.
const shouldSoftInvalidateImporter =
(importer.staticImportedUrls?.has(mod.url) || softInvalidate) &&
importer.type !== 'css'
importer.type === 'js'
this.invalidateModule(
importer,
seen,
Expand Down Expand Up @@ -402,12 +402,13 @@ export class EnvironmentModuleGraph {

const url = `${FS_PREFIX}${file}`
for (const m of fileMappedModules) {
if (m.url === url || m.id === file) {
if ((m.url === url || m.id === file) && m.type === 'asset') {
return m
}
}

const mod = new EnvironmentModuleNode(url, this.environment)
mod.type = 'asset'
mod.file = file
fileMappedModules.add(mod)
return mod
Expand Down
14 changes: 14 additions & 0 deletions playground/assets/__tests__/assets.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,20 @@ test('Unknown extension assets import', async () => {

test('?raw import', async () => {
expect(await page.textContent('.raw')).toMatch('SVG')
expect(await page.textContent('.raw-html')).toBe('<div>partial</div>\n')

if (isBuild) return
editFile('nested/partial.html', (code) =>
code.replace('<div>partial</div>', '<div>partial updated</div>'),
)
await expect
.poll(() => page.textContent('.raw-html'))
.toBe('<div>partial updated</div>\n')
expect(browserLogs).toStrictEqual(
expect.arrayContaining([
expect.stringContaining('hot updated: /nested/partial.html?raw via'),
]),
)
})

test('?no-inline svg import', async () => {
Expand Down
7 changes: 7 additions & 0 deletions playground/assets/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ <h2>Unknown extension assets import</h2>

<h2>?raw import</h2>
<code class="raw"></code>
<code class="raw-html"></code>

<h2>?no-inline svg import</h2>
<code class="no-inline-svg"></code>
Expand Down Expand Up @@ -546,6 +547,12 @@ <h3>assets in template</h3>
import rawSvg from './nested/fragment.svg?raw'
text('.raw', rawSvg)

import rawHtml from './nested/partial.html?raw'
text('.raw-html', rawHtml)
import.meta.hot?.accept('./nested/partial.html?raw', (m) => {
text('.raw-html', m.default)
})

import noInlineSvg from './nested/fragment.svg?no-inline'
text('.no-inline-svg', noInlineSvg)

Expand Down
1 change: 1 addition & 0 deletions playground/assets/nested/partial.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div>partial</div>
16 changes: 16 additions & 0 deletions playground/tailwind/__test__/tailwind.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@ test('should render', async () => {
expect(await page.textContent('#pagetitle')).toBe('Page title')
})

test.runIf(isServe)(
'full reload happens when the HTML is changed',
async () => {
await expect
.poll(() => getColor('.html'))
.toBe('oklch(0.623 0.214 259.815)')

editFile('index.html', (code) =>
code.replace('"html text-blue-500"', '"html text-green-500"'),
)
await expect
.poll(() => getColor('.html'))
.toBe('oklch(0.723 0.219 149.579)')
},
)

test.runIf(isServe)('regenerate CSS and HMR (glob pattern)', async () => {
const el = page.locator('#view1-text')
expect(await getColor(el)).toBe('oklch(0.627 0.194 149.214)')
Expand Down
2 changes: 2 additions & 0 deletions playground/tailwind/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@

<div id="app"></div>

<div class="html text-blue-500">html</div>

<script type="module" src="/src/main.js" defer></script>
1 change: 1 addition & 0 deletions playground/tailwind/tailwind.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export default {
// Look https://github.com/vitejs/vite/pull/6959 for more details
__dirname + '/src/{components,views}/**/*.js',
__dirname + '/src/main.js',
__dirname + '/index.html',
],
theme: {
extend: {},
Expand Down
Loading