Skip to content

Commit

Permalink
fix(css): track dependencies from addWatchFile for HMR (#15608)
Browse files Browse the repository at this point in the history
  • Loading branch information
bluwy authored Jan 16, 2024
1 parent 3b7e0c3 commit dfcb83d
Show file tree
Hide file tree
Showing 8 changed files with 128 additions and 60 deletions.
131 changes: 74 additions & 57 deletions packages/vite/src/node/plugins/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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>>()
Expand Down Expand Up @@ -292,16 +287,14 @@ export function cssPlugin(config: ResolvedConfig): Plugin {
}
},

async transform(raw, id, options) {
async transform(raw, id) {
if (
!isCSSRequest(id) ||
commonjsProxyRE.test(id) ||
SPECIAL_QUERY_RE.test(id)
) {
return
}
const ssr = options?.ssr === true

const urlReplacer: CssUrlReplacer = async (url, importer) => {
const decodedUrl = decodeURI(url)
if (checkPublicFile(decodedUrl, config)) {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/vite/src/node/plugins/importAnalysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions packages/vite/src/node/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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[]
}

Expand Down
8 changes: 8 additions & 0 deletions playground/hmr/__tests__/hmr.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
browserLogs,
editFile,
getBg,
getColor,
isBuild,
page,
removeFile,
Expand Down Expand Up @@ -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 changes: 8 additions & 0 deletions playground/hmr/css-deps/dep.js
Original file line number Diff line number Diff line change
@@ -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 changes: 8 additions & 0 deletions playground/hmr/css-deps/index.html
Original file line number Diff line number Diff line change
@@ -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 changes: 3 additions & 0 deletions playground/hmr/css-deps/main.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.css-deps {
color: replaced;
}
20 changes: 20 additions & 0 deletions playground/hmr/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import fs from 'node:fs/promises'
import path from 'node:path'
import { defineConfig } from 'vite'
import type { Plugin } from 'vite'

Expand All @@ -24,6 +26,7 @@ export default defineConfig({
},
virtualPlugin(),
transformCountPlugin(),
watchCssDepsPlugin(),
],
})

Expand Down Expand Up @@ -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)
}
},
}
}

0 comments on commit dfcb83d

Please sign in to comment.