Skip to content

Commit dfcb83d

Browse files
authored
fix(css): track dependencies from addWatchFile for HMR (#15608)
1 parent 3b7e0c3 commit dfcb83d

File tree

8 files changed

+128
-60
lines changed

8 files changed

+128
-60
lines changed

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

+74-57
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,6 @@ const cssUrlAssetRE = /__VITE_CSS_URL__([\da-f]+)__/g
237237
*/
238238
export function cssPlugin(config: ResolvedConfig): Plugin {
239239
const isBuild = config.command === 'build'
240-
let server: ViteDevServer
241240
let moduleCache: Map<string, Record<string, string>>
242241

243242
const resolveUrl = config.createResolver({
@@ -254,10 +253,6 @@ export function cssPlugin(config: ResolvedConfig): Plugin {
254253
return {
255254
name: 'vite:css',
256255

257-
configureServer(_server) {
258-
server = _server
259-
},
260-
261256
buildStart() {
262257
// Ensure a new cache for every build (i.e. rebuilding in watch mode)
263258
moduleCache = new Map<string, Record<string, string>>()
@@ -292,16 +287,14 @@ export function cssPlugin(config: ResolvedConfig): Plugin {
292287
}
293288
},
294289

295-
async transform(raw, id, options) {
290+
async transform(raw, id) {
296291
if (
297292
!isCSSRequest(id) ||
298293
commonjsProxyRE.test(id) ||
299294
SPECIAL_QUERY_RE.test(id)
300295
) {
301296
return
302297
}
303-
const ssr = options?.ssr === true
304-
305298
const urlReplacer: CssUrlReplacer = async (url, importer) => {
306299
const decodedUrl = decodeURI(url)
307300
if (checkPublicFile(decodedUrl, config)) {
@@ -345,60 +338,12 @@ export function cssPlugin(config: ResolvedConfig): Plugin {
345338
moduleCache.set(id, modules)
346339
}
347340

348-
// track deps for build watch mode
349-
if (config.command === 'build' && config.build.watch && deps) {
341+
if (deps) {
350342
for (const file of deps) {
351343
this.addWatchFile(file)
352344
}
353345
}
354346

355-
// dev
356-
if (server) {
357-
// server only logic for handling CSS @import dependency hmr
358-
const { moduleGraph } = server
359-
const thisModule = moduleGraph.getModuleById(id)
360-
if (thisModule) {
361-
// CSS modules cannot self-accept since it exports values
362-
const isSelfAccepting =
363-
!modules && !inlineRE.test(id) && !htmlProxyRE.test(id)
364-
if (deps) {
365-
// record deps in the module graph so edits to @import css can trigger
366-
// main import to hot update
367-
const depModules = new Set<string | ModuleNode>()
368-
const devBase = config.base
369-
for (const file of deps) {
370-
depModules.add(
371-
isCSSRequest(file)
372-
? moduleGraph.createFileOnlyEntry(file)
373-
: await moduleGraph.ensureEntryFromUrl(
374-
stripBase(
375-
await fileToUrl(file, config, this),
376-
(config.server?.origin ?? '') + devBase,
377-
),
378-
ssr,
379-
),
380-
)
381-
}
382-
moduleGraph.updateModuleInfo(
383-
thisModule,
384-
depModules,
385-
null,
386-
// The root CSS proxy module is self-accepting and should not
387-
// have an explicit accept list
388-
new Set(),
389-
null,
390-
isSelfAccepting,
391-
ssr,
392-
)
393-
for (const file of deps) {
394-
this.addWatchFile(file)
395-
}
396-
} else {
397-
thisModule.isSelfAccepting = isSelfAccepting
398-
}
399-
}
400-
}
401-
402347
return {
403348
code: css,
404349
map,
@@ -945,6 +890,78 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
945890
}
946891
}
947892

893+
export function cssAnalysisPlugin(config: ResolvedConfig): Plugin {
894+
let server: ViteDevServer
895+
896+
return {
897+
name: 'vite:css-analysis',
898+
899+
configureServer(_server) {
900+
server = _server
901+
},
902+
903+
async transform(_, id, options) {
904+
if (
905+
!isCSSRequest(id) ||
906+
commonjsProxyRE.test(id) ||
907+
SPECIAL_QUERY_RE.test(id)
908+
) {
909+
return
910+
}
911+
912+
const ssr = options?.ssr === true
913+
const { moduleGraph } = server
914+
const thisModule = moduleGraph.getModuleById(id)
915+
916+
// Handle CSS @import dependency HMR and other added modules via this.addWatchFile.
917+
// JS-related HMR is handled in the import-analysis plugin.
918+
if (thisModule) {
919+
// CSS modules cannot self-accept since it exports values
920+
const isSelfAccepting =
921+
!cssModulesCache.get(config)?.get(id) &&
922+
!inlineRE.test(id) &&
923+
!htmlProxyRE.test(id)
924+
// attached by pluginContainer.addWatchFile
925+
const pluginImports = (this as any)._addedImports as
926+
| Set<string>
927+
| undefined
928+
if (pluginImports) {
929+
// record deps in the module graph so edits to @import css can trigger
930+
// main import to hot update
931+
const depModules = new Set<string | ModuleNode>()
932+
const devBase = config.base
933+
for (const file of pluginImports) {
934+
depModules.add(
935+
isCSSRequest(file)
936+
? moduleGraph.createFileOnlyEntry(file)
937+
: await moduleGraph.ensureEntryFromUrl(
938+
stripBase(
939+
await fileToUrl(file, config, this),
940+
(config.server?.origin ?? '') + devBase,
941+
),
942+
ssr,
943+
),
944+
)
945+
}
946+
moduleGraph.updateModuleInfo(
947+
thisModule,
948+
depModules,
949+
null,
950+
// The root CSS proxy module is self-accepting and should not
951+
// have an explicit accept list
952+
new Set(),
953+
null,
954+
isSelfAccepting,
955+
ssr,
956+
)
957+
} else {
958+
thisModule.isSelfAccepting = isSelfAccepting
959+
}
960+
}
961+
},
962+
}
963+
}
964+
948965
/**
949966
* Create a replacer function that takes code and replaces given pure CSS chunk imports
950967
* @param pureCssChunkNames The chunks that only contain pure CSS and should be replaced

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -744,7 +744,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
744744
}
745745

746746
// update the module graph for HMR analysis.
747-
// node CSS imports does its own graph update in the css plugin so we
747+
// node CSS imports does its own graph update in the css-analysis plugin so we
748748
// only handle js graph updates here.
749749
if (!isCSSRequest(importer)) {
750750
// attached by pluginContainer.addWatchFile

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

+6-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { resolvePlugin } from './resolve'
1212
import { optimizedDepsPlugin } from './optimizedDeps'
1313
import { esbuildPlugin } from './esbuild'
1414
import { importAnalysisPlugin } from './importAnalysis'
15-
import { cssPlugin, cssPostPlugin } from './css'
15+
import { cssAnalysisPlugin, cssPlugin, cssPostPlugin } from './css'
1616
import { assetPlugin } from './asset'
1717
import { clientInjectionsPlugin } from './clientInjections'
1818
import { buildHtmlPlugin, htmlInlineProxyPlugin } from './html'
@@ -101,7 +101,11 @@ export async function resolvePlugins(
101101
// internal server-only plugins are always applied after everything else
102102
...(isBuild
103103
? []
104-
: [clientInjectionsPlugin(config), importAnalysisPlugin(config)]),
104+
: [
105+
clientInjectionsPlugin(config),
106+
cssAnalysisPlugin(config),
107+
importAnalysisPlugin(config),
108+
]),
105109
].filter(Boolean) as Plugin[]
106110
}
107111

playground/hmr/__tests__/hmr.spec.ts

+8
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
browserLogs,
66
editFile,
77
getBg,
8+
getColor,
89
isBuild,
910
page,
1011
removeFile,
@@ -919,4 +920,11 @@ if (import.meta.hot) {
919920
)
920921
await untilUpdated(() => el.evaluate((it) => `${it.clientHeight}`), '40')
921922
})
923+
924+
test('CSS HMR with this.addWatchFile', async () => {
925+
await page.goto(viteTestUrl + '/css-deps/index.html')
926+
expect(await getColor('.css-deps')).toMatch('red')
927+
editFile('css-deps/dep.js', (code) => code.replace(`red`, `green`))
928+
await untilUpdated(() => getColor('.css-deps'), 'green')
929+
})
922930
}

playground/hmr/css-deps/dep.js

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// This file is depended by main.css via this.addWatchFile
2+
export const color = 'red'
3+
4+
// Self-accept so that updating this file would not trigger a page reload.
5+
// We only want to observe main.css updating itself.
6+
if (import.meta.hot) {
7+
import.meta.hot.accept()
8+
}

playground/hmr/css-deps/index.html

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<div class="css-deps">should be red</div>
2+
3+
<script type="module">
4+
import './main.css'
5+
// Import dep.js so that not only the CSS depends on dep.js, as Vite will do
6+
// a full page reload if the only importers are CSS files.
7+
import './dep.js'
8+
</script>

playground/hmr/css-deps/main.css

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.css-deps {
2+
color: replaced;
3+
}

playground/hmr/vite.config.ts

+20
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import fs from 'node:fs/promises'
2+
import path from 'node:path'
13
import { defineConfig } from 'vite'
24
import type { Plugin } from 'vite'
35

@@ -24,6 +26,7 @@ export default defineConfig({
2426
},
2527
virtualPlugin(),
2628
transformCountPlugin(),
29+
watchCssDepsPlugin(),
2730
],
2831
})
2932

@@ -66,3 +69,20 @@ function transformCountPlugin(): Plugin {
6669
},
6770
}
6871
}
72+
73+
function watchCssDepsPlugin(): Plugin {
74+
return {
75+
name: 'watch-css-deps',
76+
async transform(code, id) {
77+
// replace the `replaced` identifier in the CSS file with the adjacent
78+
// `dep.js` file's `color` variable.
79+
if (id.includes('css-deps/main.css')) {
80+
const depPath = path.resolve(__dirname, './css-deps/dep.js')
81+
const dep = await fs.readFile(depPath, 'utf-8')
82+
const color = dep.match(/color = '(.+?)'/)[1]
83+
this.addWatchFile(depPath)
84+
return code.replace('replaced', color)
85+
}
86+
},
87+
}
88+
}

0 commit comments

Comments
 (0)