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:
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):


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
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.getCssFilesForChunkstops 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. ButanalyzedImportedCssFilesis shared by every HTML entry ingenerateBundle, so the next entry that reaches that chunk reuses the incomplete list.In the reproduction,
index.html -> shared <-> featureis walked first.featureis cached withoutshared.css.dashboard.html -> featurethen reuses that list, anddashboard.htmlships withoutshared.css.Giving each HTML entry its own cache fixes it. In
packages/vite/src/node/plugins/html.ts,getCssTagsForChunkwould callgetCssFilesForChunk(chunk, bundle, new Map())instead of sharinganalyzedImportedCssFiles. 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
seenCssdiffered. 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
checkbuilds 10 times and reports the stylesheets each page is missing:To see it in the browser:
Each page has a row per stylesheet. A row is red ("missing") by default, and the stylesheet itself turns it green ("loaded"). Open
/index.htmland/dashboard.html; one of them shows a red row.shots/has a capture of each from a build wheredashboard.htmllostshared.css. Undervite(dev) both pages show both rows green. StackBlitz didn't reproduce it either, so run it locally.Both pages import both
shared.jsandfeature.js, so both should linkshared.cssandfeature.css.This isn't specific to rolldown. Vite 7.3.6 with
manualChunksproduces the same result onceoutput.hoistTransitiveImportsisfalse(8 of 8 builds wrong). With the defaulttrue, 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.htmlanddashboard.html. About half of all builds shippedindex.htmlwithout the stylesheet holding Tailwind. The cycle came from rolldown'sstrictExecutionOrder, which makes chunks import each other's module initializers.Screenshots (from
npm start):System Info
Used Package Manager
npm
Logs
No warnings or errors. The build succeeds and the output is missing the
<link>tags.Validations