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 } /**