Skip to content

Commit

Permalink
Merge branch 'main' into perf/create-astro
Browse files Browse the repository at this point in the history
  • Loading branch information
natemoo-re authored Sep 7, 2023
2 parents beaba06 + 85fe213 commit 3654604
Show file tree
Hide file tree
Showing 12 changed files with 168 additions and 74 deletions.
5 changes: 5 additions & 0 deletions .changeset/curvy-dolls-thank.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Only transition between pages where both have ViewTransitions enabled
5 changes: 5 additions & 0 deletions .changeset/forty-hotels-itch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/vercel': patch
---

Fix Astro's `domains` and `remotePatterns` not being used by Vercel when using Vercel Image Optimization
64 changes: 33 additions & 31 deletions packages/astro/components/ViewTransitions.astro
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ const { fallback = 'animate' } = Astro.props as Props;
};
type Events = 'astro:page-load' | 'astro:after-swap';

const persistState = (state: State) => history.replaceState(state, '');
// only update history entries that are managed by us
// leave other entries alone and do not accidently add state.
const persistState = (state: State) => history.state && history.replaceState(state, '');
const supportsViewTransitions = !!document.startViewTransition;
const transitionEnabledOnThisPage = () =>
!!document.querySelector('[name="astro-view-transitions-enabled"]');
Expand All @@ -32,11 +34,13 @@ const { fallback = 'animate' } = Astro.props as Props;
// can use that to determine popstate if going forward or back.
let currentHistoryIndex = 0;
if (history.state) {
// we reloaded a page with history state (e.g. back button or browser reload)
// we reloaded a page with history state
// (e.g. history navigation from non-transition page or browser reload)
currentHistoryIndex = history.state.index;
scrollTo({ left: 0, top: history.state.scrollY });
} else if (transitionEnabledOnThisPage()) {
history.replaceState({ index: currentHistoryIndex, scrollY }, '');
}

const throttle = (cb: (...args: any[]) => any, delay: number) => {
let wait = false;
// During the waiting time additional events are lost.
Expand Down Expand Up @@ -109,9 +113,7 @@ const { fallback = 'animate' } = Astro.props as Props;

const parser = new DOMParser();

async function updateDOM(html: string, state?: State, fallback?: Fallback) {
const doc = parser.parseFromString(html, 'text/html');

async function updateDOM(doc: Document, loc: URL, state?: State, fallback?: Fallback) {
// 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 => {
Expand Down Expand Up @@ -189,19 +191,17 @@ const { fallback = 'animate' } = Astro.props as Props;
// Chromium based browsers (Chrome, Edge, Opera, ...)
scrollTo({ left: 0, top: 0, behavior: 'instant' });

if (state?.scrollY === 0 && location.hash) {
const id = decodeURIComponent(location.hash.slice(1));
let initialScrollY = 0;
if (!state && loc.hash) {
const id = decodeURIComponent(loc.hash.slice(1));
const elem = document.getElementById(id);
// prefer scrollIntoView() over scrollTo() because it takes scroll-padding into account
if (elem) {
state.scrollY = elem.offsetTop;
persistState(state); // first guess, later updated by scroll handler
elem.scrollIntoView(); // for Firefox, this should better be {behavior: 'instant'}
}
elem && (initialScrollY = elem.offsetTop) && elem.scrollIntoView();
} else if (state && state.scrollY !== 0) {
scrollTo(0, state.scrollY); // usings default scrollBehavior
}

!state &&
history.pushState({ index: ++currentHistoryIndex, scrollY: initialScrollY }, '', loc.href);
triggerEvent('astro:after-swap');
};

Expand Down Expand Up @@ -247,19 +247,26 @@ const { fallback = 'animate' } = Astro.props as Props;
}
}

async function navigate(dir: Direction, href: string, state?: State) {
async function navigate(dir: Direction, loc: URL, state?: State) {
let finished: Promise<void>;
const href = loc.href;
const { html, ok } = await getHTML(href);
// If there is a problem fetching the new page, just do an MPA navigation to it.
if (!ok) {
location.href = href;
return;
}
const doc = parser.parseFromString(html, 'text/html');
if (!doc.querySelector('[name="astro-view-transitions-enabled"]')) {
location.href = href;
return;
}

document.documentElement.dataset.astroTransition = dir;
if (supportsViewTransitions) {
finished = document.startViewTransition(() => updateDOM(html, state)).finished;
finished = document.startViewTransition(() => updateDOM(doc, loc, state)).finished;
} else {
finished = updateDOM(html, state, getFallback());
finished = updateDOM(doc, loc, state, getFallback());
}
try {
await finished;
Expand Down Expand Up @@ -311,11 +318,11 @@ const { fallback = 'animate' } = Astro.props as Props;
ev.shiftKey || // new window
ev.defaultPrevented ||
!transitionEnabledOnThisPage()
)
) {
// No page transitions in these cases,
// Let the browser standard action handle this
return;

}
// We do not need to handle same page links because there are no page transitions
// Same page means same path and same query params (but different hash)
if (location.pathname === link.pathname && location.search === link.search) {
Expand All @@ -341,10 +348,8 @@ const { fallback = 'animate' } = Astro.props as Props;

// these are the cases we will handle: same origin, different page
ev.preventDefault();
navigate('forward', link.href, { index: ++currentHistoryIndex, scrollY: 0 });
const newState: State = { index: currentHistoryIndex, scrollY };
persistState({ index: currentHistoryIndex - 1, scrollY });
history.pushState(newState, '', link.href);
persistState({ index: currentHistoryIndex, scrollY });
navigate('forward', new URL(link.href));
});

addEventListener('popstate', (ev) => {
Expand Down Expand Up @@ -374,11 +379,11 @@ const { fallback = 'animate' } = Astro.props as Props;
history.scrollRestoration = 'manual';
}

const state: State | undefined = history.state;
const nextIndex = state?.index ?? currentHistoryIndex + 1;
const state: State = history.state;
const nextIndex = state.index;
const direction: Direction = nextIndex > currentHistoryIndex ? 'forward' : 'back';
navigate(direction, location.href, state);
currentHistoryIndex = nextIndex;
navigate(direction, new URL(location.href), state);
});

['mouseenter', 'touchstart', 'focus'].forEach((evName) => {
Expand All @@ -403,12 +408,9 @@ const { fallback = 'animate' } = Astro.props as Props;
// There's not a good way to record scroll position before a back button.
// So the way we do it is by listening to scrollend if supported, and if not continuously record the scroll position.
const updateState = () => {
// only update history entries that are managed by us
// leave other entries alone and do not accidently add state.
if (history.state) {
persistState({ ...history.state, scrollY });
}
persistState({ ...history.state, scrollY });
};

if ('onscrollend' in window) addEventListener('scrollend', updateState);
else addEventListener('scroll', throttle(updateState, 300));
}
Expand Down
11 changes: 11 additions & 0 deletions packages/astro/e2e/fixtures/view-transitions/src/pages/five.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<html>
<head>
<title>Page 5</title>
</head>
<body>
<main>
<p id="five">Page 5</p>
<a id="click-three" href="/two">go to 3</a>
</main>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
<p id="three">Page 3</p>
<a id="click-two" href="/two">go to 2</a>
<br/>
<a id="click-five" href="/five">go to 5</a>
<br/>
<a id="click-hash" href="#click-hash">hash target</a>
<p style="height: 150vh">Long paragraph</p>
</main>
Expand Down
75 changes: 48 additions & 27 deletions packages/astro/e2e/view-transitions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ test.describe('View Transitions', () => {
expect(loads.length, 'There should only be 1 page load').toEqual(1);
});

test('Moving from a page without ViewTransitions triggers a full page navigation', async ({
test('Moving to a page without ViewTransitions triggers a full page navigation', async ({
page,
astro,
}) => {
Expand All @@ -102,10 +102,6 @@ test.describe('View Transitions', () => {
p = page.locator('#three');
await expect(p, 'should have content').toHaveText('Page 3');

await page.click('#click-two');
p = page.locator('#two');
await expect(p, 'should have content').toHaveText('Page 2');

expect(
loads.length,
'There should be 2 page loads. The original, then going from 3 to 2'
Expand Down Expand Up @@ -142,8 +138,8 @@ test.describe('View Transitions', () => {

expect(
loads.length,
'There should be only 1 page load. The original, but no additional loads for the hash change'
).toEqual(1);
'There should be only 2 page loads (for page one & three), but no additional loads for the hash change'
).toEqual(2);
});

test('Moving from a page without ViewTransitions w/ back button', async ({ page, astro }) => {
Expand Down Expand Up @@ -501,25 +497,50 @@ test.describe('View Transitions', () => {
await page.click('#click-logo');
await downloadPromise;
});
});

test('Scroll position is restored on back navigation from page w/o ViewTransitions', async ({
page,
astro,
}) => {
// Go to middle of long page
await page.goto(astro.resolveUrl('/long-page#click-external'));

let locator = page.locator('#click-external');
await expect(locator).toBeInViewport();

// Go to a page that has not enabled ViewTransistions
await page.click('#click-external');
locator = page.locator('#three');
await expect(locator).toHaveText('Page 3');

// Scroll back to long page
await page.goBack();
locator = page.locator('#click-external');
await expect(locator).toBeInViewport();
test('Scroll position is restored on back navigation from page w/o ViewTransitions', async ({
page,
astro,
}) => {
// Go to middle of long page
await page.goto(astro.resolveUrl('/long-page#click-external'));

let locator = page.locator('#click-external');
await expect(locator).toBeInViewport();

// Go to a page that has not enabled ViewTransistions
await page.click('#click-external');
locator = page.locator('#three');
await expect(locator).toHaveText('Page 3');

// Scroll back to long page
await page.goBack();
locator = page.locator('#click-external');
await expect(locator).toBeInViewport();
});

test("Non transition navigation doesn't loose handlers", async ({ page, astro }) => {
// Go to page 1
await page.goto(astro.resolveUrl('/one'));
let p = page.locator('#one');
await expect(p, 'should have content').toHaveText('Page 1');

// go to page 3
await page.click('#click-three');
p = page.locator('#three');
await expect(p, 'should have content').toHaveText('Page 3');

// go to page 5
await page.click('#click-five');
p = page.locator('#five');
await expect(p, 'should have content').toHaveText('Page 5');

await page.goBack();
p = page.locator('#three');
await expect(p, 'should have content').toHaveText('Page 3');

await page.goBack();
p = page.locator('#one');
await expect(p, 'should have content').toHaveText('Page 1');
});
});
2 changes: 2 additions & 0 deletions packages/integrations/vercel/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ export default defineConfig({

Configuration options for [Vercel's Image Optimization API](https://vercel.com/docs/concepts/image-optimization). See [Vercel's image configuration documentation](https://vercel.com/docs/build-output-api/v3/configuration#images) for a complete list of supported parameters.

The `domains` and `remotePatterns` properties will automatically be filled using [the Astro corresponding `image` settings](https://docs.astro.build/en/reference/configuration-reference/#image-options).

```js
// astro.config.mjs
import { defineConfig } from 'astro/config';
Expand Down
23 changes: 14 additions & 9 deletions packages/integrations/vercel/src/image/shared.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import type { ImageMetadata, ImageQualityPreset, ImageTransform } from 'astro';

export const defaultImageConfig: VercelImageConfig = {
sizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
domains: [],
};
import type { AstroConfig, ImageMetadata, ImageQualityPreset, ImageTransform } from 'astro';

export function getDefaultImageConfig(astroImageConfig: AstroConfig['image']): VercelImageConfig {
return {
sizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
domains: astroImageConfig.domains ?? [],
// Cast is necessary here because Vercel's types are slightly different from ours regarding allowed protocols. Behavior should be the same, however.
remotePatterns: (astroImageConfig.remotePatterns as VercelImageConfig['remotePatterns']) ?? [],
};
}

export function isESMImportedImage(src: ImageMetadata | string): src is ImageMetadata {
return typeof src === 'object';
Expand Down Expand Up @@ -56,10 +60,11 @@ export const qualityTable: Record<ImageQualityPreset, number> = {
max: 100,
};

export function getImageConfig(
export function getAstroImageConfig(
images: boolean | undefined,
imagesConfig: VercelImageConfig | undefined,
command: string
command: string,
astroImageConfig: AstroConfig['image']
) {
if (images) {
return {
Expand All @@ -69,7 +74,7 @@ export function getImageConfig(
command === 'dev'
? '@astrojs/vercel/dev-image-service'
: '@astrojs/vercel/build-image-service',
config: imagesConfig ? imagesConfig : defaultImageConfig,
config: imagesConfig ? imagesConfig : getDefaultImageConfig(astroImageConfig),
},
},
};
Expand Down
21 changes: 18 additions & 3 deletions packages/integrations/vercel/src/serverless/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ import { AstroError } from 'astro/errors';
import glob from 'fast-glob';
import { basename } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { defaultImageConfig, getImageConfig, type VercelImageConfig } from '../image/shared.js';
import {
getAstroImageConfig,
getDefaultImageConfig,
type VercelImageConfig,
} from '../image/shared.js';
import { exposeEnv } from '../lib/env.js';
import { getVercelOutput, removeDir, writeJson } from '../lib/fs.js';
import { copyDependenciesToFunction } from '../lib/nft.js';
Expand Down Expand Up @@ -143,7 +147,7 @@ export default function vercelServerless({
external: ['@vercel/nft'],
},
},
...getImageConfig(imageService, imagesConfig, command),
...getAstroImageConfig(imageService, imagesConfig, command, config.image),
});
},
'astro:config:done': ({ setAdapter, config, logger }) => {
Expand Down Expand Up @@ -250,7 +254,18 @@ You can set functionPerRoute: false to prevent surpassing the limit.`
...routeDefinitions,
],
...(imageService || imagesConfig
? { images: imagesConfig ? imagesConfig : defaultImageConfig }
? {
images: imagesConfig
? {
...imagesConfig,
domains: [...imagesConfig.domains, ..._config.image.domains],
remotePatterns: [
...(imagesConfig.remotePatterns ?? []),
..._config.image.remotePatterns,
],
}
: getDefaultImageConfig(_config.image),
}
: {}),
});

Expand Down
Loading

0 comments on commit 3654604

Please sign in to comment.