diff --git a/packages/playground/assets/__tests__/assets.spec.ts b/packages/playground/assets/__tests__/assets.spec.ts index c671e93cbb308b..1b38346e49c700 100644 --- a/packages/playground/assets/__tests__/assets.spec.ts +++ b/packages/playground/assets/__tests__/assets.spec.ts @@ -6,7 +6,9 @@ import { isBuild, listAssets, readManifest, - readFile + readFile, + editFile, + notifyRebuildComplete } from '../../testUtils' const assetMatch = isBuild @@ -195,3 +197,15 @@ if (isBuild) { } }) } +describe('css and assets in css in build watch', () => { + if (isBuild) { + test('css will not be lost and css does not contain undefined', async () => { + editFile('index.html', (code) => code.replace('Assets', 'assets'), true) + await notifyRebuildComplete(watcher) + const cssFile = findAssetFile(/index\.\w+\.css$/, 'foo') + expect(cssFile).not.toBe('') + expect(cssFile).not.toMatch(/undefined/) + watcher?.close() + }) + } +}) diff --git a/packages/playground/assets/vite.config.js b/packages/playground/assets/vite.config.js index 113191c246a4a8..1ecd3318627521 100644 --- a/packages/playground/assets/vite.config.js +++ b/packages/playground/assets/vite.config.js @@ -13,6 +13,7 @@ module.exports = { }, build: { outDir: 'dist/foo', - manifest: true + manifest: true, + watch: {} } } diff --git a/packages/playground/testEnv.d.ts b/packages/playground/testEnv.d.ts index a0633e43826b62..73fa5e62c73011 100644 --- a/packages/playground/testEnv.d.ts +++ b/packages/playground/testEnv.d.ts @@ -1,4 +1,5 @@ import { Page } from 'playwright-chromium' +import { RollupWatcher } from 'rollup' declare global { // injected by the custom jest env in scripts/jestEnv.js @@ -7,4 +8,5 @@ declare global { // injected in scripts/jestPerTestSetup.ts const browserLogs: string[] const viteTestUrl: string + const watcher: RollupWatcher } diff --git a/packages/playground/testUtils.ts b/packages/playground/testUtils.ts index 3546b18c72aa45..9c7235e000c48f 100644 --- a/packages/playground/testUtils.ts +++ b/packages/playground/testUtils.ts @@ -66,8 +66,12 @@ export function readFile(filename: string) { return fs.readFileSync(path.resolve(testDir, filename), 'utf-8') } -export function editFile(filename: string, replacer: (str: string) => string) { - if (isBuild) return +export function editFile( + filename: string, + replacer: (str: string) => string, + runInBuild: boolean = false +): void { + if (isBuild && !runInBuild) return filename = path.resolve(testDir, filename) const content = fs.readFileSync(filename, 'utf-8') const modified = replacer(content) @@ -122,3 +126,8 @@ export async function untilUpdated( } } } + +/** + * Send the rebuild complete message in build watch + */ +export { notifyRebuildComplete } from '../../scripts/jestPerTestSetup' diff --git a/packages/vite/src/node/plugins/asset.ts b/packages/vite/src/node/plugins/asset.ts index 5b728d18dd6a01..dce0a8243c7aed 100644 --- a/packages/vite/src/node/plugins/asset.ts +++ b/packages/vite/src/node/plugins/asset.ts @@ -27,17 +27,21 @@ const assetHashToFilenameMap = new WeakMap< ResolvedConfig, Map >() +// save hashes of the files that has been emitted in build watch +const emittedHashMap = new WeakMap>() /** * Also supports loading plain strings with import text from './foo.txt?raw' */ export function assetPlugin(config: ResolvedConfig): Plugin { + // assetHashToFilenameMap initialization in buildStart causes getAssetFilename to return undefined + assetHashToFilenameMap.set(config, new Map()) return { name: 'vite:asset', buildStart() { assetCache.set(config, new Map()) - assetHashToFilenameMap.set(config, new Map()) + emittedHashMap.set(config, new Set()) }, resolveId(id) { @@ -202,8 +206,6 @@ async function fileToBuiltUrl( } const file = cleanUrl(id) - const { search, hash } = parseUrl(id) - const postfix = (search || '') + (hash || '') const content = await fsp.readFile(file) let url @@ -223,21 +225,26 @@ async function fileToBuiltUrl( // https://bundlers.tooling.report/hashing/asset-cascade/ // https://github.com/rollup/rollup/issues/3415 const map = assetHashToFilenameMap.get(config)! - const contentHash = getAssetHash(content) + const { search, hash } = parseUrl(id) + const postfix = (search || '') + (hash || '') + const basename = path.basename(file) + const ext = path.extname(basename) + const fileName = path.posix.join( + config.build.assetsDir, + `${basename.slice(0, -ext.length)}.${contentHash}${ext}` + ) if (!map.has(contentHash)) { - const basename = path.basename(file) - const ext = path.extname(basename) - const fileName = path.posix.join( - config.build.assetsDir, - `${basename.slice(0, -ext.length)}.${contentHash}${ext}` - ) map.set(contentHash, fileName) + } + const emittedSet = emittedHashMap.get(config)! + if (!emittedSet.has(contentHash)) { pluginContext.emitFile({ fileName, type: 'asset', source: content }) + emittedSet.add(contentHash) } url = `__VITE_ASSET__${contentHash}__${postfix ? `$_${postfix}__` : ``}` diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index de27f4d845426b..c848931f2b3dd3 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -223,7 +223,8 @@ export function cssPlugin(config: ResolvedConfig): Plugin { * Plugin applied after user plugins */ export function cssPostPlugin(config: ResolvedConfig): Plugin { - let styles: Map + // styles initialization in buildStart causes a styling loss in watch + const styles: Map = new Map() let pureCssChunks: Set // when there are multiple rollup outputs and extracting CSS, only emit once, @@ -236,7 +237,6 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { buildStart() { // Ensure new caches for every build (i.e. rebuilding in watch mode) - styles = new Map() pureCssChunks = new Set() outputToExtractedCSSMap = new Map() }, diff --git a/scripts/jestPerTestSetup.ts b/scripts/jestPerTestSetup.ts index f240ac430e056d..d578124341b7ad 100644 --- a/scripts/jestPerTestSetup.ts +++ b/scripts/jestPerTestSetup.ts @@ -2,8 +2,17 @@ import fs from 'fs-extra' import * as http from 'http' import { resolve, dirname } from 'path' import sirv from 'sirv' -import { createServer, build, ViteDevServer, UserConfig } from 'vite' +import { + createServer, + build, + ViteDevServer, + UserConfig, + PluginOption, + ResolvedConfig +} from 'vite' import { Page } from 'playwright-chromium' +// eslint-disable-next-line node/no-extraneous-import +import { RollupWatcher, RollupWatcherEvent } from 'rollup' const isBuildTest = !!process.env.VITE_TEST_BUILD @@ -18,6 +27,7 @@ declare global { interface Global { page?: Page viteTestUrl?: string + watcher?: RollupWatcher } } } @@ -99,7 +109,22 @@ beforeAll(async () => { await page.goto(url) } else { process.env.VITE_INLINE = 'inline-build' - await build(options) + // determine build watch + let resolvedConfig: ResolvedConfig + const resolvedPlugin: () => PluginOption = () => ({ + name: 'vite-plugin-watcher', + configResolved(config) { + resolvedConfig = config + } + }) + options.plugins = [resolvedPlugin()] + const rollupOutput = await build(options) + const isWatch = !!resolvedConfig!.build.watch + // in build watch,call startStaticServer after the build is complete + if (isWatch) { + global.watcher = rollupOutput as RollupWatcher + await notifyRebuildComplete(global.watcher) + } const url = (global.viteTestUrl = await startStaticServer()) await page.goto(url) } @@ -168,3 +193,21 @@ function startStaticServer(): Promise { }) }) } + +/** + * Send the rebuild complete message in build watch + */ +export async function notifyRebuildComplete( + watcher: RollupWatcher +): Promise { + let callback: (event: RollupWatcherEvent) => void + await new Promise((resolve, reject) => { + callback = (event) => { + if (event.code === 'END') { + resolve(true) + } + } + watcher.on('event', callback) + }) + return watcher.removeListener('event', callback) +}