Skip to content

Stylesheet links missing from HTML entries that share a cyclic chunk #23628

Description

@gkurt

Describe the bug

With more than one HTML entry, a page can be built without some of its <link rel="stylesheet"> tags. That happens when the chunks it imports form a cycle. Which page loses which stylesheet depends on the order vite processes the HTML entries in.

getCssFilesForChunk stops at a chunk it is already visiting, which prevents infinite recursion. The list it then caches for the chunk that hit the cycle is incomplete: it lacks the stylesheets of the chunk still being visited. That doesn't hurt the entry being walked, because the visiting chunk adds its own stylesheets further up. But analyzedImportedCssFiles is shared by every HTML entry in generateBundle, so the next entry that reaches that chunk reuses the incomplete list.

In the reproduction, index.html -> shared <-> feature is walked first. feature is cached without shared.css. dashboard.html -> feature then reuses that list, and dashboard.html ships without shared.css.

Giving each HTML entry its own cache fixes it. In packages/vite/src/node/plugins/html.ts, getCssTagsForChunk would call getCssFilesForChunk(chunk, bundle, new Map()) instead of sharing analyzedImportedCssFiles. With that change, 10 of 10 builds of the reproduction link both stylesheets on both pages. A narrower alternative is to skip caching a chunk whose walk hit a chunk still being visited.

Related: #19143 introduced the cache for ordering. #21298 (closed) and #22017 fixed missing styles across entries when seenCss differed. The unit tests from #22016 cover circular imports only for termination, not for what is cached.

Reproduction

https://github.com/gkurt/vite-css-cycle-repro

Steps to reproduce

npm install
npm run check

check builds 10 times and reports the stylesheets each page is missing:

6/10  index.html: ok | dashboard.html: missing shared.css
4/10  index.html: missing feature.css | dashboard.html: ok

To see it in the browser:

npm start

Each page has a row per stylesheet. A row is red ("missing") by default, and the stylesheet itself turns it green ("loaded"). Open /index.html and /dashboard.html; one of them shows a red row. shots/ has a capture of each from a build where dashboard.html lost shared.css. Under vite (dev) both pages show both rows green. StackBlitz didn't reproduce it either, so run it locally.

Both pages import both shared.js and feature.js, so both should link shared.css and feature.css.

This isn't specific to rolldown. Vite 7.3.6 with manualChunks produces the same result once output.hoistTransitiveImports is false (8 of 8 builds wrong). With the default true, Rollup adds both chunks to each entry's imports, which hides the bad cache entry. Rolldown does not hoist transitive imports, so on Vite 8 this shows up with default options.

We hit this in a production app with index.html and dashboard.html. About half of all builds shipped index.html without the stylesheet holding Tailwind. The cycle came from rolldown's strictExecutionOrder, which makes chunks import each other's module initializers.

Screenshots (from npm start):

index.html loads shared.css and feature.css
dashboard.html is missing shared.css

System Info

System:
  OS: macOS 26.7
  CPU: (12) arm64 Apple M3 Pro
Binaries:
  Node: 24.20.0
  npm: 11.19.0
npmPackages:
  vite: 8.3.1 => 8.3.1

Used Package Manager

npm

Logs

No warnings or errors. The build succeeds and the output is missing the <link> tags.

Validations

Activity

  1. added theissue type on Oct 8, 2026
  2. added a commit that references this issue on Oct 8, 2026
    15d82f7
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    feat: htmlp3-minor-bugAn edge case that only affects very specific usage (priority)

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions