Skip to content

CSS entry points sharing the same basename in different directories produce incorrect cross-linked css dependencies in manifest #22013

@chamby

Description

@chamby

Describe the bug

vite-css-cross-link-repro.zip

CSS-only entry points with the same filename in different directories are incorrectly cross-linked in the manifest

Environment

  • Vite: 8.0.2
  • Rolldown: 1.0.0-rc.11
  • OS: macOS (Darwin 24.6.0)
  • CSS preprocessor: Sass (via scss)

Description

When two CSS-only entry points (SCSS files) in different directories share the same base filename, Vite's CSS post-processing incorrectly adds one as a CSS dependency of the other in the build manifest. This causes both stylesheets to be loaded at runtime when only one was requested, leading to unintended style overrides.

Reproduction

A minimal reproduction project is attached: vite-css-cross-link-repro.zip. To reproduce:

npm install
npm run build
cat dist/.vite/manifest.json

The project contains two independent SCSS entry points that share the same base filename in different directories:

resources/assets/scss/
├── store/skins/
│   └── store_skin_85535.scss    ← entry point A (color: #f75a38)
└── store3/skins/
    └── store_skin_85535.scss    ← entry point B (color: #ffffff)

The vite.config.js lists both as build inputs with build.manifest: true. No other plugins are needed to trigger the bug.

Expected manifest output

Each entry should be independent — no cross-references:

{
  "resources/assets/scss/store/skins/store_skin_85535.scss": {
    "file": "assets/store_skin_85535-AAAA.css",
    "src": "resources/assets/scss/store/skins/store_skin_85535.scss"
  },
  "resources/assets/scss/store3/skins/store_skin_85535.scss": {
    "file": "assets/store_skin_85535-BBBB.css",
    "src": "resources/assets/scss/store3/skins/store_skin_85535.scss",
    "isEntry": true
  }
}

Actual manifest output

The store3 entry incorrectly lists the store entry's CSS file as a dependency (output from the minimal reproduction):

{
  "resources/assets/scss/store/skins/store_skin_85535.scss": {
    "file": "assets/store_skin_85535-wmVeozrv.css",
    "src": "resources/assets/scss/store/skins/store_skin_85535.scss"
  },
  "resources/assets/scss/store3/skins/store_skin_85535.scss": {
    "file": "assets/store_skin_85535-CixdYeUC.css",
    "name": "store_skin_85535",
    "names": [
      "store_skin_85535.css"
    ],
    "src": "resources/assets/scss/store3/skins/store_skin_85535.scss",
    "isEntry": true,
    "css": [
      "assets/store_skin_85535-wmVeozrv.css"
    ]
  }
}

The "css" array causes the framework's @vite() directive to emit <link> tags for both files when only the store3 version was requested.

Impact

  • The two files compile to CSS with identical selectors but different property values (they are different versions/generations of the same theme).
  • Because both are loaded, the unwanted stylesheet's rules override the intended ones based on source order.
  • In our case, color: #fff (correct, from store3) is overridden by color: #f75a38 (wrong, from store), making button text invisible against its background.
  • This only manifests in production builds — the Vite dev server serves each entry independently and does not exhibit the cross-linking.

Scale

This is not limited to a single file. In our build, every pair of SCSS entry points that share the same base filename across the store/ and store3/ directories is affected (24 pairs total). The cross-link is one-directional: store3 entries pull in store entries, but not vice versa.

Root cause

The bug is in Vite's CSS post-processing plugin, not in Rolldown's bundling logic. Rolldown correctly assigns distinct facadeModuleId values to each chunk but gives both the same chunk.name (store_skin_85535) since they share a basename — this is standard bundler behaviour.

Vite's cssEntriesMap uses chunk.name as a Map key when registering CSS entry chunks:

// vite/src/node/plugins/css.ts — during renderChunk
cssEntriesMap.get(this.environment).set(chunk.name, referenceId);

Because both entries share chunk.name = "store_skin_85535", the second write overwrites the first. Later, when resolving the "real" CSS asset for each pure-CSS entry chunk:

// vite/src/node/plugins/css.ts — during generateBundle
const cssReferenceId = cssEntriesMap.get(this.environment).get(emptyJsPlaceholder.name);
const realCssEntryName = this.getFileName(cssReferenceId);
const realCssEntry = bundle[realCssEntryName];
importedCss.delete(realCssEntryName);          // fails — wrong filename
if (importedCss.size) realCssEntry.viteMetadata.importedCss = importedCss;  // spurious transfer

The first placeholder processed looks up the overwritten reference, gets the wrong CSS asset, fails to self-delete from importedCss, and transfers its CSS file as a dependency of the other entry's asset. This surfaces in the manifest as the spurious "css" array.

A fix would need to use a unique key (e.g., facadeModuleId or the full relative input path) instead of chunk.name.

Reproduction

see included
vite-css-cross-link-repro.zip

Steps to reproduce

see included
vite-css-cross-link-repro.zip

System Info

System:
    OS: macOS 26.3.1
    CPU: (10) arm64 Apple M1 Max
    Memory: 9.23 GB / 64.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 25.8.0 - /opt/homebrew/bin/node
    Yarn: 1.22.22 - /opt/homebrew/bin/yarn
    npm: 11.11.0 - /opt/homebrew/bin/npm
    Deno: 2.7.3 - /opt/homebrew/bin/deno
  Browsers:
    Chrome: 146.0.7680.155
    Firefox Developer Edition: 149.0
    Safari: 26.3.1
  npmPackages:
    vite: ^8.0.2 => 8.0.2

Used Package Manager

npm

Logs

No response

Validations

Metadata

Metadata

Assignees

Labels

feat: buildp2-edge-caseBug, but has workaround or limited in scope (priority)

Type

No fields configured for Bug.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions