fix(css): cache CSS after integration transforms to fix UnoCSS @apply with View Transitions#16383
Conversation
Split astroDevCssPlugin to return { plugins, cssCachePlugin }.
The cache plugin runs AFTER integration plugins so CSS is cached
after @apply and other integration transforms have resolved.
Part of fix for withastro#16373.
…navigations Extend vueScopedStyleId() to also match ?astro&type=style&...css styles alongside Vue scoped styles. Prevents processed CSS being replaced with unprocessed versions during soft navigations. Part of fix for withastro#16373.
🦋 Changeset detectedLatest commit: f983897 The changes in this PR will be included in the next version bump. Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
There was a problem hiding this comment.
Pull request overview
This PR adjusts Astro’s dev-time CSS handling to avoid caching unprocessed CSS before integration transforms (e.g. UnoCSS @apply) and to preserve transformed component styles across ClientRouter soft navigations (View Transitions).
Changes:
- Extracts dev CSS caching into a dedicated Vite plugin intended to run after integration plugins.
- Extends soft-navigation head persistence logic to also persist Astro component style
<style data-vite-dev-id="...">entries (in addition to Vue scoped styles). - Adds a changeset documenting the patch fix.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| packages/astro/src/vite-plugin-css/index.ts | Splits CSS caching transform into a standalone plugin returned alongside the existing dev-css plugins. |
| packages/astro/src/core/create-vite.ts | Registers dev-css plugins normally, then appends the CSS cache plugin after config merges to ensure it runs after integration transforms. |
| packages/astro/src/transitions/swap-functions.ts | Expands DEV head style persistence matching to include Astro component style module IDs. |
| .changeset/fix-issue-16373-unocss-view-transitions.md | Adds a patch changeset describing the fix for UnoCSS + View Transitions in astro dev. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Add the CSS cache plugin AFTER integration plugins have been merged. | ||
| // This ensures the CSS caching transform runs after integration transforms | ||
| // (e.g. UnoCSS's @apply directive processing) so we cache fully-processed CSS. | ||
| (result.plugins as vite.PluginOption[]).push(devCss.cssCachePlugin); |
There was a problem hiding this comment.
devCss.cssCachePlugin is pushed onto the Vite plugin list unconditionally, but the cache is only used in dev (and the plugin becomes a no-op for command === 'build'). Consider only pushing this plugin when command === 'dev' to keep the build plugin chain minimal.
| (result.plugins as vite.PluginOption[]).push(devCss.cssCachePlugin); | |
| if (command === 'dev') { | |
| (result.plugins as vite.PluginOption[]).push(devCss.cssCachePlugin); | |
| } |
| // Match Astro component styles: ?astro&type=style&...lang.css | ||
| const isVueScoped = | ||
| url.searchParams.get('vue') !== null && | ||
| url.searchParams.get('type') === 'style' && | ||
| url.searchParams.has('scoped') | ||
| ? viteDevId | ||
| : ''; | ||
| url.searchParams.has('scoped'); | ||
| const isAstroStyle = /\?astro&type=style&.*lang\.css$/.test(viteDevId); | ||
| return isVueScoped || isAstroStyle ? viteDevId : ''; |
There was a problem hiding this comment.
isAstroStyle only matches ...&lang.css IDs, but Astro component styles can be emitted with other preprocessors (e.g. &lang.scss, &lang.less) based on attrs.lang. This means those styles still won’t be auto-persisted during soft navigations. Consider detecting Astro style IDs via URLSearchParams (e.g. astro + type=style + any query key starting with lang.) or loosening the regex to match lang\.[a-z0-9]+ rather than only lang\.css.
| @@ -191,11 +191,14 @@ export const vueScopedStyleId = (el: HTMLStyleElement): string => { | |||
| const viteDevId = el.dataset.viteDevId || ''; | |||
|
|
|||
| const url = new URL(viteDevId, location.href); | |||
| return url.searchParams.get('vue') !== null && | |||
| // Match Vue scoped styles: ?vue&type=style&scoped | |||
| // Match Astro component styles: ?astro&type=style&...lang.css | |||
| const isVueScoped = | |||
| url.searchParams.get('vue') !== null && | |||
| url.searchParams.get('type') === 'style' && | |||
| url.searchParams.has('scoped') | |||
| ? viteDevId | |||
| : ''; | |||
| url.searchParams.has('scoped'); | |||
| const isAstroStyle = /\?astro&type=style&.*lang\.css$/.test(viteDevId); | |||
| return isVueScoped || isAstroStyle ? viteDevId : ''; | |||
There was a problem hiding this comment.
vueScopedStyleId now also returns IDs for Astro component styles, so the name (and nearby DEV-only comments/variables like knownVueScopedStyles) no longer accurately reflect what the function does. Renaming to something framework-agnostic (and updating associated comments/identifiers) would reduce confusion for future maintenance.
Only push the CSS cache plugin during dev mode since it becomes a no-op during builds. Keeps the build plugin chain minimal. Address review comment on withastro#16383.
Loosen regex from lang\.css to lang\.[a-z0-9]+ to match .scss, .less, .sass and other preprocessor variants. Address review comment on withastro#16383.
Renames vueScopedStyleId -> devStyleId and knownVueScopedStyles -> knownDevStyles since the function now handles both Vue and Astro component styles. Updates related comments for clarity. Address review comment on withastro#16383.
ESLint: use a ! assertion to more succinctly remove null and undefined from the type. Address lint failure on withastro#16383.
Summary
astro:dev-css-cacheplugin registered after integration plugins, so that CSS is cached after UnoCSS (and similar) have resolved@applydirectivesswap-functions.tsto also auto-persist Astro component styles (?astro&type=style&...lang.css) during ClientRouter soft navigations, preventing processed CSS from being replaced with raw unprocessed versionsRoot Cause
CSS cache ordering: The
transformhook inMODULE_DEV_CSScached CSS before integration plugins like UnoCSS'stransformerDirectivescould process@applydirectives. The fix extracts caching into a plugin pushed to the end of the Vite plugin chain (after integration merges).Style persistence gap:
persistedHeadElementonly matched Vue scoped styles (?vue&type=style&...) but not Astro component styles (?astro&type=style&...). Processed Astro styles were replaced on every soft navigation.Test Plan
bun run test:unit— all tests should pass (pre-existing failures unrelated to this change)@applydirectives and View Transitions — styles should persist across soft navigations