Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support prefetch in core #8951

Merged
merged 14 commits into from
Nov 8, 2023
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.

bluwy marked this conversation as resolved.
Show resolved Hide resolved
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