Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/floppy-bottles-open.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Improves Vue scoped style handling in DEV mode during client router navigation.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import Layout from '../components/Layout.astro';
<a id="click-redirect" href="/redirect">redirect cross-origin</a>
<a id="click-404" href="/undefined-page">go to undefined page</a>
<a id="click-inline-module" href="/inline-module">go to inline module</a>
<a id="click-vue-scoped-styles" href="/island-vue-one">go to Vue scoped styles</a>
<custom-a id="custom-click-two">
<template shadowrootmode="open">
<a href="/two">go to 2</a>
Expand Down
36 changes: 34 additions & 2 deletions packages/astro/e2e/view-transitions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand Down Expand Up @@ -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');
Expand All @@ -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);
});
});
37 changes: 31 additions & 6 deletions packages/astro/src/transitions/swap-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, HTMLStyleElement>();

Comment thread
martrapp marked this conversation as resolved.
const scriptsAlreadyRan = new Set<string>();
export function detectScriptExecuted(script: HTMLScriptElement) {
const key = script.src ? new URL(script.src, location.href).href : script.textContent!;
Expand Down Expand Up @@ -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);
Comment thread
martrapp marked this conversation as resolved.
});
Comment thread
martrapp marked this conversation as resolved.
} else {
document.head.append(...doc.head.children);
}
}

export function swapBodyElement(newElement: Element, oldElement: Element) {
Expand Down Expand Up @@ -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 => {
Expand All @@ -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}"]`);
}
}
Expand Down
Loading