astro
Version:
Astro is a modern site builder with web best practices, performance, and DX front-of-mind.
108 lines (107 loc) • 4.99 kB
JavaScript
import { isRoute404, isRoute500 } from "../../../core/routing/internal/route-errors.js";
import { isPropagatingHint } from "../../../core/head-propagation/resolver.js";
import { renderToAsyncIterable, renderToReadableStream, renderToString } from "./astro/render.js";
import { encoder } from "./common.js";
import { renderComponentToString } from "./component.js";
import { renderCspContent } from "./csp.js";
import { isDeno, isNode, isWorkerd } from "./util.js";
import { isAstroComponentFactory } from "./astro/factory.js";
export async function renderPage(result, componentFactory, props, children, streaming, route) {
if (!isAstroComponentFactory(componentFactory)) {
const nonAstroMeta = result.componentMetadata.get(componentFactory.moduleId);
result._metadata.headInTree = nonAstroMeta?.containsHead ?? false;
result._metadata.routeHasPropagation = isPropagatingHint(nonAstroMeta?.propagation ?? "none");
const pageProps = {
...props ?? {},
"server:root": true
};
// Non-Astro page components (MDX, `.html`, and raw framework components
// rendered through the Container API) go through `renderComponentToString`,
// which dispatches to the correct renderer: the `astro:jsx` renderer for
// MDX (which also wraps runtime errors with a helpful hint and streams the
// content), the HTML renderer for `.html`, and framework renderers for
// components. It also injects the `<head>` for layout-less MDX pages.
const str = await renderComponentToString(result, componentFactory.name, componentFactory, pageProps, {}, true, route);
const bytes = encoder.encode(str);
const headers = new Headers([["Content-Type", "text/html"], ["Content-Length", bytes.byteLength.toString()]]);
if (result.shouldInjectCspMetaTags && (result.cspDestination === "header" || result.cspDestination === "adapter")) {
headers.set("content-security-policy", renderCspContent(result));
}
return new Response(bytes, {
headers,
status: result.response.status
});
}
// Mark if this page component contains a <head> within its tree. If it does
// We avoid implicit head injection entirely.
const pageMeta = result.componentMetadata.get(componentFactory.moduleId);
result._metadata.headInTree = pageMeta?.containsHead ?? false;
// Only routes on a propagation path need to await async slot pre-renders
// before flushing the head (see `collectPropagatedHeadParts`). Other routes
// keep streaming without blocking the head on unrelated markup `await`s.
result._metadata.routeHasPropagation = isPropagatingHint(pageMeta?.propagation ?? "none");
let body;
if (streaming) {
// isNode is true in Deno's and workerd's node-compat modes, but their
// Response constructors do not accept AsyncIterable bodies (a non-standard
// Node.js extension). Deno falls back to ReadableStream entirely; workerd
// keeps the faster AsyncIterable render path and wraps the result with
// ReadableStream.from() so the Response constructor receives a standard type.
if (isNode && !isDeno) {
const nodeBody = await renderToAsyncIterable(result, componentFactory, props, children, true, route);
if (isWorkerd && !(nodeBody instanceof Response)) {
// ReadableStream.from() is available in Node >= 20.6 and workerd but
// is not yet in TypeScript's built-in DOM lib types.
body = ReadableStream.from(nodeBody);
} else {
// Node.js allows passing in an AsyncIterable to the Response constructor.
// This is non-standard so using `any` here to preserve types everywhere else.
body = nodeBody;
}
} else {
body = await renderToReadableStream(result, componentFactory, props, children, true, route);
}
} else {
body = await renderToString(result, componentFactory, props, children, true, route);
}
// If the Astro component returns a Response on init, return that response
if (body instanceof Response) return body;
// Create final response from body
const init = result.response;
const headers = new Headers(init.headers);
if (result.shouldInjectCspMetaTags && result.cspDestination === "header" || result.cspDestination === "adapter") {
headers.set("content-security-policy", renderCspContent(result));
}
// For non-streaming, convert string to byte array to calculate Content-Length
if (!streaming && typeof body === "string") {
body = encoder.encode(body);
headers.set("Content-Length", body.byteLength.toString());
}
let status = init.status;
let statusText = init.statusText;
// Custom root 404.astro and 500.astro routes must return fixed status codes.
if (route?.route && isRoute404(route.route)) {
status = 404;
if (statusText === "OK") {
statusText = "Not Found";
}
} else if (route?.route && isRoute500(route.route)) {
status = 500;
if (statusText === "OK") {
statusText = "Internal Server Error";
}
}
if (status) {
return new Response(body, {
...init,
headers,
status,
statusText
});
} else {
return new Response(body, {
...init,
headers
});
}
}