From 8f69b2459fb6d130dcbd1f226ad94e97e35f931b Mon Sep 17 00:00:00 2001 From: Martin Trapp <94928215+martrapp@users.noreply.github.com> Date: Sun, 15 Oct 2023 14:53:24 +0200 Subject: [PATCH 01/15] Persist styles of persistent client:only components during view transitions --- .changeset/purple-dots-refuse.md | 5 ++ .../src/components/Island.jsx | 1 + .../view-transitions/src/components/css.js | 3 + .../src/components/other.postcss | 1 + packages/astro/e2e/view-transitions.test.js | 4 +- packages/astro/src/transitions/router.ts | 62 ++++++++++++++++--- .../transitions/vite-plugin-transitions.ts | 26 ++++++++ 7 files changed, 92 insertions(+), 10 deletions(-) create mode 100644 .changeset/purple-dots-refuse.md create mode 100644 packages/astro/e2e/fixtures/view-transitions/src/components/css.js create mode 100644 packages/astro/e2e/fixtures/view-transitions/src/components/other.postcss diff --git a/.changeset/purple-dots-refuse.md b/.changeset/purple-dots-refuse.md new file mode 100644 index 000000000000..01c0bc14c497 --- /dev/null +++ b/.changeset/purple-dots-refuse.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Persist styles of persistent client:only components during view transitions diff --git a/packages/astro/e2e/fixtures/view-transitions/src/components/Island.jsx b/packages/astro/e2e/fixtures/view-transitions/src/components/Island.jsx index cde38498028b..734e2011b25b 100644 --- a/packages/astro/e2e/fixtures/view-transitions/src/components/Island.jsx +++ b/packages/astro/e2e/fixtures/view-transitions/src/components/Island.jsx @@ -1,5 +1,6 @@ import React, { useState } from 'react'; import './Island.css'; +import { indirect} from './css.js'; export default function Counter({ children, count: initialCount, id }) { const [count, setCount] = useState(initialCount); diff --git a/packages/astro/e2e/fixtures/view-transitions/src/components/css.js b/packages/astro/e2e/fixtures/view-transitions/src/components/css.js new file mode 100644 index 000000000000..b2bf4b9679c4 --- /dev/null +++ b/packages/astro/e2e/fixtures/view-transitions/src/components/css.js @@ -0,0 +1,3 @@ +import "./other.postcss"; +export const indirect = ""; + diff --git a/packages/astro/e2e/fixtures/view-transitions/src/components/other.postcss b/packages/astro/e2e/fixtures/view-transitions/src/components/other.postcss new file mode 100644 index 000000000000..55b21b9202f2 --- /dev/null +++ b/packages/astro/e2e/fixtures/view-transitions/src/components/other.postcss @@ -0,0 +1 @@ +/* not much to see */ diff --git a/packages/astro/e2e/view-transitions.test.js b/packages/astro/e2e/view-transitions.test.js index d2c14aabdabc..2b4d6d6993e5 100644 --- a/packages/astro/e2e/view-transitions.test.js +++ b/packages/astro/e2e/view-transitions.test.js @@ -670,8 +670,8 @@ test.describe('View Transitions', () => { expect(loads.length, 'There should be 2 page loads').toEqual(2); }); - test.skip('client:only styles are retained on transition', async ({ page, astro }) => { - const totalExpectedStyles = 7; + test('client:only styles are retained on transition', async ({ page, astro }) => { + const totalExpectedStyles = 8; // Go to page 1 await page.goto(astro.resolveUrl('/client-only-one')); diff --git a/packages/astro/src/transitions/router.ts b/packages/astro/src/transitions/router.ts index 869ed87af5fa..ef14d29db08a 100644 --- a/packages/astro/src/transitions/router.ts +++ b/packages/astro/src/transitions/router.ts @@ -47,6 +47,8 @@ const announce = () => { }; const PERSIST_ATTR = 'data-astro-transition-persist'; +const VITE_ID = 'data-vite-dev-id'; +const CLIENT_ONLY = '?client-only='; let parser: DOMParser; @@ -202,8 +204,10 @@ async function updateDOM( ) { // Check for a head element that should persist and returns it, // either because it has the data attribute or is a link el. - const persistedHeadElement = (el: HTMLElement): Element | null => { + // Returns null if the element is not part of the new head, undefined if it should be left alone. + const persistedHeadElement = (el: HTMLElement): Element | null | undefined => { const id = el.getAttribute(PERSIST_ATTR); + if (id === '') return undefined; const newEl = id && newDocument.head.querySelector(`[${PERSIST_ATTR}="${id}"]`); if (newEl) { return newEl; @@ -226,7 +230,7 @@ async function updateDOM( // The element that currently has the focus is part of a DOM tree // that will survive the transition to the new document. // Save the element and the cursor position - if (activeElement?.closest('[data-astro-transition-persist]')) { + if (activeElement?.closest(`[${PERSIST_ATTR}]`)) { if ( activeElement instanceof HTMLInputElement || activeElement instanceof HTMLTextAreaElement @@ -290,7 +294,7 @@ async function updateDOM( // from the new document and leave the current node alone if (newEl) { newEl.remove(); - } else { + } else if (newEl === null) { // Otherwise remove the element in the head. It doesn't exist in the new page. el.remove(); } @@ -332,11 +336,9 @@ async function updateDOM( for (const el of newDocument.querySelectorAll('head link[rel=stylesheet]')) { // Do not preload links that are already on the page. if ( - !document.querySelector( - `[${PERSIST_ATTR}="${el.getAttribute( - PERSIST_ATTR - )}"], link[rel=stylesheet][href="${el.getAttribute('href')}"]` - ) + !document.querySelector(` + [${PERSIST_ATTR}="${el.getAttribute(PERSIST_ATTR)}"], + link[rel=stylesheet][href="${el.getAttribute('href')}"]`) ) { const c = document.createElement('link'); c.setAttribute('rel', 'preload'); @@ -404,6 +406,8 @@ async function transition( return; } + if (import.meta.env.DEV) await prepareForClientOnlyComponents(newDocument, toLocation); + if (!popState) { // save the current scroll position before we change the DOM and transition to the new page history.replaceState({ ...history.state, scrollX, scrollY }, ''); @@ -519,3 +523,45 @@ if (inBrowser) { markScriptsExec(); } } + +// Client:only components get their styles when they are hydrated. +// They do not have their stylesheets in the DOM when the page is parsed from the file. +// Persistent client:only components want to keep the styles +// that Vite dynamically inserted into the current page. +// Therefore, we identify these styles and mark them as persistent. +async function prepareForClientOnlyComponents(newDocument: Document, _toLocation: URL) { + const persistentClientOnlyComponents = ` + [${PERSIST_ATTR}] astro-island[client=only][component-url], + astro-island[client=only][component-url][${PERSIST_ATTR}]`; + const newPersistIds = [...newDocument.querySelectorAll(persistentClientOnlyComponents)].map( + (el) => el.closest(`[${PERSIST_ATTR}]`)!.getAttribute(PERSIST_ATTR) + ); + + // For all components that move to the next page: Add a random query parameter to their URL + const urls = new Set(); + for (const component of document.querySelectorAll(persistentClientOnlyComponents)) { + const id = component.closest(`[${PERSIST_ATTR}]`)!.getAttribute(PERSIST_ATTR); + if (newPersistIds.includes(id)) { + const componentURL = component.getAttribute('component-url')!; + const sixRandomChars = Math.random().toString(36).slice(2, 8); + const url = `${componentURL}${CLIENT_ONLY}${sixRandomChars}`; + urls.add(url); + } + } + // Import the URLs with the random query parameter and see which styles are loaded as a side effect. + await Promise.allSettled([...urls].map((url) => import(/* @vite-ignore */ url))); + // This can lead to new style elements in the header with viteDevId=xyz?client-only=... . + // (with empty content). These tell us: keep entries with viteDevId=xyz for the next page. + + // Mark all those viteDevId=xyz styles as persistent + document.head + .querySelectorAll(`[${PERSIST_ATTR}=""]`) + .forEach((el) => el.removeAttribute(PERSIST_ATTR)); + const usedOnNextPage = document.head.querySelectorAll(`style[${VITE_ID}*="${CLIENT_ONLY}"]`); + for (const style of usedOnNextPage) { + const id = style.getAttribute(VITE_ID)?.replace(/\?client-only=.*$/, ''); + document.head.querySelectorAll(`style[${VITE_ID}="${id}"]`).forEach((keep) => { + keep.setAttribute(PERSIST_ATTR, ''); + }); + } +} diff --git a/packages/astro/src/transitions/vite-plugin-transitions.ts b/packages/astro/src/transitions/vite-plugin-transitions.ts index e530fc1e2bae..55e466adef2e 100644 --- a/packages/astro/src/transitions/vite-plugin-transitions.ts +++ b/packages/astro/src/transitions/vite-plugin-transitions.ts @@ -30,5 +30,31 @@ export default function astroTransitions(): vite.Plugin { `; } }, + + // View transitions want to know which styles to preserve for persistent client:only components. + // The client-side router probes component URLs with a random query parameter ?client-only=... + // This parameter is passed on to imports that might (indirectly) contain styles + // When vite finally adds the styles to the page, they can be identified by that query parameter + // All this happens only in DEV mode. + transform(code, id) { + if (process.env.NODE_ENV === 'development') { + const match = id.match(/\?client-only=(.+)$/); + if (match) { + // For CSS files, send a quick response so that vite has something to insert into the page. + // The content is not important here, only the viteDevId resulting from the query. + if (vite.isCSSRequest(id)) { + return '/**/'; + } + // Non-CSS files can contain (indirect) imports of a style file. + // We are only interested in imports with a module identifier ending with a file extension. + // This excludes imports from packages like "svelte", "react/jsx-dev-runtime" or "astro". + return code.replaceAll( + /\bimport\s([^"';\n]*)("|')([^"':@\s]+\.\w+)\2/g, + `import $1$2$3?client-only=${match[1]}$2` + ); + } + } + return null; + }, }; } From 00fcbfb0fc6b1880a370acc001d8f3f0a0a33231 Mon Sep 17 00:00:00 2001 From: Martin Trapp <94928215+martrapp@users.noreply.github.com> Date: Mon, 16 Oct 2023 15:53:22 +0200 Subject: [PATCH 02/15] Persist styles of persistent client:only components during view transitions --- packages/astro/e2e/view-transitions.test.js | 7 ++- .../astro/src/runtime/server/astro-island.ts | 19 +++++- packages/astro/src/transitions/router.ts | 60 ++++++++----------- .../transitions/vite-plugin-transitions.ts | 25 ++++---- 4 files changed, 57 insertions(+), 54 deletions(-) diff --git a/packages/astro/e2e/view-transitions.test.js b/packages/astro/e2e/view-transitions.test.js index 2b4d6d6993e5..c8b484d93e92 100644 --- a/packages/astro/e2e/view-transitions.test.js +++ b/packages/astro/e2e/view-transitions.test.js @@ -241,15 +241,15 @@ test.describe('View Transitions', () => { let p = page.locator('#totwo'); await expect(p, 'should have content').toHaveText('Go to listener two'); // on load a CSS transition is started triggered by a class on the html element - expect(transitions).toEqual(1); - + expect(transitions).toBeLessThanOrEqual(1); + const transitionsBefore = transitions; // go to page 2 await page.click('#totwo'); p = page.locator('#toone'); await expect(p, 'should have content').toHaveText('Go to listener one'); // swap() resets that class, the after-swap listener sets it again. // the temporarily missing class must not trigger page rendering - expect(transitions).toEqual(1); + expect(transitions).toEqual(transitionsBefore); }); test('click hash links does not do navigation', async ({ page, astro }) => { @@ -686,6 +686,7 @@ test.describe('View Transitions', () => { let pageTwo = page.locator('#page-two'); await expect(pageTwo, 'should have content').toHaveText('Page 2'); + await page.waitForTimeout(500); styles = await page.locator('style').all(); expect(styles.length).toEqual(totalExpectedStyles, 'style count has not changed'); }); diff --git a/packages/astro/src/runtime/server/astro-island.ts b/packages/astro/src/runtime/server/astro-island.ts index 58eb7f2e0af0..47d1c7d26204 100644 --- a/packages/astro/src/runtime/server/astro-island.ts +++ b/packages/astro/src/runtime/server/astro-island.ts @@ -103,9 +103,26 @@ declare const Astro: { Astro[directive]!( async () => { const rendererUrl = this.getAttribute('renderer-url'); + let componentUrl = this.getAttribute('component-url')!; + let styleSheetsAsSideEffect; + + const regenerateStyles = this.closest( + '[data-astro-transition-persist][data-astro-regenerate-styles]' + ); + if (directive === 'only' && regenerateStyles) { + // the view transition router sets regenerateStyles in DEV mode only + regenerateStyles.removeAttribute('data-astro-regenerate-styles'); + const sixRandomChars = Math.random().toString(36).slice(2, 8); + styleSheetsAsSideEffect = `${componentUrl}${ + componentUrl.includes('?') ? '&' : '?' + }client-only=${sixRandomChars}`; + await import(componentUrl); + } const [componentModule, { default: hydrator }] = await Promise.all([ - import(this.getAttribute('component-url')!), + import(componentUrl), rendererUrl ? import(rendererUrl) : () => () => {}, + // we are only interested in the side effect of reloading imported styles in DEV mode + styleSheetsAsSideEffect ? import(styleSheetsAsSideEffect) : () => () => {}, ]); const componentExport = this.getAttribute('component-export') || 'default'; if (!componentExport.includes('.')) { diff --git a/packages/astro/src/transitions/router.ts b/packages/astro/src/transitions/router.ts index ef14d29db08a..9ac1426e28e2 100644 --- a/packages/astro/src/transitions/router.ts +++ b/packages/astro/src/transitions/router.ts @@ -48,7 +48,6 @@ const announce = () => { const PERSIST_ATTR = 'data-astro-transition-persist'; const VITE_ID = 'data-vite-dev-id'; -const CLIENT_ONLY = '?client-only='; let parser: DOMParser; @@ -310,16 +309,19 @@ async function updateDOM( // this will reset scroll Position document.body.replaceWith(newDocument.body); + for (const el of oldBody.querySelectorAll(`[${PERSIST_ATTR}]`)) { const id = el.getAttribute(PERSIST_ATTR); const newEl = document.querySelector(`[${PERSIST_ATTR}="${id}"]`); if (newEl) { // The element exists in the new page, replace it with the element // from the old page so that state is preserved. + + // signal the custom component script for astro-islands to reload imported style-sheets + if (import.meta.env.DEV) el.setAttribute('data-astro-regenerate-styles', ''); newEl.replaceWith(el); } } - restoreFocus(savedFocus); if (popState) { @@ -337,7 +339,7 @@ async function updateDOM( // Do not preload links that are already on the page. if ( !document.querySelector(` - [${PERSIST_ATTR}="${el.getAttribute(PERSIST_ATTR)}"], + [${PERSIST_ATTR}="${el.getAttribute(PERSIST_ATTR)}"], link[rel=stylesheet][href="${el.getAttribute('href')}"]`) ) { const c = document.createElement('link'); @@ -524,44 +526,30 @@ if (inBrowser) { } } -// Client:only components get their styles when they are hydrated. -// They do not have their stylesheets in the DOM when the page is parsed from the file. -// Persistent client:only components want to keep the styles -// that Vite dynamically inserted into the current page. -// Therefore, we identify these styles and mark them as persistent. +// For all Vue & Svelte components that move to the next page, mark their embedded styles as persistent. async function prepareForClientOnlyComponents(newDocument: Document, _toLocation: URL) { - const persistentClientOnlyComponents = ` - [${PERSIST_ATTR}] astro-island[client=only][component-url], - astro-island[client=only][component-url][${PERSIST_ATTR}]`; - const newPersistIds = [...newDocument.querySelectorAll(persistentClientOnlyComponents)].map( - (el) => el.closest(`[${PERSIST_ATTR}]`)!.getAttribute(PERSIST_ATTR) - ); + const PERSISTENT_CLIENT_ONLY = ` + [${PERSIST_ATTR}] astro-island[client=only], + astro-island[client=only][${PERSIST_ATTR}]`; - // For all components that move to the next page: Add a random query parameter to their URL - const urls = new Set(); - for (const component of document.querySelectorAll(persistentClientOnlyComponents)) { + // transition:persist values on the next page + const newPersistIds = [...newDocument.querySelectorAll(PERSISTENT_CLIENT_ONLY)].map((el) => + el.closest(`[${PERSIST_ATTR}]`)!.getAttribute(PERSIST_ATTR) + ); + // transition:persist values on the current page + for (const component of document.querySelectorAll(PERSISTENT_CLIENT_ONLY)) { const id = component.closest(`[${PERSIST_ATTR}]`)!.getAttribute(PERSIST_ATTR); + // on both pages if (newPersistIds.includes(id)) { + // Vue or Svelte component? const componentURL = component.getAttribute('component-url')!; - const sixRandomChars = Math.random().toString(36).slice(2, 8); - const url = `${componentURL}${CLIENT_ONLY}${sixRandomChars}`; - urls.add(url); + const match = componentURL.match(/\.(vue|svlete)$/); + if (match) { + // Mark the style sheet of the component as persistent + const selector = `style[${VITE_ID}*='${componentURL}?${match[1]}&type=style&']`; + const styles = document.head.querySelectorAll(selector); + styles.forEach((el) => el.setAttribute(PERSIST_ATTR, '')); + } } } - // Import the URLs with the random query parameter and see which styles are loaded as a side effect. - await Promise.allSettled([...urls].map((url) => import(/* @vite-ignore */ url))); - // This can lead to new style elements in the header with viteDevId=xyz?client-only=... . - // (with empty content). These tell us: keep entries with viteDevId=xyz for the next page. - - // Mark all those viteDevId=xyz styles as persistent - document.head - .querySelectorAll(`[${PERSIST_ATTR}=""]`) - .forEach((el) => el.removeAttribute(PERSIST_ATTR)); - const usedOnNextPage = document.head.querySelectorAll(`style[${VITE_ID}*="${CLIENT_ONLY}"]`); - for (const style of usedOnNextPage) { - const id = style.getAttribute(VITE_ID)?.replace(/\?client-only=.*$/, ''); - document.head.querySelectorAll(`style[${VITE_ID}="${id}"]`).forEach((keep) => { - keep.setAttribute(PERSIST_ATTR, ''); - }); - } } diff --git a/packages/astro/src/transitions/vite-plugin-transitions.ts b/packages/astro/src/transitions/vite-plugin-transitions.ts index 55e466adef2e..bff4e6589daa 100644 --- a/packages/astro/src/transitions/vite-plugin-transitions.ts +++ b/packages/astro/src/transitions/vite-plugin-transitions.ts @@ -31,26 +31,23 @@ export default function astroTransitions(): vite.Plugin { } }, - // View transitions want to know which styles to preserve for persistent client:only components. - // The client-side router probes component URLs with a random query parameter ?client-only=... - // This parameter is passed on to imports that might (indirectly) contain styles - // When vite finally adds the styles to the page, they can be identified by that query parameter - // All this happens only in DEV mode. + // Importing components with a random marker ensures that vite treates them as new + // and finally reinserts style sheets of imported styles in the head of the document. + // This is used by the astro-island custom components to re-establish imported style sheets + // for client:only components in DEV mode. transform(code, id) { if (process.env.NODE_ENV === 'development') { - const match = id.match(/\?client-only=(.+)$/); - if (match) { - // For CSS files, send a quick response so that vite has something to insert into the page. - // The content is not important here, only the viteDevId resulting from the query. - if (vite.isCSSRequest(id)) { - return '/**/'; - } - // Non-CSS files can contain (indirect) imports of a style file. + const hasMarker = id.match(/[?&]client-only=([^&?]+)/); + if (hasMarker) { + const marker = hasMarker[1]; // We are only interested in imports with a module identifier ending with a file extension. // This excludes imports from packages like "svelte", "react/jsx-dev-runtime" or "astro". return code.replaceAll( /\bimport\s([^"';\n]*)("|')([^"':@\s]+\.\w+)\2/g, - `import $1$2$3?client-only=${match[1]}$2` + (_, p1, p2, p3) => { + const delimiter = p3.includes('?') ? '&' : '?'; + return `import ${p1}${p2}${p3}${delimiter}client-only=${marker}${p2}`; + } ); } } From 433d77bf0961f133fa7fb0e7cd40b4ac6396986c Mon Sep 17 00:00:00 2001 From: Martin Trapp <94928215+martrapp@users.noreply.github.com> Date: Mon, 16 Oct 2023 18:16:00 +0200 Subject: [PATCH 03/15] Persist styles of persistent client:only components during view transitions --- packages/astro/src/runtime/server/astro-island.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/astro/src/runtime/server/astro-island.ts b/packages/astro/src/runtime/server/astro-island.ts index 47d1c7d26204..fcc1db828513 100644 --- a/packages/astro/src/runtime/server/astro-island.ts +++ b/packages/astro/src/runtime/server/astro-island.ts @@ -106,6 +106,9 @@ declare const Astro: { let componentUrl = this.getAttribute('component-url')!; let styleSheetsAsSideEffect; + // Persistent client:only components lose their imported styles during view transitions + // Importing the component again will re-import the styles + // A random number in the query string will convince the browser that these are all new files const regenerateStyles = this.closest( '[data-astro-transition-persist][data-astro-regenerate-styles]' ); @@ -116,7 +119,6 @@ declare const Astro: { styleSheetsAsSideEffect = `${componentUrl}${ componentUrl.includes('?') ? '&' : '?' }client-only=${sixRandomChars}`; - await import(componentUrl); } const [componentModule, { default: hydrator }] = await Promise.all([ import(componentUrl), From a9008d4be5197d5253966af5f4b0373312b4a1ce Mon Sep 17 00:00:00 2001 From: Martin Trapp <94928215+martrapp@users.noreply.github.com> Date: Mon, 16 Oct 2023 19:28:51 +0200 Subject: [PATCH 04/15] reset flag for persistent style shhets before re-calculating --- packages/astro/src/transitions/router.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/astro/src/transitions/router.ts b/packages/astro/src/transitions/router.ts index 9ac1426e28e2..3174cdff7596 100644 --- a/packages/astro/src/transitions/router.ts +++ b/packages/astro/src/transitions/router.ts @@ -532,6 +532,10 @@ async function prepareForClientOnlyComponents(newDocument: Document, _toLocation [${PERSIST_ATTR}] astro-island[client=only], astro-island[client=only][${PERSIST_ATTR}]`; + document.head + .querySelectorAll(`[${PERSIST_ATTR}='']`) + .forEach((el) => el.removeAttribute(PERSIST_ATTR)); + // transition:persist values on the next page const newPersistIds = [...newDocument.querySelectorAll(PERSISTENT_CLIENT_ONLY)].map((el) => el.closest(`[${PERSIST_ATTR}]`)!.getAttribute(PERSIST_ATTR) From bb3d74a70e7483e7137a4490d4dd8d154a6f8a11 Mon Sep 17 00:00:00 2001 From: Martin Trapp <94928215+martrapp@users.noreply.github.com> Date: Thu, 19 Oct 2023 00:15:35 +0200 Subject: [PATCH 05/15] new approach with a clear module loader cache --- .../astro/src/runtime/server/astro-island.ts | 23 +------ packages/astro/src/transitions/router.ts | 68 ++++++++++--------- .../transitions/vite-plugin-transitions.ts | 23 ------- 3 files changed, 38 insertions(+), 76 deletions(-) diff --git a/packages/astro/src/runtime/server/astro-island.ts b/packages/astro/src/runtime/server/astro-island.ts index fcc1db828513..3c897fb1e8ef 100644 --- a/packages/astro/src/runtime/server/astro-island.ts +++ b/packages/astro/src/runtime/server/astro-island.ts @@ -103,28 +103,9 @@ declare const Astro: { Astro[directive]!( async () => { const rendererUrl = this.getAttribute('renderer-url'); - let componentUrl = this.getAttribute('component-url')!; - let styleSheetsAsSideEffect; - - // Persistent client:only components lose their imported styles during view transitions - // Importing the component again will re-import the styles - // A random number in the query string will convince the browser that these are all new files - const regenerateStyles = this.closest( - '[data-astro-transition-persist][data-astro-regenerate-styles]' - ); - if (directive === 'only' && regenerateStyles) { - // the view transition router sets regenerateStyles in DEV mode only - regenerateStyles.removeAttribute('data-astro-regenerate-styles'); - const sixRandomChars = Math.random().toString(36).slice(2, 8); - styleSheetsAsSideEffect = `${componentUrl}${ - componentUrl.includes('?') ? '&' : '?' - }client-only=${sixRandomChars}`; - } const [componentModule, { default: hydrator }] = await Promise.all([ - import(componentUrl), + import(this.getAttribute('component-url')!), rendererUrl ? import(rendererUrl) : () => () => {}, - // we are only interested in the side effect of reloading imported styles in DEV mode - styleSheetsAsSideEffect ? import(styleSheetsAsSideEffect) : () => () => {}, ]); const componentExport = this.getAttribute('component-export') || 'default'; if (!componentExport.includes('.')) { @@ -201,7 +182,7 @@ declare const Astro: { client: this.getAttribute('client'), }); this.removeAttribute('ssr'); - this.dispatchEvent(new CustomEvent('astro:hydrate')); + this.dispatchEvent(new CustomEvent('astro:hydrate', { bubbles: true })); }; attributeChangedCallback() { this.hydrate(); diff --git a/packages/astro/src/transitions/router.ts b/packages/astro/src/transitions/router.ts index 3174cdff7596..0527a721a4a7 100644 --- a/packages/astro/src/transitions/router.ts +++ b/packages/astro/src/transitions/router.ts @@ -205,9 +205,14 @@ async function updateDOM( // either because it has the data attribute or is a link el. // Returns null if the element is not part of the new head, undefined if it should be left alone. const persistedHeadElement = (el: HTMLElement): Element | null | undefined => { + const vid = el.getAttribute(VITE_ID); + let newEl = vid && newDocument.head.querySelector(`[${VITE_ID}="${vid}"]`); + if (newEl) { + return newEl; + } const id = el.getAttribute(PERSIST_ATTR); if (id === '') return undefined; - const newEl = id && newDocument.head.querySelector(`[${PERSIST_ATTR}="${id}"]`); + newEl = id && newDocument.head.querySelector(`[${PERSIST_ATTR}="${id}"]`); if (newEl) { return newEl; } @@ -316,9 +321,6 @@ async function updateDOM( if (newEl) { // The element exists in the new page, replace it with the element // from the old page so that state is preserved. - - // signal the custom component script for astro-islands to reload imported style-sheets - if (import.meta.env.DEV) el.setAttribute('data-astro-regenerate-styles', ''); newEl.replaceWith(el); } } @@ -526,34 +528,36 @@ if (inBrowser) { } } -// For all Vue & Svelte components that move to the next page, mark their embedded styles as persistent. -async function prepareForClientOnlyComponents(newDocument: Document, _toLocation: URL) { - const PERSISTENT_CLIENT_ONLY = ` - [${PERSIST_ATTR}] astro-island[client=only], - astro-island[client=only][${PERSIST_ATTR}]`; - - document.head - .querySelectorAll(`[${PERSIST_ATTR}='']`) - .forEach((el) => el.removeAttribute(PERSIST_ATTR)); - - // transition:persist values on the next page - const newPersistIds = [...newDocument.querySelectorAll(PERSISTENT_CLIENT_ONLY)].map((el) => - el.closest(`[${PERSIST_ATTR}]`)!.getAttribute(PERSIST_ATTR) - ); - // transition:persist values on the current page - for (const component of document.querySelectorAll(PERSISTENT_CLIENT_ONLY)) { - const id = component.closest(`[${PERSIST_ATTR}]`)!.getAttribute(PERSIST_ATTR); - // on both pages - if (newPersistIds.includes(id)) { - // Vue or Svelte component? - const componentURL = component.getAttribute('component-url')!; - const match = componentURL.match(/\.(vue|svlete)$/); - if (match) { - // Mark the style sheet of the component as persistent - const selector = `style[${VITE_ID}*='${componentURL}?${match[1]}&type=style&']`; - const styles = document.head.querySelectorAll(selector); - styles.forEach((el) => el.setAttribute(PERSIST_ATTR, '')); - } +// Keep all styles that are potentially created by client:only components +// and required on the next page +async function prepareForClientOnlyComponents(newDocument: Document, toLocation: URL) { + const islands = newDocument.body.querySelectorAll("astro-island[client='only']"); + if (islands) { + // load the next page with an empty module loader cache + const nextPage = document.createElement('iframe'); + nextPage.setAttribute('src', toLocation.href); + nextPage.style.display = 'none'; + document.body.append(nextPage); + let counter = 0; + await new Promise((r) => { + nextPage.contentWindow?.addEventListener('astro:hydrate', () => { + if (++counter === islands.length) r(0); + }); + setInterval(r, 1000); + }); + const nextHead = nextPage.contentDocument?.head; + if (nextHead) { + // collect the vite ids of all styles that we may still need + const viteIds = [...nextHead.querySelectorAll(`style[${VITE_ID}]`)].map((style) => + style.getAttribute(VITE_ID) + ); + // mark those style in the current head as persistent + viteIds.forEach((id) => { + const style = document.head.querySelector(`style[${VITE_ID}="${id}"]`); + if (style) { + style.setAttribute(PERSIST_ATTR, ''); + } + }); } } } diff --git a/packages/astro/src/transitions/vite-plugin-transitions.ts b/packages/astro/src/transitions/vite-plugin-transitions.ts index bff4e6589daa..e530fc1e2bae 100644 --- a/packages/astro/src/transitions/vite-plugin-transitions.ts +++ b/packages/astro/src/transitions/vite-plugin-transitions.ts @@ -30,28 +30,5 @@ export default function astroTransitions(): vite.Plugin { `; } }, - - // Importing components with a random marker ensures that vite treates them as new - // and finally reinserts style sheets of imported styles in the head of the document. - // This is used by the astro-island custom components to re-establish imported style sheets - // for client:only components in DEV mode. - transform(code, id) { - if (process.env.NODE_ENV === 'development') { - const hasMarker = id.match(/[?&]client-only=([^&?]+)/); - if (hasMarker) { - const marker = hasMarker[1]; - // We are only interested in imports with a module identifier ending with a file extension. - // This excludes imports from packages like "svelte", "react/jsx-dev-runtime" or "astro". - return code.replaceAll( - /\bimport\s([^"';\n]*)("|')([^"':@\s]+\.\w+)\2/g, - (_, p1, p2, p3) => { - const delimiter = p3.includes('?') ? '&' : '?'; - return `import ${p1}${p2}${p3}${delimiter}client-only=${marker}${p2}`; - } - ); - } - } - return null; - }, }; } From c01b19c6f6778a96b364686e6038e57868e7dab4 Mon Sep 17 00:00:00 2001 From: Martin Trapp <94928215+martrapp@users.noreply.github.com> Date: Thu, 19 Oct 2023 00:34:11 +0200 Subject: [PATCH 06/15] simplifications --- packages/astro/src/transitions/router.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/packages/astro/src/transitions/router.ts b/packages/astro/src/transitions/router.ts index 0527a721a4a7..320aae171c3c 100644 --- a/packages/astro/src/transitions/router.ts +++ b/packages/astro/src/transitions/router.ts @@ -205,14 +205,9 @@ async function updateDOM( // either because it has the data attribute or is a link el. // Returns null if the element is not part of the new head, undefined if it should be left alone. const persistedHeadElement = (el: HTMLElement): Element | null | undefined => { - const vid = el.getAttribute(VITE_ID); - let newEl = vid && newDocument.head.querySelector(`[${VITE_ID}="${vid}"]`); - if (newEl) { - return newEl; - } const id = el.getAttribute(PERSIST_ATTR); if (id === '') return undefined; - newEl = id && newDocument.head.querySelector(`[${PERSIST_ATTR}="${id}"]`); + const newEl = id && newDocument.head.querySelector(`[${PERSIST_ATTR}="${id}"]`); if (newEl) { return newEl; } @@ -340,9 +335,11 @@ async function updateDOM( for (const el of newDocument.querySelectorAll('head link[rel=stylesheet]')) { // Do not preload links that are already on the page. if ( - !document.querySelector(` - [${PERSIST_ATTR}="${el.getAttribute(PERSIST_ATTR)}"], - link[rel=stylesheet][href="${el.getAttribute('href')}"]`) + !document.querySelector( + `[${PERSIST_ATTR}="${el.getAttribute( + PERSIST_ATTR + )}"], link[rel=stylesheet][href="${el.getAttribute('href')}"]` + ) ) { const c = document.createElement('link'); c.setAttribute('rel', 'preload'); @@ -554,7 +551,7 @@ async function prepareForClientOnlyComponents(newDocument: Document, toLocation: // mark those style in the current head as persistent viteIds.forEach((id) => { const style = document.head.querySelector(`style[${VITE_ID}="${id}"]`); - if (style) { + if (style && !newDocument.head.querySelector(`style[${VITE_ID}="${id}"]`)) { style.setAttribute(PERSIST_ATTR, ''); } }); From fc68c4b5aeb5a4d66dede8587fe3b745a6e8a820 Mon Sep 17 00:00:00 2001 From: Martin Trapp <94928215+martrapp@users.noreply.github.com> Date: Thu, 19 Oct 2023 13:18:58 +0200 Subject: [PATCH 07/15] wait for hydration --- .../astro/src/runtime/server/astro-island.ts | 2 +- packages/astro/src/transitions/router.ts | 40 +++++++++++++------ 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/packages/astro/src/runtime/server/astro-island.ts b/packages/astro/src/runtime/server/astro-island.ts index 3c897fb1e8ef..58eb7f2e0af0 100644 --- a/packages/astro/src/runtime/server/astro-island.ts +++ b/packages/astro/src/runtime/server/astro-island.ts @@ -182,7 +182,7 @@ declare const Astro: { client: this.getAttribute('client'), }); this.removeAttribute('ssr'); - this.dispatchEvent(new CustomEvent('astro:hydrate', { bubbles: true })); + this.dispatchEvent(new CustomEvent('astro:hydrate')); }; attributeChangedCallback() { this.hydrate(); diff --git a/packages/astro/src/transitions/router.ts b/packages/astro/src/transitions/router.ts index 320aae171c3c..6559c64cf32f 100644 --- a/packages/astro/src/transitions/router.ts +++ b/packages/astro/src/transitions/router.ts @@ -527,28 +527,30 @@ if (inBrowser) { // Keep all styles that are potentially created by client:only components // and required on the next page +//eslint-disable-next-line @typescript-eslint/no-unused-vars async function prepareForClientOnlyComponents(newDocument: Document, toLocation: URL) { - const islands = newDocument.body.querySelectorAll("astro-island[client='only']"); - if (islands) { - // load the next page with an empty module loader cache + if ( + // Any persisted client:only component on the next page? + newDocument.body.querySelector( + `[${PERSIST_ATTR}] astro-island[client='only'], + astro-island[client='only'][${PERSIST_ATTR}]` + ) + ) { + // Load the next page with an empty module loader cache const nextPage = document.createElement('iframe'); nextPage.setAttribute('src', toLocation.href); nextPage.style.display = 'none'; document.body.append(nextPage); - let counter = 0; - await new Promise((r) => { - nextPage.contentWindow?.addEventListener('astro:hydrate', () => { - if (++counter === islands.length) r(0); - }); - setInterval(r, 1000); - }); + await hydrationDone(nextPage); + const nextHead = nextPage.contentDocument?.head; if (nextHead) { - // collect the vite ids of all styles that we may still need + // Collect the vite ids of all styles present in the next head const viteIds = [...nextHead.querySelectorAll(`style[${VITE_ID}]`)].map((style) => style.getAttribute(VITE_ID) ); - // mark those style in the current head as persistent + // Mark those styles as persistent in the current head, + // if they came from hydration and not from the newDocument viteIds.forEach((id) => { const style = document.head.querySelector(`style[${VITE_ID}="${id}"]`); if (style && !newDocument.head.querySelector(`style[${VITE_ID}="${id}"]`)) { @@ -556,5 +558,19 @@ async function prepareForClientOnlyComponents(newDocument: Document, toLocation: } }); } + + // return a promise that resolves when all astro-islands are hydrated + async function hydrationDone(loadingPage: HTMLIFrameElement) { + await new Promise( + (r) => loadingPage.contentWindow?.addEventListener('load', r, { once: true }) + ); + return new Promise(async (r) => { + for (let count = 0; count <= 20; ++count) { + if (!loadingPage.contentDocument!.body.querySelector('astro-island[ssr]')) break; + await new Promise((r2) => setTimeout(r2, 50)); + } + r(); + }); + } } } From dab54f32e037650031d1329e141e9d61eda1fd3c Mon Sep 17 00:00:00 2001 From: Martin Trapp <94928215+martrapp@users.noreply.github.com> Date: Thu, 19 Oct 2023 14:03:08 +0200 Subject: [PATCH 08/15] improve changeset message --- .changeset/purple-dots-refuse.md | 2 +- packages/astro/e2e/view-transitions.test.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.changeset/purple-dots-refuse.md b/.changeset/purple-dots-refuse.md index 01c0bc14c497..40a3333bd97c 100644 --- a/.changeset/purple-dots-refuse.md +++ b/.changeset/purple-dots-refuse.md @@ -2,4 +2,4 @@ 'astro': patch --- -Persist styles of persistent client:only components during view transitions +Fixes styles of client:only components not persisting during view transitions diff --git a/packages/astro/e2e/view-transitions.test.js b/packages/astro/e2e/view-transitions.test.js index c8b484d93e92..d2fcd1f7334d 100644 --- a/packages/astro/e2e/view-transitions.test.js +++ b/packages/astro/e2e/view-transitions.test.js @@ -686,7 +686,6 @@ test.describe('View Transitions', () => { let pageTwo = page.locator('#page-two'); await expect(pageTwo, 'should have content').toHaveText('Page 2'); - await page.waitForTimeout(500); styles = await page.locator('style').all(); expect(styles.length).toEqual(totalExpectedStyles, 'style count has not changed'); }); From fbea4b684246b45f4603ce000126ba3c387f9474 Mon Sep 17 00:00:00 2001 From: Martin Trapp <94928215+martrapp@users.noreply.github.com> Date: Thu, 19 Oct 2023 14:03:53 +0200 Subject: [PATCH 09/15] improve changeset message --- .changeset/purple-dots-refuse.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/purple-dots-refuse.md b/.changeset/purple-dots-refuse.md index 40a3333bd97c..99bd71e64025 100644 --- a/.changeset/purple-dots-refuse.md +++ b/.changeset/purple-dots-refuse.md @@ -2,4 +2,4 @@ 'astro': patch --- -Fixes styles of client:only components not persisting during view transitions +Fixes styles of `client:only` components not persisting during view transitions From a3910abec845d89a1fd5903d0df2f418c056759b Mon Sep 17 00:00:00 2001 From: Martin Trapp <94928215+martrapp@users.noreply.github.com> Date: Thu, 19 Oct 2023 14:18:39 +0200 Subject: [PATCH 10/15] please the linter --- packages/astro/src/transitions/router.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/astro/src/transitions/router.ts b/packages/astro/src/transitions/router.ts index 6559c64cf32f..f236a41be59c 100644 --- a/packages/astro/src/transitions/router.ts +++ b/packages/astro/src/transitions/router.ts @@ -443,6 +443,7 @@ export function navigate(href: string, options?: Options) { 'The view transtions client API was called during a server side render. This may be unintentional as the navigate() function is expected to be called in response to user interactions. Please make sure that your usage is correct.' ); warning.name = 'Warning'; + // eslint-disable-next-line no-console console.warn(warning); navigateOnServerWarned = true; } @@ -527,7 +528,6 @@ if (inBrowser) { // Keep all styles that are potentially created by client:only components // and required on the next page -//eslint-disable-next-line @typescript-eslint/no-unused-vars async function prepareForClientOnlyComponents(newDocument: Document, toLocation: URL) { if ( // Any persisted client:only component on the next page? From 5dd638fe5d1505513c830a77a6c3eed379dea3ae Mon Sep 17 00:00:00 2001 From: Martin Trapp <94928215+martrapp@users.noreply.github.com> Date: Thu, 19 Oct 2023 15:59:55 +0200 Subject: [PATCH 11/15] additional tests for Svelte and Vue --- .../view-transitions/astro.config.mjs | 4 +- .../fixtures/view-transitions/package.json | 4 ++ .../src/components/Island.css | 2 + .../src/components/SvelteCounter.svelte | 36 ++++++++++++++++ .../src/components/VueCounter.vue | 43 +++++++++++++++++++ .../src/pages/client-only-four.astro | 15 +++++++ .../src/pages/client-only-one.astro | 2 +- .../src/pages/client-only-three.astro | 19 ++++++++ .../src/pages/client-only-two.astro | 2 +- packages/astro/e2e/view-transitions.test.js | 24 ++++++++++- packages/astro/src/transitions/router.ts | 6 ++- pnpm-lock.yaml | 12 ++++++ 12 files changed, 163 insertions(+), 6 deletions(-) create mode 100644 packages/astro/e2e/fixtures/view-transitions/src/components/SvelteCounter.svelte create mode 100644 packages/astro/e2e/fixtures/view-transitions/src/components/VueCounter.vue create mode 100644 packages/astro/e2e/fixtures/view-transitions/src/pages/client-only-four.astro create mode 100644 packages/astro/e2e/fixtures/view-transitions/src/pages/client-only-three.astro diff --git a/packages/astro/e2e/fixtures/view-transitions/astro.config.mjs b/packages/astro/e2e/fixtures/view-transitions/astro.config.mjs index 2b22ff9cf3f3..f4450f67285d 100644 --- a/packages/astro/e2e/fixtures/view-transitions/astro.config.mjs +++ b/packages/astro/e2e/fixtures/view-transitions/astro.config.mjs @@ -1,12 +1,14 @@ import { defineConfig } from 'astro/config'; import react from '@astrojs/react'; +import vue from '@astrojs/vue'; +import svelte from '@astrojs/svelte'; import nodejs from '@astrojs/node'; // https://astro.build/config export default defineConfig({ output: 'hybrid', adapter: nodejs({ mode: 'standalone' }), - integrations: [react()], + integrations: [react(),vue(),svelte()], redirects: { '/redirect-two': '/two', '/redirect-external': 'http://example.com/', diff --git a/packages/astro/e2e/fixtures/view-transitions/package.json b/packages/astro/e2e/fixtures/view-transitions/package.json index f4ba9b17b053..b53b5fcad4a6 100644 --- a/packages/astro/e2e/fixtures/view-transitions/package.json +++ b/packages/astro/e2e/fixtures/view-transitions/package.json @@ -6,6 +6,10 @@ "astro": "workspace:*", "@astrojs/node": "workspace:*", "@astrojs/react": "workspace:*", + "@astrojs/vue": "workspace:*", + "@astrojs/svelte": "workspace:*", + "svelte": "^4.2.0", + "vue": "^3.3.4", "react": "^18.1.0", "react-dom": "^18.1.0" } diff --git a/packages/astro/e2e/fixtures/view-transitions/src/components/Island.css b/packages/astro/e2e/fixtures/view-transitions/src/components/Island.css index fb21044d78cc..28c5642a9897 100644 --- a/packages/astro/e2e/fixtures/view-transitions/src/components/Island.css +++ b/packages/astro/e2e/fixtures/view-transitions/src/components/Island.css @@ -8,4 +8,6 @@ .counter-message { text-align: center; + background-color: lightskyblue; + color:black } diff --git a/packages/astro/e2e/fixtures/view-transitions/src/components/SvelteCounter.svelte b/packages/astro/e2e/fixtures/view-transitions/src/components/SvelteCounter.svelte new file mode 100644 index 000000000000..6647a19ce7d0 --- /dev/null +++ b/packages/astro/e2e/fixtures/view-transitions/src/components/SvelteCounter.svelte @@ -0,0 +1,36 @@ + + +
+ +
{count}
+ +
+
+ +
+ + diff --git a/packages/astro/e2e/fixtures/view-transitions/src/components/VueCounter.vue b/packages/astro/e2e/fixtures/view-transitions/src/components/VueCounter.vue new file mode 100644 index 000000000000..e75620aff455 --- /dev/null +++ b/packages/astro/e2e/fixtures/view-transitions/src/components/VueCounter.vue @@ -0,0 +1,43 @@ + + + + + diff --git a/packages/astro/e2e/fixtures/view-transitions/src/pages/client-only-four.astro b/packages/astro/e2e/fixtures/view-transitions/src/pages/client-only-four.astro new file mode 100644 index 000000000000..7e33cbedbf90 --- /dev/null +++ b/packages/astro/e2e/fixtures/view-transitions/src/pages/client-only-four.astro @@ -0,0 +1,15 @@ +--- +import Layout from '../components/Layout.astro'; +import Island from '../components/Island'; +import VueCounter from '../components/VueCounter.vue'; +import SvelteCounter from '../components/SvelteCounter.svelte'; +--- + +

Page 4

+
+ Vue +
+
+ Svelte +
+
diff --git a/packages/astro/e2e/fixtures/view-transitions/src/pages/client-only-one.astro b/packages/astro/e2e/fixtures/view-transitions/src/pages/client-only-one.astro index a8d5e8995ae4..a51ccc299b2a 100644 --- a/packages/astro/e2e/fixtures/view-transitions/src/pages/client-only-one.astro +++ b/packages/astro/e2e/fixtures/view-transitions/src/pages/client-only-one.astro @@ -5,6 +5,6 @@ import Island from '../components/Island'; go to page 2
- message here + message here
diff --git a/packages/astro/e2e/fixtures/view-transitions/src/pages/client-only-three.astro b/packages/astro/e2e/fixtures/view-transitions/src/pages/client-only-three.astro new file mode 100644 index 000000000000..26093655f242 --- /dev/null +++ b/packages/astro/e2e/fixtures/view-transitions/src/pages/client-only-three.astro @@ -0,0 +1,19 @@ +--- +import Layout from '../components/Layout.astro'; +import Island from '../components/Island'; +import VueCounter from '../components/VueCounter.vue'; +import SvelteCounter from '../components/SvelteCounter.svelte'; +--- + + go to page 4 +
+ + message here +
+
+ Vue +
+
+ Svelte +
+
diff --git a/packages/astro/e2e/fixtures/view-transitions/src/pages/client-only-two.astro b/packages/astro/e2e/fixtures/view-transitions/src/pages/client-only-two.astro index 884ec46833d5..4190d86efb45 100644 --- a/packages/astro/e2e/fixtures/view-transitions/src/pages/client-only-two.astro +++ b/packages/astro/e2e/fixtures/view-transitions/src/pages/client-only-two.astro @@ -5,6 +5,6 @@ import Island from '../components/Island';

Page 2

- message here + message here
diff --git a/packages/astro/e2e/view-transitions.test.js b/packages/astro/e2e/view-transitions.test.js index d2fcd1f7334d..2f1d369f86ce 100644 --- a/packages/astro/e2e/view-transitions.test.js +++ b/packages/astro/e2e/view-transitions.test.js @@ -670,10 +670,9 @@ test.describe('View Transitions', () => { expect(loads.length, 'There should be 2 page loads').toEqual(2); }); - test('client:only styles are retained on transition', async ({ page, astro }) => { + test('client:only styles are retained on transition (1/2)', async ({ page, astro }) => { const totalExpectedStyles = 8; - // Go to page 1 await page.goto(astro.resolveUrl('/client-only-one')); let msg = page.locator('.counter-message'); await expect(msg).toHaveText('message here'); @@ -690,6 +689,27 @@ test.describe('View Transitions', () => { expect(styles.length).toEqual(totalExpectedStyles, 'style count has not changed'); }); + test('client:only styles are retained on transition (2/2)', async ({ page, astro }) => { + const totalExpectedStyles_page_three = 10; + const totalExpectedStyles_page_four = 7; + + await page.goto(astro.resolveUrl('/client-only-three')); + let msg = page.locator('.counter-message'); + await expect(msg).toHaveText('message here'); + await page.waitForTimeout(400); // await hydration + + let styles = await page.locator('style').all(); + expect(styles.length).toEqual(totalExpectedStyles_page_three); + + await page.click('#click-four'); + + let pageTwo = page.locator('#page-four'); + await expect(pageTwo, 'should have content').toHaveText('Page 4'); + + styles = await page.locator('style').all(); + expect(styles.length).toEqual(totalExpectedStyles_page_four, 'style count has not changed'); + }); + test('Horizontal scroll position restored on back button', async ({ page, astro }) => { await page.goto(astro.resolveUrl('/wide-page')); let article = page.locator('#widepage'); diff --git a/packages/astro/src/transitions/router.ts b/packages/astro/src/transitions/router.ts index f236a41be59c..883e8f737f6d 100644 --- a/packages/astro/src/transitions/router.ts +++ b/packages/astro/src/transitions/router.ts @@ -532,7 +532,7 @@ async function prepareForClientOnlyComponents(newDocument: Document, toLocation: if ( // Any persisted client:only component on the next page? newDocument.body.querySelector( - `[${PERSIST_ATTR}] astro-island[client='only'], + `[${PERSIST_ATTR}] astro-island[client='only'], astro-island[client='only'][${PERSIST_ATTR}]` ) ) { @@ -549,6 +549,10 @@ async function prepareForClientOnlyComponents(newDocument: Document, toLocation: const viteIds = [...nextHead.querySelectorAll(`style[${VITE_ID}]`)].map((style) => style.getAttribute(VITE_ID) ); + // Clear former persist marks + document.head + .querySelectorAll(`style[${PERSIST_ATTR}=""]`) + .forEach((s) => s.removeAttribute(PERSIST_ATTR)); // Mark those styles as persistent in the current head, // if they came from hydration and not from the newDocument viteIds.forEach((id) => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0e4fed80ad16..685a71997db2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1490,6 +1490,12 @@ importers: '@astrojs/react': specifier: workspace:* version: link:../../../../integrations/react + '@astrojs/svelte': + specifier: workspace:* + version: link:../../../../integrations/svelte + '@astrojs/vue': + specifier: workspace:* + version: link:../../../../integrations/vue astro: specifier: workspace:* version: link:../../.. @@ -1499,6 +1505,12 @@ importers: react-dom: specifier: ^18.1.0 version: 18.2.0(react@18.2.0) + svelte: + specifier: ^4.2.0 + version: 4.2.0 + vue: + specifier: ^3.3.4 + version: 3.3.4 packages/astro/e2e/fixtures/vue-component: dependencies: From cfb031ae9f0bc0ee405e5db91ee58ad7caf843ba Mon Sep 17 00:00:00 2001 From: Martin Trapp <94928215+martrapp@users.noreply.github.com> Date: Thu, 19 Oct 2023 16:20:39 +0200 Subject: [PATCH 12/15] tidy up --- packages/astro/src/transitions/router.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/astro/src/transitions/router.ts b/packages/astro/src/transitions/router.ts index 883e8f737f6d..b316f3fd7050 100644 --- a/packages/astro/src/transitions/router.ts +++ b/packages/astro/src/transitions/router.ts @@ -545,16 +545,17 @@ async function prepareForClientOnlyComponents(newDocument: Document, toLocation: const nextHead = nextPage.contentDocument?.head; if (nextHead) { - // Collect the vite ids of all styles present in the next head - const viteIds = [...nextHead.querySelectorAll(`style[${VITE_ID}]`)].map((style) => - style.getAttribute(VITE_ID) - ); // Clear former persist marks document.head .querySelectorAll(`style[${PERSIST_ATTR}=""]`) .forEach((s) => s.removeAttribute(PERSIST_ATTR)); - // Mark those styles as persistent in the current head, - // if they came from hydration and not from the newDocument + + // Collect the vite ids of all styles present in the next head + const viteIds = [...nextHead.querySelectorAll(`style[${VITE_ID}]`)].map((style) => + style.getAttribute(VITE_ID) + ); + // Mark styles of the current head as persistent + // if they come from hydration and not from the newDocument viteIds.forEach((id) => { const style = document.head.querySelector(`style[${VITE_ID}="${id}"]`); if (style && !newDocument.head.querySelector(`style[${VITE_ID}="${id}"]`)) { From 02844d9e7db73731cf7e8b1757b1f79bdfc1f084 Mon Sep 17 00:00:00 2001 From: Martin Trapp <94928215+martrapp@users.noreply.github.com> Date: Thu, 19 Oct 2023 16:39:19 +0200 Subject: [PATCH 13/15] test fixed --- .../view-transitions/src/pages/client-only-three.astro | 1 + packages/astro/e2e/view-transitions.test.js | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/astro/e2e/fixtures/view-transitions/src/pages/client-only-three.astro b/packages/astro/e2e/fixtures/view-transitions/src/pages/client-only-three.astro index 26093655f242..a2af0b9d6eb9 100644 --- a/packages/astro/e2e/fixtures/view-transitions/src/pages/client-only-three.astro +++ b/packages/astro/e2e/fixtures/view-transitions/src/pages/client-only-three.astro @@ -16,4 +16,5 @@ import SvelteCounter from '../components/SvelteCounter.svelte';
Svelte
+

client-only-three

diff --git a/packages/astro/e2e/view-transitions.test.js b/packages/astro/e2e/view-transitions.test.js index 2f1d369f86ce..4a6f6a123ac5 100644 --- a/packages/astro/e2e/view-transitions.test.js +++ b/packages/astro/e2e/view-transitions.test.js @@ -694,8 +694,8 @@ test.describe('View Transitions', () => { const totalExpectedStyles_page_four = 7; await page.goto(astro.resolveUrl('/client-only-three')); - let msg = page.locator('.counter-message'); - await expect(msg).toHaveText('message here'); + let msg = page.locator('#name'); + await expect(msg).toHaveText('client-only-three'); await page.waitForTimeout(400); // await hydration let styles = await page.locator('style').all(); From c0ba3afb916aceeb36562fa624ad60b38a01ce7d Mon Sep 17 00:00:00 2001 From: Martin Trapp <94928215+martrapp@users.noreply.github.com> Date: Thu, 19 Oct 2023 17:25:02 +0200 Subject: [PATCH 14/15] test w/o persistence --- .../view-transitions/src/pages/client-only-four.astro | 4 ---- .../view-transitions/src/pages/client-only-three.astro | 4 ---- packages/astro/e2e/view-transitions.test.js | 2 +- packages/astro/src/transitions/router.ts | 10 +++------- 4 files changed, 4 insertions(+), 16 deletions(-) diff --git a/packages/astro/e2e/fixtures/view-transitions/src/pages/client-only-four.astro b/packages/astro/e2e/fixtures/view-transitions/src/pages/client-only-four.astro index 7e33cbedbf90..9ebfa65f04e8 100644 --- a/packages/astro/e2e/fixtures/view-transitions/src/pages/client-only-four.astro +++ b/packages/astro/e2e/fixtures/view-transitions/src/pages/client-only-four.astro @@ -6,10 +6,6 @@ import SvelteCounter from '../components/SvelteCounter.svelte'; ---

Page 4

-
Vue -
-
Svelte -
diff --git a/packages/astro/e2e/fixtures/view-transitions/src/pages/client-only-three.astro b/packages/astro/e2e/fixtures/view-transitions/src/pages/client-only-three.astro index a2af0b9d6eb9..34fa6992699b 100644 --- a/packages/astro/e2e/fixtures/view-transitions/src/pages/client-only-three.astro +++ b/packages/astro/e2e/fixtures/view-transitions/src/pages/client-only-three.astro @@ -10,11 +10,7 @@ import SvelteCounter from '../components/SvelteCounter.svelte'; message here -
Vue -
-
Svelte -

client-only-three

diff --git a/packages/astro/e2e/view-transitions.test.js b/packages/astro/e2e/view-transitions.test.js index 4a6f6a123ac5..a378df7c530c 100644 --- a/packages/astro/e2e/view-transitions.test.js +++ b/packages/astro/e2e/view-transitions.test.js @@ -691,7 +691,7 @@ test.describe('View Transitions', () => { test('client:only styles are retained on transition (2/2)', async ({ page, astro }) => { const totalExpectedStyles_page_three = 10; - const totalExpectedStyles_page_four = 7; + const totalExpectedStyles_page_four = 8; await page.goto(astro.resolveUrl('/client-only-three')); let msg = page.locator('#name'); diff --git a/packages/astro/src/transitions/router.ts b/packages/astro/src/transitions/router.ts index b316f3fd7050..1bbbc85a138d 100644 --- a/packages/astro/src/transitions/router.ts +++ b/packages/astro/src/transitions/router.ts @@ -529,13 +529,8 @@ if (inBrowser) { // Keep all styles that are potentially created by client:only components // and required on the next page async function prepareForClientOnlyComponents(newDocument: Document, toLocation: URL) { - if ( - // Any persisted client:only component on the next page? - newDocument.body.querySelector( - `[${PERSIST_ATTR}] astro-island[client='only'], - astro-island[client='only'][${PERSIST_ATTR}]` - ) - ) { + // Any client:only component on the next page? + if (newDocument.body.querySelector(`astro-island[client='only']`)) { // Load the next page with an empty module loader cache const nextPage = document.createElement('iframe'); nextPage.setAttribute('src', toLocation.href); @@ -569,6 +564,7 @@ async function prepareForClientOnlyComponents(newDocument: Document, toLocation: await new Promise( (r) => loadingPage.contentWindow?.addEventListener('load', r, { once: true }) ); + return new Promise(async (r) => { for (let count = 0; count <= 20; ++count) { if (!loadingPage.contentDocument!.body.querySelector('astro-island[ssr]')) break; From 671b5adff4028f47abd2df58f64f4bc46b355155 Mon Sep 17 00:00:00 2001 From: Martin Trapp <94928215+martrapp@users.noreply.github.com> Date: Thu, 19 Oct 2023 18:08:31 +0200 Subject: [PATCH 15/15] Update .changeset/purple-dots-refuse.md Co-authored-by: Sarah Rainsberger --- .changeset/purple-dots-refuse.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/purple-dots-refuse.md b/.changeset/purple-dots-refuse.md index 99bd71e64025..74be758b57f9 100644 --- a/.changeset/purple-dots-refuse.md +++ b/.changeset/purple-dots-refuse.md @@ -2,4 +2,4 @@ 'astro': patch --- -Fixes styles of `client:only` components not persisting during view transitions +Fixes styles of `client:only` components not persisting during view transitions in dev mode