Skip to content

Commit

Permalink
Support prefetch in core (#8951)
Browse files Browse the repository at this point in the history
Co-authored-by: Sarah Rainsberger <[email protected]>
  • Loading branch information
bluwy and sarah11918 authored Nov 8, 2023
1 parent 23c9a30 commit 38e21d1
Show file tree
Hide file tree
Showing 23 changed files with 688 additions and 41 deletions.
21 changes: 21 additions & 0 deletions .changeset/sixty-laws-argue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
'astro': minor
---

Prefetching is now supported in core

You can enable prefetching for your site with the `prefetch: true` config. It is enabled by default when using [View Transitions](https://docs.astro.build/en/guides/view-transitions/) and can also be used to configure the `prefetch` behaviour used by View Transitions.

You can enable prefetching by setting `prefetch:true` in your Astro config:

```js
// astro.config.js
import { defineConfig } from 'astro/config';

export default defineConfig({
prefetch: true
})
```

This replaces the `@astrojs/prefetch` integration, which is now deprecated and will eventually be removed.
Visit the [Prefetch guide](https://docs.astro.build/en/guides/prefetch/) for more information.
4 changes: 4 additions & 0 deletions packages/astro/client.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ declare module 'astro:transitions/client' {
export const navigate: TransitionRouterModule['navigate'];
}

declare module 'astro:prefetch' {
export { prefetch, PrefetchOptions } from 'astro/prefetch';
}

declare module 'astro:middleware' {
export * from 'astro/middleware/namespace';
}
Expand Down
45 changes: 7 additions & 38 deletions packages/astro/components/ViewTransitions.astro
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,9 @@ const { fallback = 'animate' } = Astro.props;
<meta name="astro-view-transitions-enabled" content="true" />
<meta name="astro-view-transitions-fallback" content={fallback} />
<script>
import {
supportsViewTransitions,
transitionEnabledOnThisPage,
navigate,
} from 'astro:transitions/client';
import { supportsViewTransitions, navigate } from 'astro:transitions/client';
// NOTE: import from `astro/prefetch` as `astro:prefetch` requires the `prefetch` config to be enabled
import { init } from 'astro/prefetch';
export type Fallback = 'none' | 'animate' | 'swap';

function getFallback(): Fallback {
Expand All @@ -40,21 +38,6 @@ const { fallback = 'animate' } = Astro.props;
return 'animate';
}

// Prefetching
function maybePrefetch(pathname: string) {
if (document.querySelector(`link[rel=prefetch][href="${pathname}"]`)) return;
// @ts-expect-error: connection might exist
if (navigator.connection) {
// @ts-expect-error: connection does exist
let conn = navigator.connection;
if (conn.saveData || /(2|3)g/.test(conn.effectiveType || '')) return;
}
let link = document.createElement('link');
link.setAttribute('rel', 'prefetch');
link.setAttribute('href', pathname);
document.head.append(link);
}

if (supportsViewTransitions || getFallback() !== 'none') {
document.addEventListener('click', (ev) => {
let link = ev.target;
Expand Down Expand Up @@ -89,23 +72,9 @@ const { fallback = 'animate' } = Astro.props;
});
});

['mouseenter', 'touchstart', 'focus'].forEach((evName) => {
document.addEventListener(
evName,
(ev) => {
if (ev.target instanceof HTMLAnchorElement) {
let el = ev.target;
if (
el.origin === location.origin &&
el.pathname !== location.pathname &&
transitionEnabledOnThisPage()
) {
maybePrefetch(el.pathname);
}
}
},
{ passive: true, capture: true }
);
});
// @ts-expect-error injected by vite-plugin-transitions for treeshaking
if (!__PREFETCH_DISABLED__) {
init({ prefetchAll: true });
}
}
</script>
6 changes: 6 additions & 0 deletions packages/astro/e2e/fixtures/prefetch/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { defineConfig } from 'astro/config';

// https://astro.build/config
export default defineConfig({
prefetch: true
});
8 changes: 8 additions & 0 deletions packages/astro/e2e/fixtures/prefetch/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "@e2e/prefetch",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*"
}
}
30 changes: 30 additions & 0 deletions packages/astro/e2e/fixtures/prefetch/src/pages/index.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<h1>Prefetch</h1>
<a id="prefetch-default" href="/prefetch-default">default</a>
<br>
<a id="prefetch-false" href="/prefetch-false" data-astro-prefetch="false">false</a>
<br>
<a id="prefetch-tap" href="/prefetch-tap" data-astro-prefetch="tap">tap</a>
<br>
<a id="prefetch-hover" href="/prefetch-hover" data-astro-prefetch="hover">hover</a>
<br>
<button id="prefetch-manual">manual</button>
<br>
<span>Scroll down to trigger viewport prefetch</span>
<!-- Large empty space to test viewport -->
<div style="height: 1000px;"></div>
<a id="prefetch-viewport" href="/prefetch-viewport" data-astro-prefetch="viewport">viewport</a>
<script>
// @ts-nocheck
import { prefetch } from 'astro:prefetch'
document.getElementById('prefetch-manual').addEventListener('click', () => {
prefetch('/prefetch-manual', { with: 'link' })
})
</script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h1>Prefetch default</h1>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h1>Prefetch false</h1>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h1>Prefetch hover</h1>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h1>Prefetch tap</h1>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h1>Prefetch viewport</h1>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
import Layout from '../components/Layout.astro';
---
<Layout>
<a id="prefetch-one" href="/one">Go to one with prefetch on hover</a>
</Layout>
163 changes: 163 additions & 0 deletions packages/astro/e2e/prefetch.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { expect } from '@playwright/test';
import { testFactory } from './test-utils.js';

const test = testFactory({
root: './fixtures/prefetch/',
});

test.describe('Prefetch (default)', () => {
let devServer;
/** @type {string[]} */
const reqUrls = [];

test.beforeAll(async ({ astro }) => {
devServer = await astro.startDevServer();
});

test.beforeEach(async ({ page }) => {
page.on('request', (req) => {
reqUrls.push(new URL(req.url()).pathname);
});
});

test.afterEach(() => {
reqUrls.length = 0;
});

test.afterAll(async () => {
await devServer.stop();
});

test('Link without data-astro-prefetch should not prefetch', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/'));
expect(reqUrls).not.toContainEqual('/prefetch-default');
});

test('data-astro-prefetch="false" should not prefetch', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/'));
expect(reqUrls).not.toContainEqual('/prefetch-false');
});

test('data-astro-prefetch="tap" should prefetch on tap', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/'));
expect(reqUrls).not.toContainEqual('/prefetch-tap');
await Promise.all([
page.waitForEvent('request'), // wait prefetch request
page.locator('#prefetch-tap').click(),
]);
expect(reqUrls).toContainEqual('/prefetch-tap');
});

test('data-astro-prefetch="hover" should prefetch on hover', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/'));
expect(reqUrls).not.toContainEqual('/prefetch-hover');
await Promise.all([
page.waitForEvent('request'), // wait prefetch request
page.locator('#prefetch-hover').hover(),
]);
expect(reqUrls).toContainEqual('/prefetch-hover');
});

test('data-astro-prefetch="viewport" should prefetch on viewport', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/'));
expect(reqUrls).not.toContainEqual('/prefetch-viewport');
// Scroll down to show the element
await Promise.all([
page.waitForEvent('request'), // wait prefetch request
page.locator('#prefetch-viewport').scrollIntoViewIfNeeded(),
]);
expect(reqUrls).toContainEqual('/prefetch-viewport');
expect(page.locator('link[rel="prefetch"][href$="/prefetch-viewport"]')).toBeDefined();
});

test('manual prefetch() works once', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/'));
expect(reqUrls).not.toContainEqual('/prefetch-manual');
await Promise.all([
page.waitForEvent('request'), // wait prefetch request
page.locator('#prefetch-manual').click(),
]);
expect(reqUrls).toContainEqual('/prefetch-manual');
expect(page.locator('link[rel="prefetch"][href$="/prefetch-manual"]')).toBeDefined();

// prefetch again should have no effect
await page.locator('#prefetch-manual').click();
expect(reqUrls.filter((u) => u.includes('/prefetch-manual')).length).toEqual(1);
});
});

test.describe("Prefetch (prefetchAll: true, defaultStrategy: 'tap')", () => {
let devServer;
/** @type {string[]} */
const reqUrls = [];

test.beforeAll(async ({ astro }) => {
devServer = await astro.startDevServer({
prefetch: {
prefetchAll: true,
defaultStrategy: 'tap',
},
});
});

test.beforeEach(async ({ page }) => {
page.on('request', (req) => {
reqUrls.push(new URL(req.url()).pathname);
});
});

test.afterEach(() => {
reqUrls.length = 0;
});

test.afterAll(async () => {
await devServer.stop();
});

test('Link without data-astro-prefetch should prefetch', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/'));
expect(reqUrls).not.toContainEqual('/prefetch-default');
await Promise.all([
page.waitForEvent('request'), // wait prefetch request
page.locator('#prefetch-default').click(),
]);
expect(reqUrls).toContainEqual('/prefetch-default');
});

test('data-astro-prefetch="false" should not prefetch', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/'));
expect(reqUrls).not.toContainEqual('/prefetch-false');
});

test('data-astro-prefetch="tap" should prefetch on tap', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/'));
expect(reqUrls).not.toContainEqual('/prefetch-tap');
await Promise.all([
page.waitForEvent('request'), // wait prefetch request
page.locator('#prefetch-tap').click(),
]);
expect(reqUrls).toContainEqual('/prefetch-tap');
});

test('data-astro-prefetch="hover" should prefetch on hover', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/'));
expect(reqUrls).not.toContainEqual('/prefetch-hover');
await Promise.all([
page.waitForEvent('request'), // wait prefetch request
page.locator('#prefetch-hover').hover(),
]);
expect(reqUrls).toContainEqual('/prefetch-hover');
});

test('data-astro-prefetch="viewport" should prefetch on viewport', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/'));
expect(reqUrls).not.toContainEqual('/prefetch-viewport');
// Scroll down to show the element
await Promise.all([
page.waitForEvent('request'), // wait prefetch request
page.locator('#prefetch-viewport').scrollIntoViewIfNeeded(),
]);
expect(reqUrls).toContainEqual('/prefetch-viewport');
expect(page.locator('link[rel="prefetch"][href$="/prefetch-viewport"]')).toBeDefined();
});
});
15 changes: 15 additions & 0 deletions packages/astro/e2e/view-transitions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -901,4 +901,19 @@ test.describe('View Transitions', () => {
let announcer = page.locator('.astro-route-announcer');
await expect(announcer, 'should have content').toHaveCSS('width', '1px');
});

test('should prefetch on hover by default', async ({ page, astro }) => {
/** @type {string[]} */
const reqUrls = [];
page.on('request', (req) => {
reqUrls.push(new URL(req.url()).pathname);
});
await page.goto(astro.resolveUrl('/prefetch'));
expect(reqUrls).not.toContainEqual('/one');
await Promise.all([
page.waitForEvent('request'), // wait prefetch request
page.locator('#prefetch-one').hover(),
]);
expect(reqUrls).toContainEqual('/one');
});
});
3 changes: 2 additions & 1 deletion packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@
"default": "./dist/core/middleware/namespace.js"
},
"./transitions": "./dist/transitions/index.js",
"./transitions/router": "./dist/transitions/router.js"
"./transitions/router": "./dist/transitions/router.js",
"./prefetch": "./dist/prefetch/index.js"
},
"imports": {
"#astro/*": "./dist/*.js"
Expand Down
Loading

0 comments on commit 38e21d1

Please sign in to comment.