diff --git a/.changeset/floppy-bottles-open.md b/.changeset/floppy-bottles-open.md
new file mode 100644
index 000000000000..d74368ab168e
--- /dev/null
+++ b/.changeset/floppy-bottles-open.md
@@ -0,0 +1,5 @@
+---
+'astro': patch
+---
+
+Improves Vue scoped style handling in DEV mode during client router navigation.
diff --git a/packages/astro/e2e/fixtures/view-transitions/src/pages/one.astro b/packages/astro/e2e/fixtures/view-transitions/src/pages/one.astro
index 12e0db2cf538..eee063b6d416 100644
--- a/packages/astro/e2e/fixtures/view-transitions/src/pages/one.astro
+++ b/packages/astro/e2e/fixtures/view-transitions/src/pages/one.astro
@@ -16,6 +16,7 @@ import Layout from '../components/Layout.astro';
redirect cross-origin
go to undefined page
go to inline module
+ go to Vue scoped styles
go to 2
diff --git a/packages/astro/e2e/view-transitions.test.js b/packages/astro/e2e/view-transitions.test.js
index 31e5afbdbf37..08752b7fce7c 100644
--- a/packages/astro/e2e/view-transitions.test.js
+++ b/packages/astro/e2e/view-transitions.test.js
@@ -941,7 +941,7 @@ test.describe('View Transitions', () => {
await page.click('#click-redirect');
p = page.locator('#two');
await expect(p, 'should have content').toHaveText('Page 2');
-
+
expect(consoleErrors.length, 'There should be no errors').toEqual(0);
});
@@ -1822,7 +1822,7 @@ test.describe('View Transitions', () => {
await expect(page.locator('#preload')).toHaveCount(1);
});
- test('Styles with data-vite-dev-id persist through head swap', async ({ page, astro }) => {
+ test('Vue scoped styles persist through head swap', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/island-vue-one'));
let cnt = page.locator('.counter pre');
await expect(cnt).toHaveText('AA0');
@@ -1838,4 +1838,36 @@ test.describe('View Transitions', () => {
page.locator('[data-vite-dev-id*="VueCounter.vue?vue&type=style"][data-marker="this"]'),
).toHaveCount(1);
});
+
+ test('Vue scoped styles persist through head swap even if they had been removed by navigation', async ({
+ page,
+ astro,
+ }) => {
+ await page.goto(astro.resolveUrl('/one'));
+ let p = page.locator('#one');
+ await expect(p, 'should have content').toHaveText('Page 1');
+ await page.click('#click-vue-scoped-styles');
+
+ let cnt = page.locator('.counter pre');
+ await expect(cnt).toHaveText('AA0');
+ await page
+ .locator('[data-vite-dev-id*="VueCounter.vue?vue&type=style"]').last()
+ .evaluate((el) => (el.dataset.marker = 'this'), undefined);
+ await page.goBack();
+ p = page.locator('#one');
+ await expect(p, 'should have content').toHaveText('Page 1');
+ await expect(
+ page.locator('[data-vite-dev-id*="VueCounter.vue?vue&type=style"][data-marker="this"]'),
+ ).toHaveCount(0);
+
+
+ await page.click('#click-vue-scoped-styles');
+
+ cnt = page.locator('.counter pre');
+ await expect(cnt).toHaveText('AA0');
+
+ await expect(
+ page.locator('[data-vite-dev-id*="VueCounter.vue?vue&type=style"][data-marker="this"]'),
+ ).toHaveCount(1);
+ });
});
diff --git a/packages/astro/src/transitions/swap-functions.ts b/packages/astro/src/transitions/swap-functions.ts
index e92e38090ed7..4956ee574ad2 100644
--- a/packages/astro/src/transitions/swap-functions.ts
+++ b/packages/astro/src/transitions/swap-functions.ts
@@ -8,6 +8,8 @@ const PERSIST_ATTR = 'data-astro-transition-persist';
const NON_OVERRIDABLE_ASTRO_ATTRS = ['data-astro-transition', 'data-astro-transition-fallback'];
+const knownVueScopedStyles = new Map();
+
const scriptsAlreadyRan = new Set();
export function detectScriptExecuted(script: HTMLScriptElement) {
const key = script.src ? new URL(script.src, location.href).href : script.textContent!;
@@ -61,13 +63,25 @@ export function swapHeadElements(doc: Document) {
if (newEl) {
newEl.remove();
} else {
- // Otherwise, remove the element in the head. It doesn't exist in the new page.
+ if (import.meta.env.DEV && el instanceof HTMLStyleElement) {
+ // In DEV mode, keep updated Vue scoped styles for later reuse
+ const viteDevId = vueScopedStyleId(el);
+ viteDevId && knownVueScopedStyles.set(viteDevId, el);
+ }
+ // If the element does not exist in the new document, remove the element from current the head.
el.remove();
}
}
// Everything left in the new head is new, append it all.
- document.head.append(...doc.head.children);
+ if (import.meta.env.DEV) {
+ // In DEV mode, replace known Vue scoped styles with the versions we remembered
+ [...doc.head.children].forEach((child) => {
+ document.head.append(knownVueScopedStyles.get((child as any).dataset?.viteDevId) || child);
+ });
+ } else {
+ document.head.append(...doc.head.children);
+ }
}
export function swapBodyElement(newElement: Element, oldElement: Element) {
@@ -173,6 +187,17 @@ export const restoreFocus = ({ activeElement, start, end }: SavedFocus) => {
}
};
+export const vueScopedStyleId = (el: HTMLStyleElement): string => {
+ const viteDevId = el.dataset.viteDevId || '';
+
+ const url = new URL(viteDevId, location.href);
+ return url.searchParams.get('vue') !== null &&
+ url.searchParams.get('type') === 'style' &&
+ url.searchParams.has('scoped')
+ ? viteDevId
+ : '';
+};
+
// Check for a head element that should persist and returns it,
// either because it has the data attribute or because replacing it would cause avoidable FOUC.
const persistedHeadElement = (el: HTMLElement, newDoc: Document): Element | null => {
@@ -190,12 +215,12 @@ const persistedHeadElement = (el: HTMLElement, newDoc: Document): Element | null
// Match these by their stable dev ID so the already-transformed style is preserved
// across ClientRouter soft navigations instead of being replaced by the raw version.
// There are other ids that can't be preserved and need a refresh, like Uno's /__uno.css,
- // which keeps the id with different contents.
+ // which keeps the same id, but with different contents.
// To avoid enumerating all exceptions, we only apply the auto-persist logic to elements
// that look like Vue's dev styles.
- if (import.meta.env.DEV && el.tagName === 'STYLE') {
- const viteDevId = el.getAttribute('data-vite-dev-id');
- if (/\?vue&type=style&.*lang.css$/.test(viteDevId || '')) {
+ if (import.meta.env.DEV && el instanceof HTMLStyleElement) {
+ const viteDevId = vueScopedStyleId(el);
+ if (viteDevId) {
return newDoc.head.querySelector(`style[data-vite-dev-id="${viteDevId}"]`);
}
}