withastro / astro · Issue No. 18060
Astro v7.3.3
Node v22.23.2
System macOS (arm64)
Package Manager pnpm
Output server
Adapter @astrojs/cloudflare
Integrations check-product-pages
@astrojs/react
emdash
No response
In astro dev, a route's <head> can end up with <style> and <script> tags for CSS that belongs to completely unrelated pages, if that route (or anything it imports) reaches astro:config/server, or anything else that pulls in virtual:astro:manifest.
Root cause: the dev-mode per-route CSS collector (ensureModulesLoaded and collectCSSWithOrder in packages/astro/src/vite-plugin-css/index.ts) walks a route's own Vite module graph downward via moduleGraph.importedModules to decide what CSS it needs. astro:config/server transitively imports virtual:astro:manifest, which imports virtual:astro:pages — the module that dynamically imports every page in the app so the router can dispatch to any of them. The walk doesn't stop there, so it continues into every other page's own import graph and attaches all of their CSS too.
This is the same bug class as #16115 / #16179, which fixed the equivalent leak in the production build CSS graph walk (packages/astro/src/core/build/plugins/plugin-css.ts, via isBuildCssBoundary and moduleIsTopLevelPage). That fix was never mirrored onto the dev-mode collector. The triage bot's own analysis on both of those issues actually concluded dev mode was safe from this class of bug ("Dev mode uses a different CSS collection pipeline that walks down the dependency tree from each page independently... The dev pipeline naturally scopes CSS to the requesting page.") — that reasoning doesn't hold once the downward walk passes through virtual:astro:pages, since that module itself points back down into every page.
One reproduction wrinkle: this only shows up on routes that are on-demand rendered (route.prerender === false), because virtual:astro:pages's load() handler filters routes by environment in dev (getRoutesForEnvironment() in vite-plugin-pages/pages.ts), and the "ssr" environment (which the CSS collector reads from) only includes non-prerendered routes. A default output: 'static' project won't reproduce it — you need output: 'server' with an adapter.
A route's dev-mode <head> should only include CSS reachable from its own real import graph — not CSS from every other page in the project just because something it imports needed the routing manifest.
Relay reads this issue against the repository's contribution signals: the files it is likely to touch, how the maintainers triage work this size, and what the first contribution would exercise.
The full analysis for this issue is still being assembled. Until then, the description above and the thread on GitHub are the most reliable context.