Skip to content

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

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

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions