diff --git a/packages/vite/src/node/plugins/html.ts b/packages/vite/src/node/plugins/html.ts index 02305b45387c38..823fb6af465788 100644 --- a/packages/vite/src/node/plugins/html.ts +++ b/packages/vite/src/node/plugins/html.ts @@ -820,42 +820,165 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { }, }) - const getCssFilesForChunk = ( + const createGetCssFilesForChunk = (): (( chunk: OutputChunk, - seenChunks: Set = new Set(), - seenCss: Set = new Set(), - ): string[] => { - if (seenChunks.has(chunk.fileName)) { - return [] - } - seenChunks.add(chunk.fileName) + ) => string[]) => { + const dirtyChunks = new Map>() + + const getCssFilesForUncheckedDirtyChunk = ( + chunk: OutputChunk, + seenChunks: Set = new Set(), + seenCss: Set = new Set(), + ): string[] => { + if (seenChunks.has(chunk.fileName)) { + return [] + } + seenChunks.add(chunk.fileName) + + if (analyzedImportedCssFiles.has(chunk)) { + const files = analyzedImportedCssFiles.get(chunk)! + const additionals = files.filter((file) => !seenCss.has(file)) + additionals.forEach((file) => seenCss.add(file)) + return additionals + } + + const additionalFiles: string[] = [] + chunk.imports.forEach((file) => { + const importee = bundle[file] + if (importee?.type === 'chunk') { + additionalFiles.push( + ...getCssFilesForUncheckedDirtyChunk( + importee, + seenChunks, + seenCss, + ), + ) + } + }) + analyzedImportedCssFiles.set(chunk, additionalFiles) - if (analyzedImportedCssFiles.has(chunk)) { - const files = analyzedImportedCssFiles.get(chunk)! - const additionals = files.filter((file) => !seenCss.has(file)) - additionals.forEach((file) => seenCss.add(file)) - return additionals + chunk.viteMetadata!.importedCss.forEach((file) => { + if (!seenCss.has(file)) { + seenCss.add(file) + additionalFiles.push(file) + } else { + let dirtyChunkUnverifiedCss = dirtyChunks.get(chunk) + if (!dirtyChunkUnverifiedCss) { + dirtyChunks.set(chunk, (dirtyChunkUnverifiedCss = new Set())) + } + dirtyChunkUnverifiedCss.add(file) + } + }) + + return additionalFiles } - const files: string[] = [] - chunk.imports.forEach((file) => { - const importee = bundle[file] - if (importee?.type === 'chunk') { - files.push(...getCssFilesForChunk(importee, seenChunks, seenCss)) + const isImportChainHasDirtyChunk = ( + chunk: OutputChunk, + seenCleanChunks: Set, + seenChunks = new Set(), + ): boolean => { + if (seenChunks.has(chunk) || seenCleanChunks.has(chunk)) { + return false } - }) - analyzedImportedCssFiles.set(chunk, files) + const importChainHasDirtyChunk = chunk.imports.some((file) => { + const importee = bundle[file] + if (importee?.type === 'chunk') { + return isImportChainHasDirtyChunk( + importee, + seenCleanChunks, + seenChunks, + ) + } + return false + }) + if (!importChainHasDirtyChunk) { + seenCleanChunks.add(chunk) + } + return importChainHasDirtyChunk + } - chunk.viteMetadata!.importedCss.forEach((file) => { - if (!seenCss.has(file)) { - seenCss.add(file) - files.push(file) + const getCssFilesForCheckedDirtyChunk = ( + chunk: OutputChunk, + seenChunks: Set = new Set(), + seenCss: Set = new Set(), + seenCleanChunks = new Set(), + ): string[] => { + if (seenChunks.has(chunk.fileName)) { + return [] } - }) + seenChunks.add(chunk.fileName) + let isNeedToRebuildCache = false + let additionalFiles: string[] = [] + if (analyzedImportedCssFiles.has(chunk)) { + isNeedToRebuildCache = isImportChainHasDirtyChunk( + chunk, + seenCleanChunks, + ) + if (!isNeedToRebuildCache) { + const files = analyzedImportedCssFiles.get(chunk)! + const additionals = files.filter((file) => !seenCss.has(file)) + additionals.forEach((file) => seenCss.add(file)) + if (!dirtyChunks.has(chunk)) { + return additionals + } else { + additionalFiles = additionals + } + } + } else { + isNeedToRebuildCache = true + } + if (isNeedToRebuildCache) { + chunk.imports.forEach((file) => { + const importee = bundle[file] + if (importee?.type === 'chunk') { + additionalFiles.push( + ...getCssFilesForCheckedDirtyChunk( + importee, + seenChunks, + seenCss, + seenCleanChunks, + ), + ) + } + }) + } + analyzedImportedCssFiles.set(chunk, additionalFiles) + + const chunkUnverifiedCss = dirtyChunks.get(chunk) ?? new Set() + chunk.viteMetadata!.importedCss.forEach((file) => { + if (!seenCss.has(file)) { + seenCss.add(file) + additionalFiles.push(file) + chunkUnverifiedCss.delete(file) + } else { + let dirtyChunkUnverifiedCss = dirtyChunks.get(chunk) + if (!dirtyChunkUnverifiedCss) { + dirtyChunks.set( + chunk, + (dirtyChunkUnverifiedCss = chunkUnverifiedCss), + ) + } + dirtyChunkUnverifiedCss.add(file) + } + }) + if (chunkUnverifiedCss.size === 0 && dirtyChunks.has(chunk)) { + dirtyChunks.delete(chunk) + } + return additionalFiles + } - return files + const getCssFilesForChunk = (chunk: OutputChunk): string[] => { + if (!dirtyChunks.size) { + return getCssFilesForUncheckedDirtyChunk(chunk) + } + return getCssFilesForCheckedDirtyChunk(chunk) + } + return getCssFilesForChunk } + const getCssFilesForChunk = createGetCssFilesForChunk() + const getCssTagsForChunk = ( chunk: OutputChunk, toOutputPath: (filename: string) => string,