Skip to content

fix(css): cache CSS after integration transforms to fix UnoCSS @apply with View Transitions#16383

Open
ossaidqadri wants to merge 7 commits intowithastro:mainfrom
ossaidqadri:fix/issue-16373-unocss-apply-view-transitions
Open

fix(css): cache CSS after integration transforms to fix UnoCSS @apply with View Transitions#16383
ossaidqadri wants to merge 7 commits intowithastro:mainfrom
ossaidqadri:fix/issue-16373-unocss-apply-view-transitions

Conversation

@ossaidqadri
Copy link
Copy Markdown
Contributor

Summary

  • Extract CSS caching transform into a separate astro:dev-css-cache plugin registered after integration plugins, so that CSS is cached after UnoCSS (and similar) have resolved @apply directives
  • Extend style persistence logic in swap-functions.ts to also auto-persist Astro component styles (?astro&type=style&...lang.css) during ClientRouter soft navigations, preventing processed CSS from being replaced with raw unprocessed versions

Root Cause

  1. CSS cache ordering: The transform hook in MODULE_DEV_CSS cached CSS before integration plugins like UnoCSS's transformerDirectives could process @apply directives. The fix extracts caching into a plugin pushed to the end of the Vite plugin chain (after integration merges).

  2. Style persistence gap: persistedHeadElement only 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

  • Run bun run test:unit — all tests should pass (pre-existing failures unrelated to this change)
  • Run a dev server with UnoCSS + @apply directives and View Transitions — styles should persist across soft navigations
  • CI passes

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.
Copilot AI review requested due to automatic review settings April 17, 2026 19:14
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 17, 2026

🦋 Changeset detected

Latest 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

@github-actions github-actions Bot added the pkg: astro Related to the core `astro` package (scope) label Apr 17, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread packages/astro/src/core/create-vite.ts Outdated
// 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);
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
(result.plugins as vite.PluginOption[]).push(devCss.cssCachePlugin);
if (command === 'dev') {
(result.plugins as vite.PluginOption[]).push(devCss.cssCachePlugin);
}

Copilot uses AI. Check for mistakes.
Comment on lines +195 to +201
// 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 : '';
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +190 to +201
@@ -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 : '';
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Apr 17, 2026

Merging this PR will not alter performance

✅ 18 untouched benchmarks


Comparing ossaidqadri:fix/issue-16373-unocss-apply-view-transitions (f983897) with main (5a84551)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (688044f) during the generation of this report, so 5a84551 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pkg: astro Related to the core `astro` package (scope)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants