From 9680cf27803d280b632c91a4252283f71e7c4aee Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Wed, 17 Jan 2024 13:11:46 +0000 Subject: [PATCH 1/6] [ci] format --- packages/astro/src/core/app/index.ts | 20 ++++---- packages/astro/src/core/app/node.ts | 45 +++++++++--------- .../client/dev-toolbar/apps/audit/a11y.ts | 44 +++++++++--------- packages/astro/test/astro-cookies.test.js | 10 ++-- packages/astro/test/client-address.test.js | 2 +- .../integrations/node/src/log-listening-on.ts | 26 ++++++----- packages/integrations/node/src/middleware.ts | 46 +++++++++---------- packages/integrations/node/src/preview.ts | 14 +++--- packages/integrations/node/src/serve-app.ts | 38 +++++++-------- .../integrations/node/src/serve-static.ts | 16 +++---- packages/integrations/node/src/server.ts | 4 +- packages/integrations/node/src/standalone.ts | 20 ++++---- packages/integrations/node/test/test-utils.js | 4 +- .../vercel/src/serverless/entrypoint.ts | 12 +++-- 14 files changed, 148 insertions(+), 153 deletions(-) diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index 0340a19f3801..ad207d129054 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -42,18 +42,18 @@ const REROUTABLE_STATUS_CODES = new Set([404, 500]); export interface RenderOptions { /** * Whether to automatically add all cookies written by `Astro.cookie.set()` to the response headers. - * + * * When set to `true`, they will be added to the `Set-Cookie` header as comma-separated key=value pairs. You can use the standard `response.headers.getSetCookie()` API to read them individually. - * + * * When set to `false`, the cookies will only be available from `App.getSetCookieFromResponse(response)`. - * + * * @default {false} */ addCookieHeader?: boolean; /** * The client IP address that will be made available as `Astro.clientAddress` in pages, and as `ctx.clientAddress` in API routes and middleware. - * + * * Default: `request[Symbol.for("astro.clientAddress")]` */ clientAddress?: string; @@ -65,7 +65,7 @@ export interface RenderOptions { /** * **Advanced API**: you probably do not need to use this. - * + * * Default: `app.match(request)` */ routeData?: RouteData; @@ -196,12 +196,10 @@ export class App { if ( routeDataOrOptions && - ( - 'addCookieHeader' in routeDataOrOptions || + ('addCookieHeader' in routeDataOrOptions || 'clientAddress' in routeDataOrOptions || 'locals' in routeDataOrOptions || - 'routeData' in routeDataOrOptions - ) + 'routeData' in routeDataOrOptions) ) { if ('addCookieHeader' in routeDataOrOptions) { addCookieHeader = routeDataOrOptions.addCookieHeader; @@ -226,7 +224,7 @@ export class App { Reflect.set(request, localsSymbol, locals); } if (clientAddress) { - Reflect.set(request, clientAddressSymbol, clientAddress) + Reflect.set(request, clientAddressSymbol, clientAddress); } // Handle requests with duplicate slashes gracefully by cloning with a cleaned-up request URL if (request.url !== collapseDuplicateSlashes(request.url)) { @@ -323,7 +321,7 @@ export class App { * @param response The response to read cookies from. * @returns An iterator that yields key-value pairs as equal-sign-separated strings. */ - static getSetCookieFromResponse = getSetCookiesFromResponse + static getSetCookieFromResponse = getSetCookiesFromResponse; /** * Creates the render context of the current route diff --git a/packages/astro/src/core/app/node.ts b/packages/astro/src/core/app/node.ts index aa777db8831c..c71aaafcecaa 100644 --- a/packages/astro/src/core/app/node.ts +++ b/packages/astro/src/core/app/node.ts @@ -33,11 +33,7 @@ export class NodeApp extends App { * @deprecated Instead of passing `RouteData` and locals individually, pass an object with `routeData` and `locals` properties. * See https://github.com/withastro/astro/pull/9199 for more information. */ - render( - request: NodeRequest | Request, - routeData?: RouteData, - locals?: object - ): Promise; + render(request: NodeRequest | Request, routeData?: RouteData, locals?: object): Promise; render( req: NodeRequest | Request, routeDataOrOptions?: RouteData | RenderOptions, @@ -55,26 +51,24 @@ export class NodeApp extends App { * ```js * import { NodeApp } from 'astro/app/node'; * import { createServer } from 'node:http'; - * + * * const server = createServer(async (req, res) => { - * const request = NodeApp.createRequest(req); - * const response = await app.render(request); - * await NodeApp.writeResponse(response, res); + * const request = NodeApp.createRequest(req); + * const response = await app.render(request); + * await NodeApp.writeResponse(response, res); * }) * ``` */ - static createRequest( - req: NodeRequest, - { skipBody = false } = {} - ): Request { - const protocol = req.headers['x-forwarded-proto'] ?? + static createRequest(req: NodeRequest, { skipBody = false } = {}): Request { + const protocol = + req.headers['x-forwarded-proto'] ?? ('encrypted' in req.socket && req.socket.encrypted ? 'https' : 'http'); const hostname = req.headers.host || req.headers[':authority']; const url = `${protocol}://${hostname}${req.url}`; const options: RequestInit = { method: req.method || 'GET', headers: makeRequestHeaders(req), - } + }; const bodyAllowed = options.method !== 'HEAD' && options.method !== 'GET' && skipBody === false; if (bodyAllowed) { Object.assign(options, makeRequestBody(req)); @@ -91,14 +85,14 @@ export class NodeApp extends App { * ```js * import { NodeApp } from 'astro/app/node'; * import { createServer } from 'node:http'; - * + * * const server = createServer(async (req, res) => { - * const request = NodeApp.createRequest(req); - * const response = await app.render(request); - * await NodeApp.writeResponse(response, res); + * const request = NodeApp.createRequest(req); + * const response = await app.render(request); + * await NodeApp.writeResponse(response, res); * }) * ``` - * @param source WhatWG Response + * @param source WhatWG Response * @param destination NodeJS ServerResponse */ static async writeResponse(source: Response, destination: ServerResponse) { @@ -111,8 +105,11 @@ export class NodeApp extends App { // Cancelling the reader may reject not just because of // an error in the ReadableStream's cancel callback, but // also because of an error anywhere in the stream. - reader.cancel().catch(err => { - console.error(`There was an uncaught error in the middle of the stream while rendering ${destination.req.url}.`, err); + reader.cancel().catch((err) => { + console.error( + `There was an uncaught error in the middle of the stream while rendering ${destination.req.url}.`, + err + ); }); }); let result = await reader.read(); @@ -120,13 +117,13 @@ export class NodeApp extends App { destination.write(result.value); result = await reader.read(); } - // the error will be logged by the "on end" callback above + // the error will be logged by the "on end" callback above } catch { destination.write('Internal server error'); } } destination.end(); - }; + } } function makeRequestHeaders(req: NodeRequest): Headers { diff --git a/packages/astro/src/runtime/client/dev-toolbar/apps/audit/a11y.ts b/packages/astro/src/runtime/client/dev-toolbar/apps/audit/a11y.ts index 537b03e00d39..e004b70b5fb1 100644 --- a/packages/astro/src/runtime/client/dev-toolbar/apps/audit/a11y.ts +++ b/packages/astro/src/runtime/client/dev-toolbar/apps/audit/a11y.ts @@ -397,29 +397,29 @@ export const a11y: AuditRuleWithSelector[] = [ const inputElements = element.querySelectorAll('input'); for (const input of inputElements) { - // Check for alt attribute if input type is image - if (input.type === 'image') { - const altAttribute = input.getAttribute('alt'); - if (altAttribute && altAttribute.trim() !== '') return false; - } - - // Check for aria-label - const inputAriaLabel = input.getAttribute('aria-label')?.trim(); - if (inputAriaLabel && inputAriaLabel !== '') return false; - - // Check for aria-labelledby - const inputAriaLabelledby = input.getAttribute('aria-labelledby')?.trim(); - if (inputAriaLabelledby) { - const ids = inputAriaLabelledby.split(' '); - for (const id of ids) { - const referencedElement = document.getElementById(id); - if (referencedElement && referencedElement.innerText.trim() !== '') return false; - } + // Check for alt attribute if input type is image + if (input.type === 'image') { + const altAttribute = input.getAttribute('alt'); + if (altAttribute && altAttribute.trim() !== '') return false; + } + + // Check for aria-label + const inputAriaLabel = input.getAttribute('aria-label')?.trim(); + if (inputAriaLabel && inputAriaLabel !== '') return false; + + // Check for aria-labelledby + const inputAriaLabelledby = input.getAttribute('aria-labelledby')?.trim(); + if (inputAriaLabelledby) { + const ids = inputAriaLabelledby.split(' '); + for (const id of ids) { + const referencedElement = document.getElementById(id); + if (referencedElement && referencedElement.innerText.trim() !== '') return false; } - - // Check for title - const title = input.getAttribute('title')?.trim(); - if (title && title !== '') return false; + } + + // Check for title + const title = input.getAttribute('title')?.trim(); + if (title && title !== '') return false; } // If all checks fail, return true indicating missing content diff --git a/packages/astro/test/astro-cookies.test.js b/packages/astro/test/astro-cookies.test.js index 7b6cfafa7478..e201ebd876b8 100644 --- a/packages/astro/test/astro-cookies.test.js +++ b/packages/astro/test/astro-cookies.test.js @@ -93,18 +93,20 @@ describe('Astro.cookies', () => { const request = new Request('http://example.com/set-value', { method: 'POST', }); - const response = await app.render(request, { addCookieHeader: true }) + const response = await app.render(request, { addCookieHeader: true }); expect(response.status).to.equal(200); - expect(response.headers.get("Set-Cookie")).to.be.a('string').and.satisfy(value => value.startsWith("admin=true; Expires=")); + expect(response.headers.get('Set-Cookie')) + .to.be.a('string') + .and.satisfy((value) => value.startsWith('admin=true; Expires=')); }); it('app.render can exclude the cookie from the Set-Cookie header', async () => { const request = new Request('http://example.com/set-value', { method: 'POST', }); - const response = await app.render(request, { addCookieHeader: false }) + const response = await app.render(request, { addCookieHeader: false }); expect(response.status).to.equal(200); - expect(response.headers.get("Set-Cookie")).to.equal(null); + expect(response.headers.get('Set-Cookie')).to.equal(null); }); it('Early returning a Response still includes set headers', async () => { diff --git a/packages/astro/test/client-address.test.js b/packages/astro/test/client-address.test.js index 5d6df5ac9e61..339d17ad94de 100644 --- a/packages/astro/test/client-address.test.js +++ b/packages/astro/test/client-address.test.js @@ -33,7 +33,7 @@ describe('Astro.clientAddress', () => { it('app.render can provide the address', async () => { const app = await fixture.loadTestAdapterApp(); const request = new Request('http://example.com/'); - const response = await app.render(request, { clientAddress: "1.1.1.1" }); + const response = await app.render(request, { clientAddress: '1.1.1.1' }); const html = await response.text(); const $ = cheerio.load(html); expect($('#address').text()).to.equal('1.1.1.1'); diff --git a/packages/integrations/node/src/log-listening-on.ts b/packages/integrations/node/src/log-listening-on.ts index 4f56b3ee8bd2..5ce93bfd7bb4 100644 --- a/packages/integrations/node/src/log-listening-on.ts +++ b/packages/integrations/node/src/log-listening-on.ts @@ -1,20 +1,24 @@ -import os from "node:os"; -import type http from "node:http"; -import https from "node:https"; -import type { AstroIntegrationLogger } from "astro"; +import os from 'node:os'; +import type http from 'node:http'; +import https from 'node:https'; +import type { AstroIntegrationLogger } from 'astro'; import type { Options } from './types.js'; -import type { AddressInfo } from "node:net"; +import type { AddressInfo } from 'node:net'; -export async function logListeningOn(logger: AstroIntegrationLogger, server: http.Server | https.Server, options: Pick) { - await new Promise(resolve => server.once('listening', resolve)) - const protocol = server instanceof https.Server ? 'https' : 'http'; - // Allow to provide host value at runtime +export async function logListeningOn( + logger: AstroIntegrationLogger, + server: http.Server | https.Server, + options: Pick +) { + await new Promise((resolve) => server.once('listening', resolve)); + const protocol = server instanceof https.Server ? 'https' : 'http'; + // Allow to provide host value at runtime const host = getResolvedHostForHttpServer( process.env.HOST !== undefined && process.env.HOST !== '' ? process.env.HOST : options.host ); - const { port } = server.address() as AddressInfo; + const { port } = server.address() as AddressInfo; const address = getNetworkAddress(protocol, host, port); - + if (host === undefined) { logger.info( `Server listening on \n local: ${address.local[0]} \t\n network: ${address.network[0]}\n` diff --git a/packages/integrations/node/src/middleware.ts b/packages/integrations/node/src/middleware.ts index a936dc5bc92c..6b6c2afee4a1 100644 --- a/packages/integrations/node/src/middleware.ts +++ b/packages/integrations/node/src/middleware.ts @@ -1,34 +1,32 @@ import { createAppHandler } from './serve-app.js'; -import type { RequestHandler } from "./types.js"; -import type { NodeApp } from "astro/app/node"; +import type { RequestHandler } from './types.js'; +import type { NodeApp } from 'astro/app/node'; /** * Creates a middleware that can be used with Express, Connect, etc. - * + * * Similar to `createAppHandler` but can additionally be placed in the express * chain as an error middleware. - * + * * https://expressjs.com/en/guide/using-middleware.html#middleware.error-handling */ -export default function createMiddleware( - app: NodeApp, -): RequestHandler { - const handler = createAppHandler(app) - const logger = app.getAdapterLogger() - // using spread args because express trips up if the function's - // stringified body includes req, res, next, locals directly - return async function (...args) { - // assume normal invocation at first - const [req, res, next, locals] = args; - // short circuit if it is an error invocation - if (req instanceof Error) { - const error = req; - if (next) { - return next(error); - } else { - throw error; - } - } +export default function createMiddleware(app: NodeApp): RequestHandler { + const handler = createAppHandler(app); + const logger = app.getAdapterLogger(); + // using spread args because express trips up if the function's + // stringified body includes req, res, next, locals directly + return async function (...args) { + // assume normal invocation at first + const [req, res, next, locals] = args; + // short circuit if it is an error invocation + if (req instanceof Error) { + const error = req; + if (next) { + return next(error); + } else { + throw error; + } + } try { await handler(req, res, next, locals); } catch (err) { @@ -39,5 +37,5 @@ export default function createMiddleware( res.end(); } } - } + }; } diff --git a/packages/integrations/node/src/preview.ts b/packages/integrations/node/src/preview.ts index 26b91756c8f0..e8747ad0d901 100644 --- a/packages/integrations/node/src/preview.ts +++ b/packages/integrations/node/src/preview.ts @@ -33,16 +33,16 @@ const createPreviewServer: CreatePreviewServer = async function (preview) { throw err; } } - const host = preview.host ?? "localhost" - const port = preview.port ?? 4321 + const host = preview.host ?? 'localhost'; + const port = preview.port ?? 4321; const server = createServer(ssrHandler, host, port); - logListeningOn(preview.logger, server.server, options) + logListeningOn(preview.logger, server.server, options); await new Promise((resolve, reject) => { - server.server.once('listening', resolve); - server.server.once('error', reject); - server.server.listen(port, host); + server.server.once('listening', resolve); + server.server.once('error', reject); + server.server.listen(port, host); }); return server; }; -export { createPreviewServer as default } +export { createPreviewServer as default }; diff --git a/packages/integrations/node/src/serve-app.ts b/packages/integrations/node/src/serve-app.ts index 51ef3157525d..f2fc61f010d4 100644 --- a/packages/integrations/node/src/serve-app.ts +++ b/packages/integrations/node/src/serve-app.ts @@ -1,5 +1,5 @@ -import { NodeApp } from "astro/app/node" -import type { RequestHandler } from "./types.js"; +import { NodeApp } from 'astro/app/node'; +import type { RequestHandler } from './types.js'; /** * Creates a Node.js http listener for on-demand rendered pages, compatible with http.createServer and Connect middleware. @@ -7,21 +7,21 @@ import type { RequestHandler } from "./types.js"; * Intended to be used in both standalone and middleware mode. */ export function createAppHandler(app: NodeApp): RequestHandler { - return async (req, res, next, locals) => { - const request = NodeApp.createRequest(req); - const routeData = app.match(request); - if (routeData) { - const response = await app.render(request, { - addCookieHeader: true, - locals, - routeData, - }); - await NodeApp.writeResponse(response, res); - } else if (next) { - return next(); - } else { - const response = await app.render(req); - await NodeApp.writeResponse(response, res); - } - } + return async (req, res, next, locals) => { + const request = NodeApp.createRequest(req); + const routeData = app.match(request); + if (routeData) { + const response = await app.render(request, { + addCookieHeader: true, + locals, + routeData, + }); + await NodeApp.writeResponse(response, res); + } else if (next) { + return next(); + } else { + const response = await app.render(req); + await NodeApp.writeResponse(response, res); + } + }; } diff --git a/packages/integrations/node/src/serve-static.ts b/packages/integrations/node/src/serve-static.ts index ee3bdaf791b6..77de9b35809c 100644 --- a/packages/integrations/node/src/serve-static.ts +++ b/packages/integrations/node/src/serve-static.ts @@ -1,9 +1,9 @@ -import path from "node:path"; -import url from "node:url"; -import send from "send"; -import type { IncomingMessage, ServerResponse } from "node:http"; -import type { Options } from "./types.js"; -import type { NodeApp } from "astro/app/node"; +import path from 'node:path'; +import url from 'node:url'; +import send from 'send'; +import type { IncomingMessage, ServerResponse } from 'node:http'; +import type { Options } from './types.js'; +import type { NodeApp } from 'astro/app/node'; /** * Creates a Node.js http listener for static files and prerendered pages. @@ -16,7 +16,7 @@ export function createStaticHandler(app: NodeApp, options: Options) { /** * @param ssr The SSR handler to be called if the static handler does not find a matching file. */ - return (req: IncomingMessage, res: ServerResponse, ssr: () => unknown) => { + return (req: IncomingMessage, res: ServerResponse, ssr: () => unknown) => { if (req.url) { let pathname = app.removeBase(req.url); pathname = decodeURI(new URL(pathname, 'http://host').pathname); @@ -39,7 +39,7 @@ export function createStaticHandler(app: NodeApp, options: Options) { ssr(); }); stream.on('headers', (_res: ServerResponse) => { - // assets in dist/_astro are hashed and should get the immutable header + // assets in dist/_astro are hashed and should get the immutable header if (pathname.startsWith(`/${options.assets}/`)) { // This is the "far future" cache header, used for static files whose name includes their digest hash. // 1 year (31,536,000 seconds) is convention. diff --git a/packages/integrations/node/src/server.ts b/packages/integrations/node/src/server.ts index 5c2577ff8d5d..d9f24cca5efb 100644 --- a/packages/integrations/node/src/server.ts +++ b/packages/integrations/node/src/server.ts @@ -11,9 +11,7 @@ export function createExports(manifest: SSRManifest, options: Options) { return { options: options, handler: - options.mode === "middleware" - ? createMiddleware(app) - : createStandaloneHandler(app, options), + options.mode === 'middleware' ? createMiddleware(app) : createStandaloneHandler(app, options), startServer: () => startServer(app, options), }; } diff --git a/packages/integrations/node/src/standalone.ts b/packages/integrations/node/src/standalone.ts index fc1875e972b4..9fa8bd88c7af 100644 --- a/packages/integrations/node/src/standalone.ts +++ b/packages/integrations/node/src/standalone.ts @@ -12,13 +12,13 @@ import type { PreviewServer } from 'astro'; export default function standalone(app: NodeApp, options: Options) { const port = process.env.PORT ? Number(process.env.PORT) : options.port ?? 8080; // Allow to provide host value at runtime - const hostOptions = typeof options.host === "boolean" ? "localhost" : options.host + const hostOptions = typeof options.host === 'boolean' ? 'localhost' : options.host; const host = process.env.HOST ?? hostOptions; const handler = createStandaloneHandler(app, options); const server = createServer(handler, host, port); - server.server.listen(port, host) - if (process.env.ASTRO_NODE_LOGGING !== "disabled") { - logListeningOn(app.getAdapterLogger(), server.server, options) + server.server.listen(port, host); + if (process.env.ASTRO_NODE_LOGGING !== 'disabled') { + logListeningOn(app.getAdapterLogger(), server.server, options); } return { server, @@ -40,15 +40,11 @@ export function createStandaloneHandler(app: NodeApp, options: Options) { return; } staticHandler(req, res, () => appHandler(req, res)); - } + }; } // also used by preview entrypoint -export function createServer( - listener: http.RequestListener, - host: string, - port: number -) { +export function createServer(listener: http.RequestListener, host: string, port: number) { let httpServer: http.Server | https.Server; if (process.env.SERVER_CERT_PATH && process.env.SERVER_KEY_PATH) { @@ -69,7 +65,7 @@ export function createServer( httpServer.addListener('close', resolve); httpServer.addListener('error', reject); }); - + const previewable = { host, port, @@ -80,7 +76,7 @@ export function createServer( await new Promise((resolve, reject) => { httpServer.destroy((err) => (err ? reject(err) : resolve(undefined))); }); - } + }, } satisfies PreviewServer; return { diff --git a/packages/integrations/node/test/test-utils.js b/packages/integrations/node/test/test-utils.js index 6c8c5d270628..8d830fee9831 100644 --- a/packages/integrations/node/test/test-utils.js +++ b/packages/integrations/node/test/test-utils.js @@ -2,8 +2,8 @@ import httpMocks from 'node-mocks-http'; import { EventEmitter } from 'node:events'; import { loadFixture as baseLoadFixture } from '../../../astro/test/test-utils.js'; -process.env.ASTRO_NODE_AUTOSTART = "disabled"; -process.env.ASTRO_NODE_LOGGING = "disabled"; +process.env.ASTRO_NODE_AUTOSTART = 'disabled'; +process.env.ASTRO_NODE_LOGGING = 'disabled'; /** * @typedef {import('../../../astro/test/test-utils').Fixture} Fixture */ diff --git a/packages/integrations/vercel/src/serverless/entrypoint.ts b/packages/integrations/vercel/src/serverless/entrypoint.ts index 5d4c7cb21cce..dedc7fcc1a64 100644 --- a/packages/integrations/vercel/src/serverless/entrypoint.ts +++ b/packages/integrations/vercel/src/serverless/entrypoint.ts @@ -9,12 +9,14 @@ export const createExports = (manifest: SSRManifest) => { const app = new NodeApp(manifest); const handler = async (req: IncomingMessage, res: ServerResponse) => { const clientAddress = req.headers['x-forwarded-for'] as string | undefined; - const localsHeader = req.headers[ASTRO_LOCALS_HEADER] + const localsHeader = req.headers[ASTRO_LOCALS_HEADER]; const locals = - typeof localsHeader === "string" ? JSON.parse(localsHeader) - : Array.isArray(localsHeader) ? JSON.parse(localsHeader[0]) - : {}; - const webResponse = await app.render(req, { locals, clientAddress }) + typeof localsHeader === 'string' + ? JSON.parse(localsHeader) + : Array.isArray(localsHeader) + ? JSON.parse(localsHeader[0]) + : {}; + const webResponse = await app.render(req, { locals, clientAddress }); await NodeApp.writeResponse(webResponse, res); }; From a5f1682347e602330246129d4666a9227374c832 Mon Sep 17 00:00:00 2001 From: Ross Robino Date: Wed, 17 Jan 2024 08:11:58 -0500 Subject: [PATCH 2/6] feat: add experimental client prerender (#9644) * feat: add experimental client prerender * Update packages/astro/src/@types/astro.ts Co-authored-by: Sarah Rainsberger * docs: add more details about effects of the feature * add changeset * add tests * edit jsdoc and changeset with suggestions * Update packages/astro/src/@types/astro.ts Co-authored-by: Bjorn Lu * Update packages/astro/src/prefetch/index.ts Co-authored-by: Bjorn Lu * Update .changeset/sixty-dogs-sneeze.md Co-authored-by: Sarah Rainsberger * Update packages/astro/src/@types/astro.ts Co-authored-by: Sarah Rainsberger * Update .changeset/sixty-dogs-sneeze.md Co-authored-by: Sarah Rainsberger * Update .changeset/sixty-dogs-sneeze.md Co-authored-by: Sarah Rainsberger * Update packages/astro/src/@types/astro.ts Co-authored-by: Sarah Rainsberger --------- Co-authored-by: Emanuele Stoppa Co-authored-by: Sarah Rainsberger Co-authored-by: Bjorn Lu --- .changeset/sixty-dogs-sneeze.md | 24 ++++ packages/astro/e2e/prefetch.test.js | 103 ++++++++++++++++++ packages/astro/src/@types/astro.ts | 36 ++++++ packages/astro/src/core/config/schema.ts | 5 + packages/astro/src/prefetch/index.ts | 34 +++++- .../src/prefetch/vite-plugin-prefetch.ts | 6 +- 6 files changed, 206 insertions(+), 2 deletions(-) create mode 100644 .changeset/sixty-dogs-sneeze.md diff --git a/.changeset/sixty-dogs-sneeze.md b/.changeset/sixty-dogs-sneeze.md new file mode 100644 index 000000000000..13f89232898d --- /dev/null +++ b/.changeset/sixty-dogs-sneeze.md @@ -0,0 +1,24 @@ +--- +"astro": minor +--- + +Adds an experimental flag `clientPrerender` to prerender your prefetched pages on the client with the [Speculation Rules API](https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API). + +```js +// astro.config.mjs +{ + prefetch: { + prefetchAll: true, + defaultStrategy: 'viewport', + }, + experimental: { + clientPrerender: true, + }, +} +``` + +Enabling this feature overrides the default `prefetch` behavior globally to prerender links on the client according to your `prefetch` configuration. Instead of appending a `` tag to the head of the document or fetching the page with JavaScript, a `