1// Alchemy modifications are licensed under Apache-2.0.
2// This file includes third-party code; see /THIRD_PARTY_LICENSES.md.
3/**
4 * Vendored from `@astrojs/cloudflare` v14.1.3 (`src/utils/prerender.ts`).
5 *
6 * Prerender utilities for the Cloudflare adapter. These endpoints are only
7 * active during the (workerd) prerender build phase and are not available in
8 * production or development. This package hardwires node prerendering
9 * (`prerenderEnvironment: 'node'`), so they are effectively dormant, but the
10 * handler keeps the code path so a workerd prerenderer can be reintroduced
11 * without touching the runtime.
12 */
13import type { BaseApp, RenderErrorOptions } from "astro/app";
14import { deserializeRouteData, serializeRouteData } from "astro/app/manifest";
15import { StaticPaths } from "astro:static-paths";
16import type {
17 PrerenderRequest,
18 SerializedStaticImageEntry,
19 StaticImagesResponse,
20 StaticPathsResponse,
21} from "../prerender-types.ts";
22import {
23 PRERENDER_ENDPOINT,
24 STATIC_IMAGES_ENDPOINT,
25 STATIC_PATHS_ENDPOINT,
26} from "./prerender-constants.ts";
27
28/**
29 * Replicates core's `BuildErrorHandler` semantics on the worker app during
30 * the prerender phase.
31 */
32export function installPrerenderErrorPropagation(app: BaseApp): void {
33 const originalRenderError = app.renderError.bind(app);
34 app.renderError = async (
35 request: Request,
36 options: RenderErrorOptions,
37 ): Promise<Response> => {
38 if (options.status === 500) {
39 if (options.response) {
40 return options.response;
41 }
42 throw options.error;
43 }
44 return originalRenderError(request, options);
45 };
46}
47
48/**
49 * Checks if the request is for the static paths prerender endpoint.
50 */
51export function isStaticPathsRequest(request: Request): boolean {
52 const { pathname } = new URL(request.url);
53 return pathname === STATIC_PATHS_ENDPOINT && request.method === "POST";
54}
55
56/**
57 * Checks if the request is for the prerender endpoint.
58 */
59export function isPrerenderRequest(request: Request): boolean {
60 const { pathname } = new URL(request.url);
61 return pathname === PRERENDER_ENDPOINT && request.method === "POST";
62}
63
64/**
65 * Handles the static paths request, returning all paths that need prerendering.
66 */
67export async function handleStaticPathsRequest(
68 app: BaseApp,
69): Promise<Response> {
70 const staticPaths = new StaticPaths(app);
71 const paths = await staticPaths.getAll();
72 const response: StaticPathsResponse = {
73 paths: paths.map(({ pathname, route }) => ({
74 pathname,
75 route: serializeRouteData(route, app.manifest.trailingSlash),
76 })),
77 };
78 return new Response(JSON.stringify(response), {
79 headers: { "Content-Type": "application/json" },
80 });
81}
82
83/**
84 * Handles a prerender request, rendering the specified page. The response
85 * body is fully buffered so streaming errors surface as a 500.
86 */
87export async function handlePrerenderRequest(
88 app: BaseApp,
89 request: Request,
90): Promise<Response> {
91 const headers = new Headers();
92 for (const [key, value] of request.headers.entries()) {
93 headers.append(key, value);
94 }
95 const body: PrerenderRequest = await request.json();
96 const routeData = deserializeRouteData(body.routeData);
97 const prerenderRequest = new Request(body.url, {
98 method: "GET",
99 headers,
100 });
101 // Buffer the full body to catch streaming errors before the HTTP layer
102 // commits a 200 status.
103 try {
104 const response = await app.render(prerenderRequest, { routeData });
105 const bufferedBody = await response.arrayBuffer();
106 return new Response(bufferedBody, {
107 status: response.status,
108 statusText: response.statusText,
109 headers: response.headers,
110 });
111 } catch (err: unknown) {
112 const message = err instanceof Error ? err.message : String(err);
113 return new Response(message, {
114 status: 500,
115 headers: {
116 "Content-Type": "text/plain",
117 "x-astro-prerender-error": message,
118 },
119 });
120 }
121}
122
123export function isStaticImagesRequest(request: Request): boolean {
124 const { pathname } = new URL(request.url);
125 return pathname === STATIC_IMAGES_ENDPOINT && request.method === "POST";
126}
127
128/** Serializes the global staticImages map collected in workerd back to the Node-side build. */
129export function handleStaticImagesRequest(): Response {
130 const staticImages = globalThis.astroAsset?.staticImages;
131 if (!staticImages || staticImages.size === 0) {
132 return new Response("[]", {
133 headers: { "Content-Type": "application/json" },
134 });
135 }
136
137 const entries: StaticImagesResponse = [];
138 for (const [originalPath, { originalSrcPath, transforms }] of staticImages) {
139 const serializedTransforms: SerializedStaticImageEntry["transforms"] = [];
140 for (const [hash, { finalPath, transform }] of transforms) {
141 serializedTransforms.push({
142 hash,
143 finalPath,
144 transform: transform as Record<string, any>,
145 });
146 }
147 entries.push({
148 originalPath,
149 originalSrcPath,
150 transforms: serializedTransforms,
151 });
152 }
153
154 return new Response(JSON.stringify(entries), {
155 headers: { "Content-Type": "application/json" },
156 });
157}