mirror of
https://github.com/vitejs/vite.git
synced 2024-11-21 14:48:41 +00:00
fix(css): track dependencies from addWatchFile for HMR (#15608)
This commit is contained in:
parent
3b7e0c3dd2
commit
dfcb83d41a
@ -237,7 +237,6 @@ const cssUrlAssetRE = /__VITE_CSS_URL__([\da-f]+)__/g
|
||||
*/
|
||||
export function cssPlugin(config: ResolvedConfig): Plugin {
|
||||
const isBuild = config.command === 'build'
|
||||
let server: ViteDevServer
|
||||
let moduleCache: Map<string, Record<string, string>>
|
||||
|
||||
const resolveUrl = config.createResolver({
|
||||
@ -254,10 +253,6 @@ export function cssPlugin(config: ResolvedConfig): Plugin {
|
||||
return {
|
||||
name: 'vite:css',
|
||||
|
||||
configureServer(_server) {
|
||||
server = _server
|
||||
},
|
||||
|
||||
buildStart() {
|
||||
// Ensure a new cache for every build (i.e. rebuilding in watch mode)
|
||||
moduleCache = new Map<string, Record<string, string>>()
|
||||
@ -292,7 +287,7 @@ export function cssPlugin(config: ResolvedConfig): Plugin {
|
||||
}
|
||||
},
|
||||
|
||||
async transform(raw, id, options) {
|
||||
async transform(raw, id) {
|
||||
if (
|
||||
!isCSSRequest(id) ||
|
||||
commonjsProxyRE.test(id) ||
|
||||
@ -300,8 +295,6 @@ export function cssPlugin(config: ResolvedConfig): Plugin {
|
||||
) {
|
||||
return
|
||||
}
|
||||
const ssr = options?.ssr === true
|
||||
|
||||
const urlReplacer: CssUrlReplacer = async (url, importer) => {
|
||||
const decodedUrl = decodeURI(url)
|
||||
if (checkPublicFile(decodedUrl, config)) {
|
||||
@ -345,60 +338,12 @@ export function cssPlugin(config: ResolvedConfig): Plugin {
|
||||
moduleCache.set(id, modules)
|
||||
}
|
||||
|
||||
// track deps for build watch mode
|
||||
if (config.command === 'build' && config.build.watch && deps) {
|
||||
if (deps) {
|
||||
for (const file of deps) {
|
||||
this.addWatchFile(file)
|
||||
}
|
||||
}
|
||||
|
||||
// dev
|
||||
if (server) {
|
||||
// server only logic for handling CSS @import dependency hmr
|
||||
const { moduleGraph } = server
|
||||
const thisModule = moduleGraph.getModuleById(id)
|
||||
if (thisModule) {
|
||||
// CSS modules cannot self-accept since it exports values
|
||||
const isSelfAccepting =
|
||||
!modules && !inlineRE.test(id) && !htmlProxyRE.test(id)
|
||||
if (deps) {
|
||||
// record deps in the module graph so edits to @import css can trigger
|
||||
// main import to hot update
|
||||
const depModules = new Set<string | ModuleNode>()
|
||||
const devBase = config.base
|
||||
for (const file of deps) {
|
||||
depModules.add(
|
||||
isCSSRequest(file)
|
||||
? moduleGraph.createFileOnlyEntry(file)
|
||||
: await moduleGraph.ensureEntryFromUrl(
|
||||
stripBase(
|
||||
await fileToUrl(file, config, this),
|
||||
(config.server?.origin ?? '') + devBase,
|
||||
),
|
||||
ssr,
|
||||
),
|
||||
)
|
||||
}
|
||||
moduleGraph.updateModuleInfo(
|
||||
thisModule,
|
||||
depModules,
|
||||
null,
|
||||
// The root CSS proxy module is self-accepting and should not
|
||||
// have an explicit accept list
|
||||
new Set(),
|
||||
null,
|
||||
isSelfAccepting,
|
||||
ssr,
|
||||
)
|
||||
for (const file of deps) {
|
||||
this.addWatchFile(file)
|
||||
}
|
||||
} else {
|
||||
thisModule.isSelfAccepting = isSelfAccepting
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
code: css,
|
||||
map,
|
||||
@ -945,6 +890,78 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
export function cssAnalysisPlugin(config: ResolvedConfig): Plugin {
|
||||
let server: ViteDevServer
|
||||
|
||||
return {
|
||||
name: 'vite:css-analysis',
|
||||
|
||||
configureServer(_server) {
|
||||
server = _server
|
||||
},
|
||||
|
||||
async transform(_, id, options) {
|
||||
if (
|
||||
!isCSSRequest(id) ||
|
||||
commonjsProxyRE.test(id) ||
|
||||
SPECIAL_QUERY_RE.test(id)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const ssr = options?.ssr === true
|
||||
const { moduleGraph } = server
|
||||
const thisModule = moduleGraph.getModuleById(id)
|
||||
|
||||
// Handle CSS @import dependency HMR and other added modules via this.addWatchFile.
|
||||
// JS-related HMR is handled in the import-analysis plugin.
|
||||
if (thisModule) {
|
||||
// CSS modules cannot self-accept since it exports values
|
||||
const isSelfAccepting =
|
||||
!cssModulesCache.get(config)?.get(id) &&
|
||||
!inlineRE.test(id) &&
|
||||
!htmlProxyRE.test(id)
|
||||
// attached by pluginContainer.addWatchFile
|
||||
const pluginImports = (this as any)._addedImports as
|
||||
| Set<string>
|
||||
| undefined
|
||||
if (pluginImports) {
|
||||
// record deps in the module graph so edits to @import css can trigger
|
||||
// main import to hot update
|
||||
const depModules = new Set<string | ModuleNode>()
|
||||
const devBase = config.base
|
||||
for (const file of pluginImports) {
|
||||
depModules.add(
|
||||
isCSSRequest(file)
|
||||
? moduleGraph.createFileOnlyEntry(file)
|
||||
: await moduleGraph.ensureEntryFromUrl(
|
||||
stripBase(
|
||||
await fileToUrl(file, config, this),
|
||||
(config.server?.origin ?? '') + devBase,
|
||||
),
|
||||
ssr,
|
||||
),
|
||||
)
|
||||
}
|
||||
moduleGraph.updateModuleInfo(
|
||||
thisModule,
|
||||
depModules,
|
||||
null,
|
||||
// The root CSS proxy module is self-accepting and should not
|
||||
// have an explicit accept list
|
||||
new Set(),
|
||||
null,
|
||||
isSelfAccepting,
|
||||
ssr,
|
||||
)
|
||||
} else {
|
||||
thisModule.isSelfAccepting = isSelfAccepting
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a replacer function that takes code and replaces given pure CSS chunk imports
|
||||
* @param pureCssChunkNames The chunks that only contain pure CSS and should be replaced
|
||||
|
@ -744,7 +744,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
|
||||
}
|
||||
|
||||
// update the module graph for HMR analysis.
|
||||
// node CSS imports does its own graph update in the css plugin so we
|
||||
// node CSS imports does its own graph update in the css-analysis plugin so we
|
||||
// only handle js graph updates here.
|
||||
if (!isCSSRequest(importer)) {
|
||||
// attached by pluginContainer.addWatchFile
|
||||
|
@ -12,7 +12,7 @@ import { resolvePlugin } from './resolve'
|
||||
import { optimizedDepsPlugin } from './optimizedDeps'
|
||||
import { esbuildPlugin } from './esbuild'
|
||||
import { importAnalysisPlugin } from './importAnalysis'
|
||||
import { cssPlugin, cssPostPlugin } from './css'
|
||||
import { cssAnalysisPlugin, cssPlugin, cssPostPlugin } from './css'
|
||||
import { assetPlugin } from './asset'
|
||||
import { clientInjectionsPlugin } from './clientInjections'
|
||||
import { buildHtmlPlugin, htmlInlineProxyPlugin } from './html'
|
||||
@ -101,7 +101,11 @@ export async function resolvePlugins(
|
||||
// internal server-only plugins are always applied after everything else
|
||||
...(isBuild
|
||||
? []
|
||||
: [clientInjectionsPlugin(config), importAnalysisPlugin(config)]),
|
||||
: [
|
||||
clientInjectionsPlugin(config),
|
||||
cssAnalysisPlugin(config),
|
||||
importAnalysisPlugin(config),
|
||||
]),
|
||||
].filter(Boolean) as Plugin[]
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
browserLogs,
|
||||
editFile,
|
||||
getBg,
|
||||
getColor,
|
||||
isBuild,
|
||||
page,
|
||||
removeFile,
|
||||
@ -919,4 +920,11 @@ if (import.meta.hot) {
|
||||
)
|
||||
await untilUpdated(() => el.evaluate((it) => `${it.clientHeight}`), '40')
|
||||
})
|
||||
|
||||
test('CSS HMR with this.addWatchFile', async () => {
|
||||
await page.goto(viteTestUrl + '/css-deps/index.html')
|
||||
expect(await getColor('.css-deps')).toMatch('red')
|
||||
editFile('css-deps/dep.js', (code) => code.replace(`red`, `green`))
|
||||
await untilUpdated(() => getColor('.css-deps'), 'green')
|
||||
})
|
||||
}
|
||||
|
8
playground/hmr/css-deps/dep.js
Normal file
8
playground/hmr/css-deps/dep.js
Normal file
@ -0,0 +1,8 @@
|
||||
// This file is depended by main.css via this.addWatchFile
|
||||
export const color = 'red'
|
||||
|
||||
// Self-accept so that updating this file would not trigger a page reload.
|
||||
// We only want to observe main.css updating itself.
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept()
|
||||
}
|
8
playground/hmr/css-deps/index.html
Normal file
8
playground/hmr/css-deps/index.html
Normal file
@ -0,0 +1,8 @@
|
||||
<div class="css-deps">should be red</div>
|
||||
|
||||
<script type="module">
|
||||
import './main.css'
|
||||
// Import dep.js so that not only the CSS depends on dep.js, as Vite will do
|
||||
// a full page reload if the only importers are CSS files.
|
||||
import './dep.js'
|
||||
</script>
|
3
playground/hmr/css-deps/main.css
Normal file
3
playground/hmr/css-deps/main.css
Normal file
@ -0,0 +1,3 @@
|
||||
.css-deps {
|
||||
color: replaced;
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { defineConfig } from 'vite'
|
||||
import type { Plugin } from 'vite'
|
||||
|
||||
@ -24,6 +26,7 @@ export default defineConfig({
|
||||
},
|
||||
virtualPlugin(),
|
||||
transformCountPlugin(),
|
||||
watchCssDepsPlugin(),
|
||||
],
|
||||
})
|
||||
|
||||
@ -66,3 +69,20 @@ function transformCountPlugin(): Plugin {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function watchCssDepsPlugin(): Plugin {
|
||||
return {
|
||||
name: 'watch-css-deps',
|
||||
async transform(code, id) {
|
||||
// replace the `replaced` identifier in the CSS file with the adjacent
|
||||
// `dep.js` file's `color` variable.
|
||||
if (id.includes('css-deps/main.css')) {
|
||||
const depPath = path.resolve(__dirname, './css-deps/dep.js')
|
||||
const dep = await fs.readFile(depPath, 'utf-8')
|
||||
const color = dep.match(/color = '(.+?)'/)[1]
|
||||
this.addWatchFile(depPath)
|
||||
return code.replace('replaced', color)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user