diff --git a/packages/vite/src/node/__tests__/html.spec.ts b/packages/vite/src/node/__tests__/html.spec.ts
index 30df8e18bba545..9ad9d73dd71aaf 100644
--- a/packages/vite/src/node/__tests__/html.spec.ts
+++ b/packages/vite/src/node/__tests__/html.spec.ts
@@ -161,6 +161,74 @@ describe('getCssFilesForChunk', () => {
])
})
+ test('cached chunk does not lose CSS that was already in seenCss during first entry (#21298)', () => {
+ // entry → chunk1 (chunk1.css, chunk-shared.css)
+ // → chunk2 (chunk2.css, chunk-shared.css)
+ // entry2 → chunk2
+ const chunk1 = createChunk(
+ 'chunk1.js',
+ [],
+ ['chunk1.css', 'chunk-shared.css'],
+ )
+ const chunk2 = createChunk(
+ 'chunk2.js',
+ [],
+ ['chunk2.css', 'chunk-shared.css'],
+ )
+ const entry = createChunk(
+ 'entry.js',
+ ['chunk1.js', 'chunk2.js'],
+ ['entry.css'],
+ )
+ const entry2 = createChunk('entry2.js', ['chunk2.js'], ['entry2.css'])
+ const bundle = createBundle(entry, entry2, chunk1, chunk2)
+ const cache = new Map()
+
+ expect(getCssFilesForChunk(entry, bundle, cache)).toStrictEqual([
+ 'chunk1.css',
+ 'chunk-shared.css',
+ 'chunk2.css',
+ 'entry.css',
+ ])
+ expect(getCssFilesForChunk(entry2, bundle, cache)).toStrictEqual([
+ 'chunk2.css',
+ 'chunk-shared.css',
+ 'entry2.css',
+ ])
+ })
+
+ test('dirty leaf chunk CSS is not lost through cached parent (#21298 edge case)', () => {
+ // entry1 → other (shared.css)
+ // → mid → leaf (shared.css, leaf.css)
+ // entry2 → mid → leaf (shared.css, leaf.css)
+ const leaf = createChunk('leaf.js', [], ['shared.css', 'leaf.css'])
+ const mid = createChunk('mid.js', ['leaf.js'], ['mid.css'])
+ const other = createChunk('other.js', [], ['shared.css'])
+ const entry1 = createChunk(
+ 'entry1.js',
+ ['other.js', 'mid.js'],
+ ['entry1.css'],
+ )
+ const entry2 = createChunk('entry2.js', ['mid.js'], ['entry2.css'])
+ const bundle = createBundle(entry1, entry2, other, mid, leaf)
+ const cache = new Map()
+
+ expect(getCssFilesForChunk(entry1, bundle, cache)).toStrictEqual([
+ 'shared.css',
+ 'leaf.css',
+ 'mid.css',
+ 'entry1.css',
+ ])
+ // entry2 must still get shared.css via leaf, even though mid's cache
+ // was built while shared.css was already seen
+ expect(getCssFilesForChunk(entry2, bundle, cache)).toStrictEqual([
+ 'shared.css',
+ 'leaf.css',
+ 'mid.css',
+ 'entry2.css',
+ ])
+ })
+
test('circular imports do not cause infinite loop', () => {
const a = createChunk('a.js', ['b.js'], ['a.css'])
const b = createChunk('b.js', ['a.js'], ['b.css'])
diff --git a/packages/vite/src/node/plugins/html.ts b/packages/vite/src/node/plugins/html.ts
index 8adab33e6f9874..a8dbdb1f63983b 100644
--- a/packages/vite/src/node/plugins/html.ts
+++ b/packages/vite/src/node/plugins/html.ts
@@ -370,31 +370,40 @@ export function getCssFilesForChunk(
return additionals
}
- const files: string[] = []
+ // Collect all CSS from imports (unfiltered for caching, filtered for return)
+ const allFiles: string[] = []
+ const filteredFiles: string[] = []
chunk.imports.forEach((file) => {
const importee = bundle[file]
if (importee?.type === 'chunk') {
- files.push(
- ...getCssFilesForChunk(
- importee,
- bundle,
- analyzedImportedCssFiles,
- seenChunks,
- seenCss,
- ),
+ const importeeCss = getCssFilesForChunk(
+ importee,
+ bundle,
+ analyzedImportedCssFiles,
+ seenChunks,
+ seenCss,
)
+ filteredFiles.push(...importeeCss)
+ // For cache: use the importee's full cached list
+ if (analyzedImportedCssFiles.has(importee)) {
+ allFiles.push(...analyzedImportedCssFiles.get(importee)!)
+ } else {
+ allFiles.push(...importeeCss)
+ }
}
})
- analyzedImportedCssFiles.set(chunk, files)
chunk.viteMetadata!.importedCss.forEach((file) => {
+ allFiles.push(file)
if (!seenCss.has(file)) {
seenCss.add(file)
- files.push(file)
+ filteredFiles.push(file)
}
})
- return files
+ analyzedImportedCssFiles.set(chunk, unique(allFiles))
+
+ return filteredFiles
}
/**