Skip to content

Commit 7eac8dd

Browse files
authored
fix(hmr): register css deps as type: asset (vitejs#20391)
1 parent e6aaf17 commit 7eac8dd

File tree

10 files changed

+78
-64
lines changed

10 files changed

+78
-64
lines changed

packages/vite/src/node/plugins/css.ts

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,6 @@ import { addToHTMLProxyTransformResult } from './html'
9898
import {
9999
assetUrlRE,
100100
cssEntriesMap,
101-
fileToDevUrl,
102101
fileToUrl,
103102
publicAssetUrlCache,
104103
publicAssetUrlRE,
@@ -1096,20 +1095,7 @@ export function cssAnalysisPlugin(config: ResolvedConfig): Plugin {
10961095
// main import to hot update
10971096
const depModules = new Set<string | EnvironmentModuleNode>()
10981097
for (const file of pluginImports) {
1099-
if (isCSSRequest(file)) {
1100-
depModules.add(moduleGraph.createFileOnlyEntry(file))
1101-
} else {
1102-
const url = await fileToDevUrl(
1103-
this.environment,
1104-
file,
1105-
/* skipBase */ true,
1106-
)
1107-
if (url.startsWith('data:')) {
1108-
depModules.add(moduleGraph.createFileOnlyEntry(file))
1109-
} else {
1110-
depModules.add(await moduleGraph.ensureEntryFromUrl(url))
1111-
}
1112-
}
1098+
depModules.add(moduleGraph.createFileOnlyEntry(file))
11131099
}
11141100
moduleGraph.updateModuleInfo(
11151101
thisModule,

packages/vite/src/node/server/hmr.ts

Lines changed: 31 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,7 @@ import type {
1010
InvokeSendData,
1111
} from '../../shared/invokeMethods'
1212
import { CLIENT_DIR } from '../constants'
13-
import {
14-
createDebugger,
15-
isCSSRequest,
16-
monotonicDateNow,
17-
normalizePath,
18-
} from '../utils'
13+
import { createDebugger, monotonicDateNow, normalizePath } from '../utils'
1914
import type { InferCustomEventPayload, ViteDevServer } from '..'
2015
import { getHookHandler } from '../plugins'
2116
import { isExplicitImportRequired } from '../plugins/importAnalysis'
@@ -73,7 +68,7 @@ export interface HmrContext {
7368
}
7469

7570
interface PropagationBoundary {
76-
boundary: EnvironmentModuleNode
71+
boundary: EnvironmentModuleNode & { type: 'js' | 'css' }
7772
acceptedVia: EnvironmentModuleNode
7873
isWithinCircularImport: boolean
7974
}
@@ -693,7 +688,16 @@ export function updateModules(
693688
)
694689
}
695690

696-
if (needFullReload) {
691+
// html file cannot be hot updated because it may be used as the template for a top-level request response.
692+
const isClientHtmlChange =
693+
file.endsWith('.html') &&
694+
environment.name === 'client' &&
695+
// if the html file is imported as a module, we assume that this file is
696+
// not used as the template for top-level request response
697+
// (i.e. not used by the middleware).
698+
modules.every((mod) => mod.type !== 'js')
699+
700+
if (needFullReload || isClientHtmlChange) {
697701
const reason =
698702
typeof needFullReload === 'string'
699703
? colors.dim(` (${needFullReload})`)
@@ -705,6 +709,12 @@ export function updateModules(
705709
hot.send({
706710
type: 'full-reload',
707711
triggeredBy: path.resolve(environment.config.root, file),
712+
path:
713+
!isClientHtmlChange ||
714+
environment.config.server.middlewareMode ||
715+
updates.length > 0 // if there's an update, other URLs may be affected
716+
? '*'
717+
: '/' + file,
708718
})
709719
return
710720
}
@@ -761,25 +771,13 @@ function propagateUpdate(
761771
}
762772

763773
if (node.isSelfAccepting) {
774+
// isSelfAccepting is only true for js and css
775+
const boundary = node as EnvironmentModuleNode & { type: 'js' | 'css' }
764776
boundaries.push({
765-
boundary: node,
766-
acceptedVia: node,
777+
boundary,
778+
acceptedVia: boundary,
767779
isWithinCircularImport: isNodeWithinCircularImports(node, currentChain),
768780
})
769-
770-
// additionally check for CSS importers, since a PostCSS plugin like
771-
// Tailwind JIT may register any file as a dependency to a CSS file.
772-
for (const importer of node.importers) {
773-
if (isCSSRequest(importer.url) && !currentChain.includes(importer)) {
774-
propagateUpdate(
775-
importer,
776-
traversedModules,
777-
boundaries,
778-
currentChain.concat(importer),
779-
)
780-
}
781-
}
782-
783781
return false
784782
}
785783

@@ -789,36 +787,29 @@ function propagateUpdate(
789787
// Also, the imported module (this one) must be updated before the importers,
790788
// so that they do get the fresh imported module when/if they are reloaded.
791789
if (node.acceptedHmrExports) {
790+
// acceptedHmrExports is only true for js and css
791+
const boundary = node as EnvironmentModuleNode & { type: 'js' | 'css' }
792792
boundaries.push({
793-
boundary: node,
794-
acceptedVia: node,
793+
boundary,
794+
acceptedVia: boundary,
795795
isWithinCircularImport: isNodeWithinCircularImports(node, currentChain),
796796
})
797797
} else {
798798
if (!node.importers.size) {
799799
return true
800800
}
801-
802-
// #3716, #3913
803-
// For a non-CSS file, if all of its importers are CSS files (registered via
804-
// PostCSS plugins) it should be considered a dead end and force full reload.
805-
if (
806-
!isCSSRequest(node.url) &&
807-
// we assume .svg is never an entrypoint and does not need a full reload
808-
// to avoid frequent full reloads when an SVG file is referenced in CSS files (#18979)
809-
!node.file?.endsWith('.svg') &&
810-
[...node.importers].every((i) => isCSSRequest(i.url))
811-
) {
812-
return true
813-
}
814801
}
815802

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

819806
if (importer.acceptedHmrDeps.has(node)) {
807+
// acceptedHmrDeps has value only for js and css
808+
const boundary = importer as EnvironmentModuleNode & {
809+
type: 'js' | 'css'
810+
}
820811
boundaries.push({
821-
boundary: importer,
812+
boundary,
822813
acceptedVia: node,
823814
isWithinCircularImport: isNodeWithinCircularImports(importer, subChain),
824815
})
@@ -886,11 +877,6 @@ function isNodeWithinCircularImports(
886877
// Node may import itself which is safe
887878
if (importer === node) continue
888879

889-
// a PostCSS plugin like Tailwind JIT may register
890-
// any file as a dependency to a CSS file.
891-
// But in that case, the actual dependency chain is separate.
892-
if (isCSSRequest(importer.url)) continue
893-
894880
// Check circular imports
895881
const importerIndex = nodeChain.indexOf(importer)
896882
if (importerIndex > -1) {

packages/vite/src/node/server/mixedModuleGraph.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ export class ModuleNode {
140140
set file(value: string | null) {
141141
this._set('file', value)
142142
}
143-
get type(): 'js' | 'css' {
143+
get type(): 'js' | 'css' | 'asset' {
144144
return this._get('type')
145145
}
146146
// `info` needs special care as it's defined as a proxy in `pluginContainer`,

packages/vite/src/node/server/moduleGraph.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export class EnvironmentModuleNode {
2222
*/
2323
id: string | null = null
2424
file: string | null = null
25-
type: 'js' | 'css'
25+
type: 'js' | 'css' | 'asset'
2626
info?: ModuleInfo
2727
meta?: Record<string, any>
2828
importers = new Set<EnvironmentModuleNode>()
@@ -219,7 +219,7 @@ export class EnvironmentModuleGraph {
219219
// But we exclude direct CSS files as those cannot be soft invalidated.
220220
const shouldSoftInvalidateImporter =
221221
(importer.staticImportedUrls?.has(mod.url) || softInvalidate) &&
222-
importer.type !== 'css'
222+
importer.type === 'js'
223223
this.invalidateModule(
224224
importer,
225225
seen,
@@ -402,12 +402,13 @@ export class EnvironmentModuleGraph {
402402

403403
const url = `${FS_PREFIX}${file}`
404404
for (const m of fileMappedModules) {
405-
if (m.url === url || m.id === file) {
405+
if ((m.url === url || m.id === file) && m.type === 'asset') {
406406
return m
407407
}
408408
}
409409

410410
const mod = new EnvironmentModuleNode(url, this.environment)
411+
mod.type = 'asset'
411412
mod.file = file
412413
fileMappedModules.add(mod)
413414
return mod

playground/assets/__tests__/assets.spec.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,20 @@ test('Unknown extension assets import', async () => {
463463

464464
test('?raw import', async () => {
465465
expect(await page.textContent('.raw')).toMatch('SVG')
466+
expect(await page.textContent('.raw-html')).toBe('<div>partial</div>\n')
467+
468+
if (isBuild) return
469+
editFile('nested/partial.html', (code) =>
470+
code.replace('<div>partial</div>', '<div>partial updated</div>'),
471+
)
472+
await expect
473+
.poll(() => page.textContent('.raw-html'))
474+
.toBe('<div>partial updated</div>\n')
475+
expect(browserLogs).toStrictEqual(
476+
expect.arrayContaining([
477+
expect.stringContaining('hot updated: /nested/partial.html?raw via'),
478+
]),
479+
)
466480
})
467481

468482
test('?no-inline svg import', async () => {

playground/assets/index.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@ <h2>Unknown extension assets import</h2>
265265

266266
<h2>?raw import</h2>
267267
<code class="raw"></code>
268+
<code class="raw-html"></code>
268269

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

550+
import rawHtml from './nested/partial.html?raw'
551+
text('.raw-html', rawHtml)
552+
import.meta.hot?.accept('./nested/partial.html?raw', (m) => {
553+
text('.raw-html', m.default)
554+
})
555+
549556
import noInlineSvg from './nested/fragment.svg?no-inline'
550557
text('.no-inline-svg', noInlineSvg)
551558

playground/assets/nested/partial.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div>partial</div>

playground/tailwind/__test__/tailwind.spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,22 @@ test('should render', async () => {
55
expect(await page.textContent('#pagetitle')).toBe('Page title')
66
})
77

8+
test.runIf(isServe)(
9+
'full reload happens when the HTML is changed',
10+
async () => {
11+
await expect
12+
.poll(() => getColor('.html'))
13+
.toBe('oklch(0.623 0.214 259.815)')
14+
15+
editFile('index.html', (code) =>
16+
code.replace('"html text-blue-500"', '"html text-green-500"'),
17+
)
18+
await expect
19+
.poll(() => getColor('.html'))
20+
.toBe('oklch(0.723 0.219 149.579)')
21+
},
22+
)
23+
824
test.runIf(isServe)('regenerate CSS and HMR (glob pattern)', async () => {
925
const el = page.locator('#view1-text')
1026
expect(await getColor(el)).toBe('oklch(0.627 0.194 149.214)')

playground/tailwind/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,6 @@
22

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

5+
<div class="html text-blue-500">html</div>
6+
57
<script type="module" src="/src/main.js" defer></script>

playground/tailwind/tailwind.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export default {
66
// Look https://github.com/vitejs/vite/pull/6959 for more details
77
__dirname + '/src/{components,views}/**/*.js',
88
__dirname + '/src/main.js',
9+
__dirname + '/index.html',
910
],
1011
theme: {
1112
extend: {},

0 commit comments

Comments
 (0)