diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index 467bb6a84ef66c..fd688b5ae4ea27 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -98,7 +98,6 @@ import { addToHTMLProxyTransformResult } from './html' import { assetUrlRE, cssEntriesMap, - fileToDevUrl, fileToUrl, publicAssetUrlCache, publicAssetUrlRE, @@ -1096,20 +1095,7 @@ export function cssAnalysisPlugin(config: ResolvedConfig): Plugin { // main import to hot update const depModules = new Set() 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)) - } - } + depModules.add(moduleGraph.createFileOnlyEntry(file)) } moduleGraph.updateModuleInfo( thisModule, diff --git a/packages/vite/src/node/server/hmr.ts b/packages/vite/src/node/server/hmr.ts index 7b3d8a2e406c8b..94cb0665da7401 100644 --- a/packages/vite/src/node/server/hmr.ts +++ b/packages/vite/src/node/server/hmr.ts @@ -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' @@ -73,7 +68,7 @@ export interface HmrContext { } interface PropagationBoundary { - boundary: EnvironmentModuleNode + boundary: EnvironmentModuleNode & { type: 'js' | 'css' } acceptedVia: EnvironmentModuleNode isWithinCircularImport: boolean } @@ -693,7 +688,16 @@ export function updateModules( ) } - if (needFullReload) { + // html file cannot be hot updated because it may be used as the template for a top-level request response. + 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})`) @@ -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 } @@ -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), - ) - } - } - return false } @@ -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), }) @@ -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) { diff --git a/packages/vite/src/node/server/mixedModuleGraph.ts b/packages/vite/src/node/server/mixedModuleGraph.ts index a5183b4658c466..14023e5159c40b 100644 --- a/packages/vite/src/node/server/mixedModuleGraph.ts +++ b/packages/vite/src/node/server/mixedModuleGraph.ts @@ -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`, diff --git a/packages/vite/src/node/server/moduleGraph.ts b/packages/vite/src/node/server/moduleGraph.ts index e102453e1ddf12..f84f9ecbb54975 100644 --- a/packages/vite/src/node/server/moduleGraph.ts +++ b/packages/vite/src/node/server/moduleGraph.ts @@ -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 importers = new Set() @@ -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, @@ -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 diff --git a/playground/assets/__tests__/assets.spec.ts b/playground/assets/__tests__/assets.spec.ts index c5a54c1cec00e4..ccc910b4a720be 100644 --- a/playground/assets/__tests__/assets.spec.ts +++ b/playground/assets/__tests__/assets.spec.ts @@ -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('
partial
\n') + + if (isBuild) return + editFile('nested/partial.html', (code) => + code.replace('
partial
', '
partial updated
'), + ) + await expect + .poll(() => page.textContent('.raw-html')) + .toBe('
partial updated
\n') + expect(browserLogs).toStrictEqual( + expect.arrayContaining([ + expect.stringContaining('hot updated: /nested/partial.html?raw via'), + ]), + ) }) test('?no-inline svg import', async () => { diff --git a/playground/assets/index.html b/playground/assets/index.html index 53cd4aec5e1a3b..5024f5a7f2ed25 100644 --- a/playground/assets/index.html +++ b/playground/assets/index.html @@ -265,6 +265,7 @@

Unknown extension assets import

?raw import

+

?no-inline svg import

@@ -546,6 +547,12 @@

assets in template

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) diff --git a/playground/assets/nested/partial.html b/playground/assets/nested/partial.html new file mode 100644 index 00000000000000..3d8c6454a288fa --- /dev/null +++ b/playground/assets/nested/partial.html @@ -0,0 +1 @@ +
partial
diff --git a/playground/tailwind/__test__/tailwind.spec.ts b/playground/tailwind/__test__/tailwind.spec.ts index 7e46a481a353df..a8b977ef9e1f3d 100644 --- a/playground/tailwind/__test__/tailwind.spec.ts +++ b/playground/tailwind/__test__/tailwind.spec.ts @@ -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)') diff --git a/playground/tailwind/index.html b/playground/tailwind/index.html index 0c7bee6b26884f..c8c6e3f0b1bb0a 100644 --- a/playground/tailwind/index.html +++ b/playground/tailwind/index.html @@ -2,4 +2,6 @@
+
html
+ diff --git a/playground/tailwind/tailwind.config.ts b/playground/tailwind/tailwind.config.ts index 0493bb41f439a5..fffc58f733f406 100644 --- a/playground/tailwind/tailwind.config.ts +++ b/playground/tailwind/tailwind.config.ts @@ -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: {},