Skip to content

Commit

Permalink
Merge branch 'main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
mayank99 authored Aug 2, 2023
2 parents d99d270 + 6806629 commit cf80ebf
Show file tree
Hide file tree
Showing 43 changed files with 495 additions and 62 deletions.
27 changes: 27 additions & 0 deletions .changeset/empty-experts-unite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
'astro': minor
---

Persistent DOM and Islands in Experimental View Transitions

With `viewTransitions: true` enabled in your Astro config's experimental section, pages using the `<ViewTransition />` routing component can now access a new `transition:persist` directive.

With this directive, you can keep the state of DOM elements and islands on the old page when transitioning to the new page.

For example, to keep a video playing across page navigation, add `transition:persist` to the element:

```astro
<video controls="" autoplay="" transition:persist>
<source src="https://ia804502.us.archive.org/33/items/GoldenGa1939_3/GoldenGa1939_3_512kb.mp4" type="video/mp4">
</video>
```

This `<video>` element, with its current state, will be moved over to the next page (if the video also exists on that page).

Likewise, this feature works with any client-side framework component island. In this example, a counter's state is preserved and moved to the new page:

```astro
<Counter count={5} client:load transition:persist />
```

See our [View Transitions Guide](https://docs.astro.build/en/guides/view-transitions/#maintaining-state) to learn more on usage.
5 changes: 5 additions & 0 deletions .changeset/four-ways-tap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Fixes an issue that prevents importing `'astro/app'`
5 changes: 5 additions & 0 deletions .changeset/olive-knives-rush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/node': patch
---

fix issuse #7590 "res.writeHead is not a function" in Express/Node middleware
5 changes: 5 additions & 0 deletions .changeset/tidy-tips-doubt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/vercel': minor
---

Add cache headers to assets in Vercel adapter
6 changes: 6 additions & 0 deletions .changeset/wise-glasses-hang.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@astrojs/image': patch
'astro': patch
---

Improve sourcemap generation and performance
5 changes: 5 additions & 0 deletions .changeset/witty-bikes-relate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

fix for #7882 by setting state in page navigation (view transitions)
77 changes: 65 additions & 12 deletions packages/astro/components/ViewTransitions.astro
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const { fallback = 'animate' } = Astro.props as Props;
!!document.querySelector('[name="astro-view-transitions-enabled"]');
const triggerEvent = (name: Events) => document.dispatchEvent(new Event(name));
const onload = () => triggerEvent('astro:load');
const PERSIST_ATTR = 'data-astro-transition-persist';

const throttle = (cb: (...args: any[]) => any, delay: number) => {
let wait = false;
Expand Down Expand Up @@ -86,8 +87,50 @@ const { fallback = 'animate' } = Astro.props as Props;
async function updateDOM(dir: Direction, html: string, state?: State, fallback?: Fallback) {
const doc = parser.parseFromString(html, 'text/html');
doc.documentElement.dataset.astroTransition = dir;

// Check for a head element that should persist, either because it has the data
// attribute or is a link el.
const persistedHeadElement = (el: Element): Element | null => {
const id = el.getAttribute(PERSIST_ATTR);
const newEl = id && doc.head.querySelector(`[${PERSIST_ATTR}="${id}"]`);
if (newEl) {
return newEl;
}
if (el.matches('link[rel=stylesheet]')) {
const href = el.getAttribute('href');
return doc.head.querySelector(`link[rel=stylesheet][href="${href}"]`);
}
return null;
};

const swap = () => {
document.documentElement.replaceWith(doc.documentElement);
// Swap head
for (const el of Array.from(document.head.children)) {
const newEl = persistedHeadElement(el);
// If the element exists in the document already, remove it
// from the new document and leave the current node alone
if (newEl) {
newEl.remove();
} else {
// Otherwise remove the element in the head. It doesn't exist in the new page.
el.remove();
}
}
// Everything left in the new head is new, append it all.
document.head.append(...doc.head.children);

// Move over persist stuff in the body
const oldBody = document.body;
document.body.replaceWith(doc.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.
newEl.replaceWith(el);
}
}

if (state?.scrollY != null) {
scrollTo(0, state.scrollY);
Expand All @@ -97,17 +140,27 @@ const { fallback = 'animate' } = Astro.props as Props;
};

// Wait on links to finish, to prevent FOUC
const links = Array.from(doc.querySelectorAll('head link[rel=stylesheet]')).map(
(link) =>
new Promise((resolve) => {
const c = link.cloneNode();
['load', 'error'].forEach((evName) => c.addEventListener(evName, resolve));
document.head.append(c);
})
);
if (links.length) {
await Promise.all(links);
const links: Promise<any>[] = [];
for (const el of doc.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]`
)
) {
const c = document.createElement('link');
c.setAttribute('rel', 'preload');
c.setAttribute('as', 'style');
c.setAttribute('href', el.getAttribute('href')!);
links.push(
new Promise<any>((resolve) => {
['load', 'error'].forEach((evName) => c.addEventListener(evName, resolve));
document.head.append(c);
})
);
}
}
links.length && (await Promise.all(links));

if (fallback === 'animate') {
let isAnimating = false;
Expand Down Expand Up @@ -187,7 +240,7 @@ const { fallback = 'animate' } = Astro.props as Props;
transitionEnabledOnThisPage()
) {
ev.preventDefault();
navigate('forward', link.href);
navigate('forward', link.href, { index: currentHistoryIndex, scrollY: 0 });
currentHistoryIndex++;
const newState: State = { index: currentHistoryIndex, scrollY };
persistState({ index: currentHistoryIndex - 1, scrollY });
Expand Down
7 changes: 7 additions & 0 deletions packages/astro/e2e/fixtures/view-transitions/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';

// https://astro.build/config
export default defineConfig({
integrations: [react()],
experimental: {
viewTransitions: true,
assets: true,
},
vite: {
build: {
assetsInlineLimit: 0,
},
},
});
5 changes: 4 additions & 1 deletion packages/astro/e2e/fixtures/view-transitions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*"
"astro": "workspace:*",
"@astrojs/react": "workspace:*",
"react": "^18.1.0",
"react-dom": "^18.1.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.counter {
display: grid;
font-size: 2em;
grid-template-columns: repeat(3, minmax(0, 1fr));
margin-top: 2em;
place-items: center;
}

.counter-message {
text-align: center;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React, { useState } from 'react';
import './Island.css';

export default function Counter({ children, count: initialCount, id }) {
const [count, setCount] = useState(initialCount);
const add = () => setCount((i) => i + 1);
const subtract = () => setCount((i) => i - 1);

return (
<>
<div id={id} className="counter">
<button className="decrement" onClick={subtract}>-</button>
<pre>{count}</pre>
<button className="increment" onClick={add}>+</button>
</div>
<div className="counter-message">{children}</div>
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<video controls="" autoplay="" name="media" transition:persist transition:name="video">
<source src="https://ia804502.us.archive.org/33/items/GoldenGa1939_3/GoldenGa1939_3_512kb.mp4" type="video/mp4">
</video>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
import Layout from '../components/Layout.astro';
import Island from '../components/Island.jsx';
---
<Layout>
<p id="island-one">Page 1</p>
<a id="click-two" href="/island-two">go to 2</a>
<Island count={5} client:load transition:persist transition:name="counter" />
</Layout>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
import Layout from '../components/Layout.astro';
import Island from '../components/Island.jsx';
---
<Layout>
<p id="island-two">Page 2</p>
<a id="click-one" href="/island-one">go to 1</a>
<Island count={2} client:load transition:persist transition:name="counter" />
</Layout>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
import Layout from '../components/Layout.astro';
import Video from '../components/Video.astro';
---
<Layout>
<p id="video-one">Page 1</p>
<a id="click-two" href="/video-two">go to 2</a>
<Video />
<script>
const vid = document.querySelector('video');
vid.addEventListener('canplay', () => {
// Jump to the 1 minute mark
vid.currentTime = 60;
vid.dataset.ready = '';
}, { once: true });
</script>
</Layout>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
import Layout from '../components/Layout.astro';
import Video from '../components/Video.astro';
---
<style>
#video-two {
color: blue;
}
</style>
<Layout>
<p id="video-two">Page 2</p>
<a id="click-one" href="/video-one">go to 1</a>
<Video />
</Layout>
36 changes: 36 additions & 0 deletions packages/astro/e2e/view-transitions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -243,4 +243,40 @@ test.describe('View Transitions', () => {
const img = page.locator('img[data-astro-transition-scope]');
await expect(img).toBeVisible('The image tag should have the transition scope attribute.');
});

test('<video> can persist using transition:persist', async ({ page, astro }) => {
const getTime = () => document.querySelector('video').currentTime;

// Go to page 1
await page.goto(astro.resolveUrl('/video-one'));
const vid = page.locator('video[data-ready]');
await expect(vid).toBeVisible();
const firstTime = await page.evaluate(getTime);

// Navigate to page 2
await page.click('#click-two');
const p = page.locator('#video-two');
await expect(p).toBeVisible();
const secondTime = await page.evaluate(getTime);

expect(secondTime).toBeGreaterThanOrEqual(firstTime);
});

test('Islands can persist using transition:persist', async ({ page, astro }) => {
// Go to page 1
await page.goto(astro.resolveUrl('/island-one'));
let cnt = page.locator('.counter pre');
await expect(cnt).toHaveText('5');

await page.click('.increment');
await expect(cnt).toHaveText('6');

// Navigate to page 2
await page.click('#click-two');
const p = page.locator('#island-two');
await expect(p).toBeVisible();
cnt = page.locator('.counter pre');
// Count should remain
await expect(cnt).toHaveText('6');
});
});
4 changes: 2 additions & 2 deletions packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@
"test:e2e:match": "playwright test -g"
},
"dependencies": {
"@astrojs/compiler": "^1.6.3",
"@astrojs/compiler": "^1.8.0",
"@astrojs/internal-helpers": "^0.1.1",
"@astrojs/language-server": "^1.0.0",
"@astrojs/markdown-remark": "^2.2.1",
Expand Down Expand Up @@ -150,7 +150,7 @@
"html-escaper": "^3.0.3",
"js-yaml": "^4.1.0",
"kleur": "^4.1.4",
"magic-string": "^0.27.0",
"magic-string": "^0.30.2",
"mime": "^3.0.0",
"network-information-types": "^0.1.1",
"ora": "^6.3.1",
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export interface AstroBuiltinAttributes {
'is:raw'?: boolean;
'transition:animate'?: 'morph' | 'slide' | 'fade' | TransitionDirectionalAnimations;
'transition:name'?: string;
'transition:persist'?: boolean | string;
}

export interface AstroDefineVarsAttribute {
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/assets/vite-plugin-assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ export default function assets({
if (s) {
return {
code: s.toString(),
map: resolvedConfig.build.sourcemap ? s.generateMap({ hires: true }) : null,
map: resolvedConfig.build.sourcemap ? s.generateMap({ hires: 'boundary' }) : null,
};
} else {
return null;
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/core/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { consoleLogDestination } from '../logger/console.js';
import { error, type LogOptions } from '../logger/core.js';
import { prependForwardSlash, removeTrailingForwardSlash } from '../path.js';
import { RedirectSinglePageBuiltModule } from '../redirects/index.js';
import { isResponse } from '../render/core';
import { isResponse } from '../render/core.js';
import {
createEnvironment,
createRenderContext,
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/core/compile/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export async function compile({
scopedStyleStrategy: astroConfig.scopedStyleStrategy,
resultScopedSlot: true,
experimentalTransitions: astroConfig.experimental.viewTransitions,
experimentalPersistence: astroConfig.experimental.viewTransitions,
transitionsAnimationURL: 'astro/components/viewtransitions.css',
preprocessStyle: createStylePreprocessor({
filename,
Expand Down
11 changes: 11 additions & 0 deletions packages/astro/src/runtime/server/hydration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ interface ExtractedProps {
props: Record<string | number | symbol, any>;
}

const transitionDirectivesToCopyOnIsland = Object.freeze([
'data-astro-transition-scope',
'data-astro-transition-persist',
]);

// Used to extract the directives, aka `client:load` information about a component.
// Finds these special props and removes them from what gets passed into the component.
export function extractDirectives(
Expand Down Expand Up @@ -166,5 +171,11 @@ export async function generateHydrateScript(
})
);

transitionDirectivesToCopyOnIsland.forEach((name) => {
if (props[name]) {
island.props[name] = props[name];
}
});

return island;
}
Loading

0 comments on commit cf80ebf

Please sign in to comment.