From 166adde96c221aa6193e716079f3ef436da1776c Mon Sep 17 00:00:00 2001 From: GG ZIBLAKING Date: Thu, 26 Mar 2026 19:47:34 +0800 Subject: [PATCH 1/4] fix(css): use unique key for cssEntriesMap to prevent same-basename collision When multiple CSS-only entry points in different directories share the same basename, `cssEntriesMap` used `chunk.name` as the Map key, causing the second entry to overwrite the first. This led to incorrect CSS cross-linking in the build manifest and spurious style loading at runtime. Use `originalFileName` (the normalized relative path from `getChunkOriginalFileName`) instead of `chunk.name` as the Map key, which is always unique per entry. Falls back to `chunk.name` when `facadeModuleId` is unavailable. Fixes #22013 Co-Authored-By: Codex Co-Authored-By: Claude Opus 4.6 --- .../vite/src/node/__tests__/build.spec.ts | 28 +++++++++++++++++++ .../css-entry-same-basename/a/asset-a.svg | 3 ++ .../css-entry-same-basename/a/index.css | 3 ++ .../css-entry-same-basename/b/asset-b.svg | 3 ++ .../css-entry-same-basename/b/index.css | 3 ++ packages/vite/src/node/plugins/asset.ts | 2 +- packages/vite/src/node/plugins/css.ts | 13 +++++++-- packages/vite/src/node/plugins/manifest.ts | 10 +++++-- 8 files changed, 59 insertions(+), 6 deletions(-) create mode 100644 packages/vite/src/node/__tests__/fixtures/css-entry-same-basename/a/asset-a.svg create mode 100644 packages/vite/src/node/__tests__/fixtures/css-entry-same-basename/a/index.css create mode 100644 packages/vite/src/node/__tests__/fixtures/css-entry-same-basename/b/asset-b.svg create mode 100644 packages/vite/src/node/__tests__/fixtures/css-entry-same-basename/b/index.css diff --git a/packages/vite/src/node/__tests__/build.spec.ts b/packages/vite/src/node/__tests__/build.spec.ts index 79e2a612c274e5..3209bc16af00d1 100644 --- a/packages/vite/src/node/__tests__/build.spec.ts +++ b/packages/vite/src/node/__tests__/build.spec.ts @@ -144,6 +144,34 @@ describe('build', () => { assertOutputHashContentChange(result[0], result[1]) }) + test('css entries with the same basename should not cross-link manifest assets', async () => { + const root = resolve(dirname, 'fixtures/css-entry-same-basename') + const result = (await build({ + root, + logLevel: 'silent', + build: { + write: false, + manifest: true, + assetsInlineLimit: 0, + rollupOptions: { + input: [resolve(root, 'a/index.css'), resolve(root, 'b/index.css')], + }, + }, + })) as RolldownOutput + + const manifest = JSON.parse( + (result.output.find((o) => o.fileName === '.vite/manifest.json') as any) + .source, + ) + + expect(manifest['a/index.css']).toMatchObject({ + assets: [expect.stringMatching(/assets\/asset-a-[-\w]{8}\.svg/)], + }) + expect(manifest['b/index.css']).toMatchObject({ + assets: [expect.stringMatching(/assets\/asset-b-[-\w]{8}\.svg/)], + }) + }) + test.for([ [true, true], [true, false], diff --git a/packages/vite/src/node/__tests__/fixtures/css-entry-same-basename/a/asset-a.svg b/packages/vite/src/node/__tests__/fixtures/css-entry-same-basename/a/asset-a.svg new file mode 100644 index 00000000000000..adb7b6bcff3d8d --- /dev/null +++ b/packages/vite/src/node/__tests__/fixtures/css-entry-same-basename/a/asset-a.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/vite/src/node/__tests__/fixtures/css-entry-same-basename/a/index.css b/packages/vite/src/node/__tests__/fixtures/css-entry-same-basename/a/index.css new file mode 100644 index 00000000000000..7468510d58df10 --- /dev/null +++ b/packages/vite/src/node/__tests__/fixtures/css-entry-same-basename/a/index.css @@ -0,0 +1,3 @@ +.entry-a { + background-image: url('./asset-a.svg'); +} diff --git a/packages/vite/src/node/__tests__/fixtures/css-entry-same-basename/b/asset-b.svg b/packages/vite/src/node/__tests__/fixtures/css-entry-same-basename/b/asset-b.svg new file mode 100644 index 00000000000000..c79e8f52e51f4e --- /dev/null +++ b/packages/vite/src/node/__tests__/fixtures/css-entry-same-basename/b/asset-b.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/vite/src/node/__tests__/fixtures/css-entry-same-basename/b/index.css b/packages/vite/src/node/__tests__/fixtures/css-entry-same-basename/b/index.css new file mode 100644 index 00000000000000..e6632c31ad8e3f --- /dev/null +++ b/packages/vite/src/node/__tests__/fixtures/css-entry-same-basename/b/index.css @@ -0,0 +1,3 @@ +.entry-b { + background-image: url('./asset-b.svg'); +} diff --git a/packages/vite/src/node/plugins/asset.ts b/packages/vite/src/node/plugins/asset.ts index b5e2b652da3cbc..7604e848cc472a 100644 --- a/packages/vite/src/node/plugins/asset.ts +++ b/packages/vite/src/node/plugins/asset.ts @@ -56,7 +56,7 @@ const assetCache = new WeakMap>() /** a set of referenceId for entry CSS assets for each environment */ export const cssEntriesMap: WeakMap< Environment, - Map + Map > = new WeakMap() // add own dictionary entry by directly assigning mrmime diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index 6618d57cfbfd86..d6805a8553ef5c 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -914,7 +914,10 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { if (isEntry) { cssEntriesMap .get(this.environment)! - .set(chunk.name, referenceId) + .set(originalFileName ?? chunk.name, { + referenceId, + name: chunk.name, + }) } chunk.viteMetadata!.importedCss.add( this.getFileName(referenceId), @@ -1107,9 +1110,15 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { if (emptyJsPlaceholder.isEntry) { const { importedAssets, importedCss } = emptyJsPlaceholder.viteMetadata! + const entryKey = + getChunkOriginalFileName( + emptyJsPlaceholder, + config.root, + false, + ) ?? emptyJsPlaceholder.name const cssReferenceId = cssEntriesMap .get(this.environment)! - .get(emptyJsPlaceholder.name)! + .get(entryKey)!.referenceId const realCssEntryName = this.getFileName(cssReferenceId) const realCssEntry = bundle[realCssEntryName]! importedCss.delete(realCssEntryName) diff --git a/packages/vite/src/node/plugins/manifest.ts b/packages/vite/src/node/plugins/manifest.ts index 70ec394874663a..b58e0f738b5524 100644 --- a/packages/vite/src/node/plugins/manifest.ts +++ b/packages/vite/src/node/plugins/manifest.ts @@ -99,9 +99,13 @@ export function manifestPlugin(): Plugin { isOutputOptionsForLegacyChunks: environment.config.isOutputOptionsForLegacyChunks, cssEntries() { - return Object.fromEntries( - cssEntriesMap.get(envs[environment.name])!.entries(), - ) + const cssEntries: Record = {} + for (const [, entry] of cssEntriesMap + .get(envs[environment.name])! + .entries()) { + cssEntries[entry.name] = entry.referenceId + } + return cssEntries }, }), { From 0d3b6bfa95b4815585345689f0c6f58ca49267aa Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Fri, 10 Apr 2026 17:11:23 +0900 Subject: [PATCH 2/4] test: should ensure `isEntry` is set --- packages/vite/src/node/__tests__/build.spec.ts | 5 ++--- .../__tests__/fixtures/css-entry-same-basename/a/asset-a.svg | 3 --- .../__tests__/fixtures/css-entry-same-basename/a/index.css | 2 +- .../__tests__/fixtures/css-entry-same-basename/b/asset-b.svg | 3 --- .../__tests__/fixtures/css-entry-same-basename/b/index.css | 2 +- 5 files changed, 4 insertions(+), 11 deletions(-) delete mode 100644 packages/vite/src/node/__tests__/fixtures/css-entry-same-basename/a/asset-a.svg delete mode 100644 packages/vite/src/node/__tests__/fixtures/css-entry-same-basename/b/asset-b.svg diff --git a/packages/vite/src/node/__tests__/build.spec.ts b/packages/vite/src/node/__tests__/build.spec.ts index 3209bc16af00d1..024bba0322a447 100644 --- a/packages/vite/src/node/__tests__/build.spec.ts +++ b/packages/vite/src/node/__tests__/build.spec.ts @@ -163,12 +163,11 @@ describe('build', () => { (result.output.find((o) => o.fileName === '.vite/manifest.json') as any) .source, ) - expect(manifest['a/index.css']).toMatchObject({ - assets: [expect.stringMatching(/assets\/asset-a-[-\w]{8}\.svg/)], + isEntry: true, }) expect(manifest['b/index.css']).toMatchObject({ - assets: [expect.stringMatching(/assets\/asset-b-[-\w]{8}\.svg/)], + isEntry: true, }) }) diff --git a/packages/vite/src/node/__tests__/fixtures/css-entry-same-basename/a/asset-a.svg b/packages/vite/src/node/__tests__/fixtures/css-entry-same-basename/a/asset-a.svg deleted file mode 100644 index adb7b6bcff3d8d..00000000000000 --- a/packages/vite/src/node/__tests__/fixtures/css-entry-same-basename/a/asset-a.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/vite/src/node/__tests__/fixtures/css-entry-same-basename/a/index.css b/packages/vite/src/node/__tests__/fixtures/css-entry-same-basename/a/index.css index 7468510d58df10..98b1044b009ca5 100644 --- a/packages/vite/src/node/__tests__/fixtures/css-entry-same-basename/a/index.css +++ b/packages/vite/src/node/__tests__/fixtures/css-entry-same-basename/a/index.css @@ -1,3 +1,3 @@ .entry-a { - background-image: url('./asset-a.svg'); + background-color: red; } diff --git a/packages/vite/src/node/__tests__/fixtures/css-entry-same-basename/b/asset-b.svg b/packages/vite/src/node/__tests__/fixtures/css-entry-same-basename/b/asset-b.svg deleted file mode 100644 index c79e8f52e51f4e..00000000000000 --- a/packages/vite/src/node/__tests__/fixtures/css-entry-same-basename/b/asset-b.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/vite/src/node/__tests__/fixtures/css-entry-same-basename/b/index.css b/packages/vite/src/node/__tests__/fixtures/css-entry-same-basename/b/index.css index e6632c31ad8e3f..ad560331d88c6d 100644 --- a/packages/vite/src/node/__tests__/fixtures/css-entry-same-basename/b/index.css +++ b/packages/vite/src/node/__tests__/fixtures/css-entry-same-basename/b/index.css @@ -1,3 +1,3 @@ .entry-b { - background-image: url('./asset-b.svg'); + background-color: blue; } From 8d8cb836308d5b06794f7ef473e721abd8619061 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Fri, 10 Apr 2026 17:12:42 +0900 Subject: [PATCH 3/4] fix: pass cssEntries in reverse key-value order --- packages/vite/src/node/plugins/manifest.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/vite/src/node/plugins/manifest.ts b/packages/vite/src/node/plugins/manifest.ts index b58e0f738b5524..feff710a7bc9f0 100644 --- a/packages/vite/src/node/plugins/manifest.ts +++ b/packages/vite/src/node/plugins/manifest.ts @@ -99,13 +99,13 @@ export function manifestPlugin(): Plugin { isOutputOptionsForLegacyChunks: environment.config.isOutputOptionsForLegacyChunks, cssEntries() { - const cssEntries: Record = {} - for (const [, entry] of cssEntriesMap - .get(envs[environment.name])! - .entries()) { - cssEntries[entry.name] = entry.referenceId - } - return cssEntries + return Object.fromEntries( + [...cssEntriesMap.get(envs[environment.name])!.values()].map( + ({ name, referenceId }) => { + return [referenceId, name] + }, + ), + ) }, }), { From c96bffa8bab8d157043b71b26dc873b3033dfe02 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Fri, 10 Apr 2026 17:12:59 +0900 Subject: [PATCH 4/4] refactor: use chunk.filename --- packages/vite/src/node/plugins/css.ts | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index d6805a8553ef5c..9ef1fa74b9f5b6 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -914,10 +914,7 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { if (isEntry) { cssEntriesMap .get(this.environment)! - .set(originalFileName ?? chunk.name, { - referenceId, - name: chunk.name, - }) + .set(chunk.fileName, { referenceId, name: chunk.name }) } chunk.viteMetadata!.importedCss.add( this.getFileName(referenceId), @@ -1106,19 +1103,13 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { const removedPureCssFiles = removedPureCssFilesCache.get(config)! pureCssChunkNames.forEach((fileName) => { - const emptyJsPlaceholder = bundle[fileName] as RenderedChunk + const emptyJsPlaceholder = bundle[fileName] as OutputChunk if (emptyJsPlaceholder.isEntry) { const { importedAssets, importedCss } = emptyJsPlaceholder.viteMetadata! - const entryKey = - getChunkOriginalFileName( - emptyJsPlaceholder, - config.root, - false, - ) ?? emptyJsPlaceholder.name const cssReferenceId = cssEntriesMap .get(this.environment)! - .get(entryKey)!.referenceId + .get(emptyJsPlaceholder.preliminaryFileName)!.referenceId const realCssEntryName = this.getFileName(cssReferenceId) const realCssEntry = bundle[realCssEntryName]! importedCss.delete(realCssEntryName)