Files
Tiber365/node_modules/astro/dist/core/render-context.js
2025-07-24 18:46:24 +02:00

689 lines
24 KiB
JavaScript

import { green } from "kleur/colors";
import { getActionContext } from "../actions/runtime/virtual/server.js";
import { deserializeActionResult } from "../actions/runtime/virtual/shared.js";
import { createCallAction, createGetActionResult, hasActionPayload } from "../actions/utils.js";
import {
computeCurrentLocale,
computePreferredLocale,
computePreferredLocaleList
} from "../i18n/utils.js";
import { renderEndpoint } from "../runtime/server/endpoint.js";
import { renderPage } from "../runtime/server/index.js";
import {
ASTRO_VERSION,
clientAddressSymbol,
REROUTE_DIRECTIVE_HEADER,
REWRITE_DIRECTIVE_HEADER_KEY,
REWRITE_DIRECTIVE_HEADER_VALUE,
ROUTE_TYPE_HEADER,
responseSentSymbol
} from "./constants.js";
import { AstroCookies, attachCookiesToResponse } from "./cookies/index.js";
import { getCookiesFromResponse } from "./cookies/response.js";
import { generateCspDigest } from "./encryption.js";
import { CspNotEnabled, ForbiddenRewrite } from "./errors/errors-data.js";
import { AstroError, AstroErrorData } from "./errors/index.js";
import { callMiddleware } from "./middleware/callMiddleware.js";
import { sequence } from "./middleware/index.js";
import { renderRedirect } from "./redirects/render.js";
import { getParams, getProps, Slots } from "./render/index.js";
import { isRoute404or500, isRouteExternalRedirect, isRouteServerIsland } from "./routing/match.js";
import { copyRequest, getOriginPathname, setOriginPathname } from "./routing/rewrite.js";
import { AstroSession } from "./session.js";
const apiContextRoutesSymbol = Symbol.for("context.routes");
class RenderContext {
constructor(pipeline, locals, middleware, actions, pathname, request, routeData, status, clientAddress, cookies = new AstroCookies(request), params = getParams(routeData, pathname), url = new URL(request.url), props = {}, partial = void 0, session = pipeline.manifest.sessionConfig ? new AstroSession(cookies, pipeline.manifest.sessionConfig, pipeline.runtimeMode) : void 0) {
this.pipeline = pipeline;
this.locals = locals;
this.middleware = middleware;
this.actions = actions;
this.pathname = pathname;
this.request = request;
this.routeData = routeData;
this.status = status;
this.clientAddress = clientAddress;
this.cookies = cookies;
this.params = params;
this.url = url;
this.props = props;
this.partial = partial;
this.session = session;
}
/**
* A flag that tells the render content if the rewriting was triggered
*/
isRewriting = false;
/**
* A safety net in case of loops
*/
counter = 0;
result = void 0;
static async create({
locals = {},
middleware,
pathname,
pipeline,
request,
routeData,
clientAddress,
status = 200,
props,
partial = void 0,
actions
}) {
const pipelineMiddleware = await pipeline.getMiddleware();
const pipelineActions = actions ?? await pipeline.getActions();
setOriginPathname(
request,
pathname,
pipeline.manifest.trailingSlash,
pipeline.manifest.buildFormat
);
return new RenderContext(
pipeline,
locals,
sequence(...pipeline.internalMiddleware, middleware ?? pipelineMiddleware),
pipelineActions,
pathname,
request,
routeData,
status,
clientAddress,
void 0,
void 0,
void 0,
props,
partial
);
}
/**
* The main function of the RenderContext.
*
* Use this function to render any route known to Astro.
* It attempts to render a route. A route can be a:
*
* - page
* - redirect
* - endpoint
* - fallback
*/
async render(componentInstance, slots = {}) {
const { cookies, middleware, pipeline } = this;
const { logger, serverLike, streaming, manifest } = pipeline;
const props = Object.keys(this.props).length > 0 ? this.props : await getProps({
mod: componentInstance,
routeData: this.routeData,
routeCache: this.pipeline.routeCache,
pathname: this.pathname,
logger,
serverLike,
base: manifest.base
});
const actionApiContext = this.createActionAPIContext();
const apiContext = this.createAPIContext(props, actionApiContext);
this.counter++;
if (this.counter === 4) {
return new Response("Loop Detected", {
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/508
status: 508,
statusText: "Astro detected a loop where you tried to call the rewriting logic more than four times."
});
}
const lastNext = async (ctx, payload) => {
if (payload) {
const oldPathname = this.pathname;
pipeline.logger.debug("router", "Called rewriting to:", payload);
const {
routeData,
componentInstance: newComponent,
pathname,
newUrl
} = await pipeline.tryRewrite(payload, this.request);
if (this.pipeline.serverLike === true && this.routeData.prerender === false && routeData.prerender === true) {
throw new AstroError({
...ForbiddenRewrite,
message: ForbiddenRewrite.message(this.pathname, pathname, routeData.component),
hint: ForbiddenRewrite.hint(routeData.component)
});
}
this.routeData = routeData;
componentInstance = newComponent;
if (payload instanceof Request) {
this.request = payload;
} else {
this.request = copyRequest(
newUrl,
this.request,
// need to send the flag of the previous routeData
routeData.prerender,
this.pipeline.logger,
this.routeData.route
);
}
this.isRewriting = true;
this.url = new URL(this.request.url);
this.params = getParams(routeData, pathname);
this.pathname = pathname;
this.status = 200;
setOriginPathname(
this.request,
oldPathname,
this.pipeline.manifest.trailingSlash,
this.pipeline.manifest.buildFormat
);
}
let response2;
if (!ctx.isPrerendered) {
const { action, setActionResult, serializeActionResult } = getActionContext(ctx);
if (action?.calledFrom === "form") {
const actionResult = await action.handler();
setActionResult(action.name, serializeActionResult(actionResult));
}
}
switch (this.routeData.type) {
case "endpoint": {
response2 = await renderEndpoint(
componentInstance,
ctx,
this.routeData.prerender,
logger
);
break;
}
case "redirect":
return renderRedirect(this);
case "page": {
this.result = await this.createResult(componentInstance, actionApiContext);
try {
response2 = await renderPage(
this.result,
componentInstance?.default,
props,
slots,
streaming,
this.routeData
);
} catch (e) {
this.result.cancelled = true;
throw e;
}
response2.headers.set(ROUTE_TYPE_HEADER, "page");
if (this.routeData.route === "/404" || this.routeData.route === "/500") {
response2.headers.set(REROUTE_DIRECTIVE_HEADER, "no");
}
if (this.isRewriting) {
response2.headers.set(REWRITE_DIRECTIVE_HEADER_KEY, REWRITE_DIRECTIVE_HEADER_VALUE);
}
break;
}
case "fallback": {
return new Response(null, { status: 500, headers: { [ROUTE_TYPE_HEADER]: "fallback" } });
}
}
const responseCookies = getCookiesFromResponse(response2);
if (responseCookies) {
cookies.merge(responseCookies);
}
return response2;
};
if (isRouteExternalRedirect(this.routeData)) {
return renderRedirect(this);
}
const response = await callMiddleware(middleware, apiContext, lastNext);
if (response.headers.get(ROUTE_TYPE_HEADER)) {
response.headers.delete(ROUTE_TYPE_HEADER);
}
attachCookiesToResponse(response, cookies);
return response;
}
createAPIContext(props, context) {
const redirect = (path, status = 302) => new Response(null, { status, headers: { Location: path } });
Reflect.set(context, apiContextRoutesSymbol, this.pipeline);
return Object.assign(context, {
props,
redirect,
getActionResult: createGetActionResult(context.locals),
callAction: createCallAction(context)
});
}
async #executeRewrite(reroutePayload) {
this.pipeline.logger.debug("router", "Calling rewrite: ", reroutePayload);
const oldPathname = this.pathname;
const { routeData, componentInstance, newUrl, pathname } = await this.pipeline.tryRewrite(
reroutePayload,
this.request
);
const isI18nFallback = routeData.fallbackRoutes && routeData.fallbackRoutes.length > 0;
if (this.pipeline.serverLike && !this.routeData.prerender && routeData.prerender && !isI18nFallback) {
throw new AstroError({
...ForbiddenRewrite,
message: ForbiddenRewrite.message(this.pathname, pathname, routeData.component),
hint: ForbiddenRewrite.hint(routeData.component)
});
}
this.routeData = routeData;
if (reroutePayload instanceof Request) {
this.request = reroutePayload;
} else {
this.request = copyRequest(
newUrl,
this.request,
// need to send the flag of the previous routeData
routeData.prerender,
this.pipeline.logger,
this.routeData.route
);
}
this.url = new URL(this.request.url);
this.cookies = new AstroCookies(this.request);
this.params = getParams(routeData, pathname);
this.pathname = pathname;
this.isRewriting = true;
this.status = 200;
setOriginPathname(
this.request,
oldPathname,
this.pipeline.manifest.trailingSlash,
this.pipeline.manifest.buildFormat
);
return await this.render(componentInstance);
}
createActionAPIContext() {
const renderContext = this;
const { cookies, params, pipeline, url } = this;
const generator = `Astro v${ASTRO_VERSION}`;
const rewrite = async (reroutePayload) => {
return await this.#executeRewrite(reroutePayload);
};
return {
cookies,
routePattern: this.routeData.route,
isPrerendered: this.routeData.prerender,
get clientAddress() {
return renderContext.getClientAddress();
},
get currentLocale() {
return renderContext.computeCurrentLocale();
},
generator,
get locals() {
return renderContext.locals;
},
set locals(_) {
throw new AstroError(AstroErrorData.LocalsReassigned);
},
params,
get preferredLocale() {
return renderContext.computePreferredLocale();
},
get preferredLocaleList() {
return renderContext.computePreferredLocaleList();
},
rewrite,
request: this.request,
site: pipeline.site,
url,
get originPathname() {
return getOriginPathname(renderContext.request);
},
get session() {
if (this.isPrerendered) {
pipeline.logger.warn(
"session",
`context.session was used when rendering the route ${green(this.routePattern)}, but it is not available on prerendered routes. If you need access to sessions, make sure that the route is server-rendered using \`export const prerender = false;\` or by setting \`output\` to \`"server"\` in your Astro config to make all your routes server-rendered by default. For more information, see https://docs.astro.build/en/guides/sessions/`
);
return void 0;
}
if (!renderContext.session) {
pipeline.logger.warn(
"session",
`context.session was used when rendering the route ${green(this.routePattern)}, but no storage configuration was provided. Either configure the storage manually or use an adapter that provides session storage. For more information, see https://docs.astro.build/en/guides/sessions/`
);
return void 0;
}
return renderContext.session;
},
insertDirective(payload) {
if (!pipeline.manifest.csp) {
throw new AstroError(CspNotEnabled);
}
renderContext.result?.directives.push(payload);
},
insertScriptResource(resource) {
if (!pipeline.manifest.csp) {
throw new AstroError(CspNotEnabled);
}
renderContext.result?.scriptResources.push(resource);
},
insertStyleResource(resource) {
if (!pipeline.manifest.csp) {
throw new AstroError(CspNotEnabled);
}
renderContext.result?.styleResources.push(resource);
},
insertStyleHash(hash) {
if (!pipeline.manifest.csp) {
throw new AstroError(CspNotEnabled);
}
renderContext.result?.styleHashes.push(hash);
},
insertScriptHash(hash) {
if (!!pipeline.manifest.csp === false) {
throw new AstroError(CspNotEnabled);
}
renderContext.result?.scriptHashes.push(hash);
}
};
}
async createResult(mod, ctx) {
const { cookies, pathname, pipeline, routeData, status } = this;
const { clientDirectives, inlinedScripts, compressHTML, manifest, renderers, resolve } = pipeline;
const { links, scripts, styles } = await pipeline.headElements(routeData);
const extraStyleHashes = [];
const extraScriptHashes = [];
const shouldInjectCspMetaTags = !!manifest.csp;
const cspAlgorithm = manifest.csp?.algorithm ?? "SHA-256";
if (shouldInjectCspMetaTags) {
for (const style of styles) {
extraStyleHashes.push(await generateCspDigest(style.children, cspAlgorithm));
}
for (const script of scripts) {
extraScriptHashes.push(await generateCspDigest(script.children, cspAlgorithm));
}
}
const componentMetadata = await pipeline.componentMetadata(routeData) ?? manifest.componentMetadata;
const headers = new Headers({ "Content-Type": "text/html" });
const partial = typeof this.partial === "boolean" ? this.partial : Boolean(mod.partial);
const actionResult = hasActionPayload(this.locals) ? deserializeActionResult(this.locals._actionPayload.actionResult) : void 0;
const response = {
status: actionResult?.error ? actionResult?.error.status : status,
statusText: actionResult?.error ? actionResult?.error.type : "OK",
get headers() {
return headers;
},
// Disallow `Astro.response.headers = new Headers`
set headers(_) {
throw new AstroError(AstroErrorData.AstroResponseHeadersReassigned);
}
};
const result = {
base: manifest.base,
userAssetsBase: manifest.userAssetsBase,
cancelled: false,
clientDirectives,
inlinedScripts,
componentMetadata,
compressHTML,
cookies,
/** This function returns the `Astro` faux-global */
createAstro: (astroGlobal, props, slots) => this.createAstro(result, astroGlobal, props, slots, ctx),
links,
params: this.params,
partial,
pathname,
renderers,
resolve,
response,
request: this.request,
scripts,
styles,
actionResult,
serverIslandNameMap: manifest.serverIslandNameMap ?? /* @__PURE__ */ new Map(),
key: manifest.key,
trailingSlash: manifest.trailingSlash,
_metadata: {
hasHydrationScript: false,
rendererSpecificHydrationScripts: /* @__PURE__ */ new Set(),
hasRenderedHead: false,
renderedScripts: /* @__PURE__ */ new Set(),
hasDirectives: /* @__PURE__ */ new Set(),
hasRenderedServerIslandRuntime: false,
headInTree: false,
extraHead: [],
extraStyleHashes,
extraScriptHashes,
propagators: /* @__PURE__ */ new Set()
},
cspDestination: manifest.csp?.cspDestination ?? (routeData.prerender ? "meta" : "header"),
shouldInjectCspMetaTags,
cspAlgorithm,
// The following arrays must be cloned, otherwise they become mutable across routes.
scriptHashes: manifest.csp?.scriptHashes ? [...manifest.csp.scriptHashes] : [],
scriptResources: manifest.csp?.scriptResources ? [...manifest.csp.scriptResources] : [],
styleHashes: manifest.csp?.styleHashes ? [...manifest.csp.styleHashes] : [],
styleResources: manifest.csp?.styleResources ? [...manifest.csp.styleResources] : [],
directives: manifest.csp?.directives ? [...manifest.csp.directives] : [],
isStrictDynamic: manifest.csp?.isStrictDynamic ?? false
};
return result;
}
#astroPagePartial;
/**
* The Astro global is sourced in 3 different phases:
* - **Static**: `.generator` and `.glob` is printed by the compiler, instantiated once per process per astro file
* - **Page-level**: `.request`, `.cookies`, `.locals` etc. These remain the same for the duration of the request.
* - **Component-level**: `.props`, `.slots`, and `.self` are unique to each _use_ of each component.
*
* The page level partial is used as the prototype of the user-visible `Astro` global object, which is instantiated once per use of a component.
*/
createAstro(result, astroStaticPartial, props, slotValues, apiContext) {
let astroPagePartial;
if (this.isRewriting) {
astroPagePartial = this.#astroPagePartial = this.createAstroPagePartial(
result,
astroStaticPartial,
apiContext
);
} else {
astroPagePartial = this.#astroPagePartial ??= this.createAstroPagePartial(
result,
astroStaticPartial,
apiContext
);
}
const astroComponentPartial = { props, self: null };
const Astro = Object.assign(
Object.create(astroPagePartial),
astroComponentPartial
);
let _slots;
Object.defineProperty(Astro, "slots", {
get: () => {
if (!_slots) {
_slots = new Slots(
result,
slotValues,
this.pipeline.logger
);
}
return _slots;
}
});
return Astro;
}
createAstroPagePartial(result, astroStaticPartial, apiContext) {
const renderContext = this;
const { cookies, locals, params, pipeline, url } = this;
const { response } = result;
const redirect = (path, status = 302) => {
if (this.request[responseSentSymbol]) {
throw new AstroError({
...AstroErrorData.ResponseSentError
});
}
return new Response(null, { status, headers: { Location: path } });
};
const rewrite = async (reroutePayload) => {
return await this.#executeRewrite(reroutePayload);
};
const callAction = createCallAction(apiContext);
return {
generator: astroStaticPartial.generator,
glob: astroStaticPartial.glob,
routePattern: this.routeData.route,
isPrerendered: this.routeData.prerender,
cookies,
get session() {
if (this.isPrerendered) {
pipeline.logger.warn(
"session",
`Astro.session was used when rendering the route ${green(this.routePattern)}, but it is not available on prerendered pages. If you need access to sessions, make sure that the page is server-rendered using \`export const prerender = false;\` or by setting \`output\` to \`"server"\` in your Astro config to make all your pages server-rendered by default. For more information, see https://docs.astro.build/en/guides/sessions/`
);
return void 0;
}
if (!renderContext.session) {
pipeline.logger.warn(
"session",
`Astro.session was used when rendering the route ${green(this.routePattern)}, but no storage configuration was provided. Either configure the storage manually or use an adapter that provides session storage. For more information, see https://docs.astro.build/en/guides/sessions/`
);
return void 0;
}
return renderContext.session;
},
get clientAddress() {
return renderContext.getClientAddress();
},
get currentLocale() {
return renderContext.computeCurrentLocale();
},
params,
get preferredLocale() {
return renderContext.computePreferredLocale();
},
get preferredLocaleList() {
return renderContext.computePreferredLocaleList();
},
locals,
redirect,
rewrite,
request: this.request,
response,
site: pipeline.site,
getActionResult: createGetActionResult(locals),
get callAction() {
return callAction;
},
url,
get originPathname() {
return getOriginPathname(renderContext.request);
},
insertDirective(payload) {
if (!pipeline.manifest.csp) {
throw new AstroError(CspNotEnabled);
}
renderContext.result?.directives.push(payload);
},
insertScriptResource(resource) {
if (!pipeline.manifest.csp) {
throw new AstroError(CspNotEnabled);
}
renderContext.result?.scriptResources.push(resource);
},
insertStyleResource(resource) {
if (!pipeline.manifest.csp) {
throw new AstroError(CspNotEnabled);
}
renderContext.result?.styleResources.push(resource);
},
insertStyleHash(hash) {
if (!pipeline.manifest.csp) {
throw new AstroError(CspNotEnabled);
}
renderContext.result?.styleHashes.push(hash);
},
insertScriptHash(hash) {
if (!!pipeline.manifest.csp === false) {
throw new AstroError(CspNotEnabled);
}
renderContext.result?.scriptHashes.push(hash);
}
};
}
getClientAddress() {
const { pipeline, request, routeData, clientAddress } = this;
if (routeData.prerender) {
throw new AstroError({
...AstroErrorData.PrerenderClientAddressNotAvailable,
message: AstroErrorData.PrerenderClientAddressNotAvailable.message(routeData.component)
});
}
if (clientAddress) {
return clientAddress;
}
if (clientAddressSymbol in request) {
return Reflect.get(request, clientAddressSymbol);
}
if (pipeline.adapterName) {
throw new AstroError({
...AstroErrorData.ClientAddressNotAvailable,
message: AstroErrorData.ClientAddressNotAvailable.message(pipeline.adapterName)
});
}
throw new AstroError(AstroErrorData.StaticClientAddressNotAvailable);
}
/**
* API Context may be created multiple times per request, i18n data needs to be computed only once.
* So, it is computed and saved here on creation of the first APIContext and reused for later ones.
*/
#currentLocale;
computeCurrentLocale() {
const {
url,
pipeline: { i18n },
routeData
} = this;
if (!i18n) return;
const { defaultLocale, locales, strategy } = i18n;
const fallbackTo = strategy === "pathname-prefix-other-locales" || strategy === "domains-prefix-other-locales" ? defaultLocale : void 0;
if (this.#currentLocale) {
return this.#currentLocale;
}
let computedLocale;
if (isRouteServerIsland(routeData)) {
let referer = this.request.headers.get("referer");
if (referer) {
if (URL.canParse(referer)) {
referer = new URL(referer).pathname;
}
computedLocale = computeCurrentLocale(referer, locales, defaultLocale);
}
} else {
let pathname = routeData.pathname;
if (!routeData.pattern.test(url.pathname)) {
for (const fallbackRoute of routeData.fallbackRoutes) {
if (fallbackRoute.pattern.test(url.pathname)) {
pathname = fallbackRoute.pathname;
break;
}
}
}
pathname = pathname && !isRoute404or500(routeData) ? pathname : url.pathname;
computedLocale = computeCurrentLocale(pathname, locales, defaultLocale);
}
this.#currentLocale = computedLocale ?? fallbackTo;
return this.#currentLocale;
}
#preferredLocale;
computePreferredLocale() {
const {
pipeline: { i18n },
request
} = this;
if (!i18n) return;
return this.#preferredLocale ??= computePreferredLocale(request, i18n.locales);
}
#preferredLocaleList;
computePreferredLocaleList() {
const {
pipeline: { i18n },
request
} = this;
if (!i18n) return;
return this.#preferredLocaleList ??= computePreferredLocaleList(request, i18n.locales);
}
}
export {
RenderContext,
apiContextRoutesSymbol
};