Skip to content

Commit a164c77

Browse files
matthewpflorian-lefebvresarah11918
authored
Support prerendering inside of adapter runtimes (#15077)
* refactor to use buildApp * fix build * remove any casting * refactor: split buildApp into top-level config and post plugin Move environment builds to top-level builder.buildApp for framework ownership, add post plugin with enforce:'post' for manifest injection and page generation. Enables proper coordination with platform plugins. * feat: add AstroPrerenderer interface for adapter-controlled prerendering Adds a new prerenderer API allowing adapters to control how pages are prerendered. Key changes: - Add AstroPrerenderer interface with setup/getStaticPaths/render/teardown - Add setPrerenderer to astro:build:start hook - Create default prerenderer wrapping current Node-based behavior - Refactor generate.ts to use prerenderer interface - Add astro:static-paths virtual module for runtime getStaticPaths * fix: pass allowPrerenderedRoutes=true when matching routes in prerenderer app.match() filters out prerendered routes by default, causing 404s * refactor: StaticPaths class for cleaner prerenderer API - Replace getStaticPaths function with StaticPaths class - Encapsulates RouteCache internally, adapters just pass app - Track pathname→route mapping to avoid route priority issues - Use app.getPathnameFromRequest for base handling - Fall back to app.match for static routes * fix: use pipeline.getComponentByRoute for redirect handling Redirects don't have page modules in pageMap - pipeline method handles redirects and fallbacks properly. * refactor: pass routeData to prerenderer.render() to avoid re-matching getStaticPaths() now returns PathWithRoute[] instead of string[]. render() receives routeData from caller, eliminating path manipulation. * fix: remove unused PathWithRoute import * fix: add prerender conflict detection to generate.ts * fix: lint issues - type import and unused exports * Fix API to not require routeData * don't use own prerender entry when prerenderer defined already * Fix some more tests * Revert render(request) only approach, restore routeData parameter The map-based route lookup added complexity and broke i18n fallback tests. Reverting to pass routeData directly to render() as before. * Get it working * Fix build * fix lint issues * bad merge thing * skipping tests * temp skip netlify * exclude cloudflare for test * Add some logging * Add unhandled rejection logging * Don't run CSS plugin in prerender * Some changes * Fix weird resolution * Fix lint * Allow more process listeners * Add some debug info for handles * Preoptimize static-paths * use the exact module name * Expand when we do this * no js * use the virtual module of course * REmove debugging stuff * Add a test * reorder entryType * Add changesets * more fixes * Update packages/astro/src/core/create-vite.ts Co-authored-by: Florian Lefebvre <contact@florian-lefebvre.dev> * Update .changeset/custom-prerenderer-api.md Co-authored-by: Florian Lefebvre <contact@florian-lefebvre.dev> * Use an options object. * Directly pass Prerenderer * PR comments * Fix build * Reorganize timers and build block * Add astro environment to dev CSS plugin * Add pnpm override to force single vite version (7.3.1) * Move entrypoint files to entrypoint/dev and entrypoint/prod * Refactor prerender endpoints into separate utility module - Create prerender.ts with documented helper functions - Add isPrerender flag to virtual cloudflare config (only true in prerender env) - Guard prerender endpoints so they're only active during build - Use helper functions for cleaner, more readable handler code * Improve error message for prerender server startup failure * Use shared endpoint constants in prerenderer * Improve error message for static paths fetch failure * Add comment explaining previewServer cleanup * Add tests for normalizePathname function * Upgrade to latest cloudflare vite plugin * Remove the override * dedupe * fix types * Move constants into constants file * Fix prerendered styles in cloudflare * Use astro.config for typing * Add back missing i18n fallback code * Add comment explaining why prerender is excluded * Update .changeset/custom-prerenderer-api.md Co-authored-by: Sarah Rainsberger <5098874+sarah11918@users.noreply.github.com> * Add a better comment --------- Co-authored-by: Florian Lefebvre <contact@florian-lefebvre.dev> Co-authored-by: Sarah Rainsberger <5098874+sarah11918@users.noreply.github.com>
1 parent c5ea720 commit a164c77

43 files changed

Lines changed: 1925 additions & 976 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@astrojs/cloudflare': minor
3+
---
4+
5+
Adds support for prerendering pages using the workerd runtime.
6+
7+
The Cloudflare adapter now uses the new `setPrerenderer()` API to prerender pages via HTTP requests to a local preview server running workerd, instead of using Node.js. This ensures prerendered pages are built using the same runtime that serves them in production.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
---
2+
'astro': minor
3+
---
4+
5+
Updates the Integration API to add `setPrerenderer()` to the `astro:build:start` hook, allowing adapters to provide custom prerendering logic.
6+
7+
The new API accepts either an `AstroPrerenderer` object directly, or a factory function that receives the default prerenderer:
8+
9+
```js
10+
'astro:build:start': ({ setPrerenderer }) => {
11+
setPrerenderer((defaultPrerenderer) => ({
12+
name: 'my-prerenderer',
13+
async setup() {
14+
// Optional: called once before prerendering starts
15+
},
16+
async getStaticPaths() {
17+
// Returns array of { pathname: string, route: RouteData }
18+
return defaultPrerenderer.getStaticPaths();
19+
},
20+
async render(request, { routeData }) {
21+
// request: Request
22+
// routeData: RouteData
23+
// Returns: Response
24+
},
25+
async teardown() {
26+
// Optional: called after all pages are prerendered
27+
}
28+
}));
29+
}
30+
```
31+
32+
Also adds the `astro:static-paths` virtual module, which exports a `StaticPaths` class for adapters to collect all prerenderable paths from within their target runtime. This is useful when implementing a custom prerenderer that runs in a non-Node environment:
33+
34+
```js
35+
// In your adapter's request handler (running in target runtime)
36+
import { App } from 'astro/app';
37+
import { StaticPaths } from 'astro:static-paths';
38+
39+
export function createApp(manifest) {
40+
const app = new App(manifest);
41+
42+
return {
43+
async fetch(request) {
44+
const { pathname } = new URL(request.url);
45+
46+
// Expose endpoint for prerenderer to get static paths
47+
if (pathname === '/__astro_static_paths') {
48+
const staticPaths = new StaticPaths(app);
49+
const paths = await staticPaths.getAll();
50+
return new Response(JSON.stringify({ paths }));
51+
}
52+
53+
// Normal request handling
54+
return app.render(request);
55+
}
56+
};
57+
}
58+
```
59+
60+
See the [adapter reference](https://v6.docs.astro.build/en/reference/adapter-reference/#custom-prerenderer) for more details on implementing a custom prerenderer.
61+
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@astrojs/internal-helpers': minor
3+
---
4+
5+
Adds `normalizePathname()` utility function for normalizing URL pathnames to match the canonical form used by route generation.

packages/astro/client.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,10 @@ declare module 'astro:ssr-manifest' {
286286
export const manifest: import('./dist/types/public/internal.js').SSRManifest;
287287
}
288288

289+
declare module 'astro:static-paths' {
290+
export const StaticPaths: typeof import('./dist/runtime/prerender/static-paths.js').StaticPaths;
291+
}
292+
289293
// Everything below are Vite's types (apart from image types, which are in `client.d.ts`)
290294

291295
// CSS modules

packages/astro/dev-only.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,7 @@ declare module 'virtual:astro:dev-css-all' {
7777
import type { ImportedDevStyles } from './src/types/astro.js';
7878
export const devCSSMap: Map<string, () => Promise<{ css: Set<ImportedDevStyles> }>>;
7979
}
80+
81+
declare module 'virtual:astro:app' {
82+
export function createApp(): import('./src/core/app/base.js').BaseApp;
83+
}

packages/astro/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@
5252
"./app": "./dist/core/app/index.js",
5353
"./app/node": "./dist/core/app/node.js",
5454
"./app/entrypoint": "./dist/core/app/entrypoint.js",
55+
"./app/entrypoint/dev": "./dist/core/app/entrypoint/dev.js",
56+
"./app/entrypoint/prod": "./dist/core/app/entrypoint/prod.js",
57+
"./app/manifest": "./dist/core/app/manifest.js",
5558
"./entrypoints/prerender": "./dist/entrypoints/prerender.js",
5659
"./entrypoints/legacy": "./dist/entrypoints/legacy.js",
5760
"./client/*": "./dist/runtime/client/*",
Lines changed: 1 addition & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1 @@
1-
import { manifest } from 'virtual:astro:manifest';
2-
import { App } from './app.js';
3-
import type { BaseApp } from './base.js';
4-
import { DevApp } from './dev/app.js';
5-
import { createConsoleLogger } from './logging.js';
6-
import type { RouteInfo } from './types.js';
7-
import type { RoutesList } from '../../types/astro.js';
8-
9-
let currentDevApp: DevApp | null = null;
10-
11-
export function createApp(dev = import.meta.env.DEV): BaseApp {
12-
if (dev) {
13-
const logger = createConsoleLogger(manifest.logLevel);
14-
currentDevApp = new DevApp(manifest, true, logger);
15-
16-
// Listen for route updates via HMR
17-
if (import.meta.hot) {
18-
import.meta.hot.on('astro:routes-updated', async () => {
19-
if (!currentDevApp) return;
20-
try {
21-
// Re-import the routes module to get fresh routes
22-
const { routes: newRoutes } = await import('virtual:astro:routes');
23-
const newRoutesList: RoutesList = {
24-
routes: newRoutes.map((r: RouteInfo) => r.routeData),
25-
};
26-
currentDevApp.updateRoutes(newRoutesList);
27-
} catch (e: any) {
28-
// Log error but don't crash - route updates are non-critical
29-
logger.error('router', `Failed to update routes via HMR:\n ${e}`);
30-
}
31-
});
32-
}
33-
34-
return currentDevApp;
35-
} else {
36-
return new App(manifest);
37-
}
38-
}
1+
export { createApp } from 'virtual:astro:app';
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { manifest } from 'virtual:astro:manifest';
2+
import type { BaseApp } from '../base.js';
3+
import { DevApp } from '../dev/app.js';
4+
import { createConsoleLogger } from '../logging.js';
5+
import type { RouteInfo } from '../types.js';
6+
import type { RoutesList } from '../../../types/astro.js';
7+
8+
let currentDevApp: DevApp | null = null;
9+
10+
export function createApp(): BaseApp {
11+
const logger = createConsoleLogger(manifest.logLevel);
12+
currentDevApp = new DevApp(manifest, true, logger);
13+
14+
// Listen for route updates via HMR
15+
if (import.meta.hot) {
16+
import.meta.hot.on('astro:routes-updated', async () => {
17+
if (!currentDevApp) return;
18+
try {
19+
// Re-import the routes module to get fresh routes
20+
const { routes: newRoutes } = await import('virtual:astro:routes');
21+
const newRoutesList: RoutesList = {
22+
routes: newRoutes.map((r: RouteInfo) => r.routeData),
23+
};
24+
currentDevApp.updateRoutes(newRoutesList);
25+
} catch (e: any) {
26+
// Log error but don't crash - route updates are non-critical
27+
logger.error('router', `Failed to update routes via HMR:\n ${e}`);
28+
}
29+
});
30+
}
31+
32+
return currentDevApp;
33+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { manifest } from 'virtual:astro:manifest';
2+
import type { BaseApp } from '../base.js';
3+
import { App } from '../app.js';
4+
5+
export function createApp(): BaseApp {
6+
return new App(manifest);
7+
}

packages/astro/src/core/app/manifest.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import type {
1010
SerializedRouteInfo,
1111
} from './types.js';
1212

13+
export type { SerializedRouteData } from '../../types/astro.js';
14+
1315
export function deserializeManifest(
1416
serializedManifest: SerializedSSRManifest,
1517
routesList?: RoutesList,

0 commit comments

Comments
 (0)