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, REROUTE_DIRECTIVE_HEADER, ROUTE_TYPE_HEADER, clientAddressSymbol, clientLocalsSymbol, responseSentSymbol } from "./constants.js"; import { AstroCookies, attachCookiesToResponse } from "./cookies/index.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 { Slots, getParams, getProps } from "./render/index.js"; class RenderContext { constructor(pipeline, locals, middleware, pathname, request, routeData, status, cookies = new AstroCookies(request), params = getParams(routeData, pathname), url = new URL(request.url)) { this.pipeline = pipeline; this.locals = locals; this.middleware = middleware; this.pathname = pathname; this.request = request; this.routeData = routeData; this.status = status; this.cookies = cookies; this.params = params; this.url = url; } static create({ locals = {}, middleware, pathname, pipeline, request, routeData, status = 200 }) { return new RenderContext( pipeline, locals, sequence(...pipeline.internalMiddleware, middleware ?? pipeline.middleware), pathname, request, routeData, status ); } /** * 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) { const { cookies, middleware, pathname, pipeline, routeData } = this; const { logger, routeCache, serverLike, streaming } = pipeline; const props = await getProps({ mod: componentInstance, routeData, routeCache, pathname, logger, serverLike }); const apiContext = this.createAPIContext(props); const lastNext = async () => { switch (routeData.type) { case "endpoint": return renderEndpoint(componentInstance, apiContext, serverLike, logger); case "redirect": return renderRedirect(this); case "page": { const result = await this.createResult(componentInstance); let response2; try { response2 = await renderPage( result, componentInstance?.default, props, {}, streaming, routeData ); } catch (e) { result.cancelled = true; throw e; } response2.headers.set(ROUTE_TYPE_HEADER, "page"); if (routeData.route === "/404" || routeData.route === "/500") { response2.headers.set(REROUTE_DIRECTIVE_HEADER, "no"); } return response2; } case "fallback": { return new Response(null, { status: 500, headers: { [ROUTE_TYPE_HEADER]: "fallback" } }); } } }; 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) { const renderContext = this; const { cookies, params, pipeline, request, url } = this; const generator = `Astro v${ASTRO_VERSION}`; const redirect = (path, status = 302) => new Response(null, { status, headers: { Location: path } }); return { cookies, get clientAddress() { return renderContext.clientAddress(); }, get currentLocale() { return renderContext.computeCurrentLocale(); }, generator, get locals() { return renderContext.locals; }, // TODO(breaking): disallow replacing the locals object set locals(val) { if (typeof val !== "object") { throw new AstroError(AstroErrorData.LocalsNotAnObject); } else { renderContext.locals = val; Reflect.set(request, clientLocalsSymbol, val); } }, params, get preferredLocale() { return renderContext.computePreferredLocale(); }, get preferredLocaleList() { return renderContext.computePreferredLocaleList(); }, props, redirect, request, site: pipeline.site, url }; } async createResult(mod) { const { cookies, pathname, pipeline, routeData, status } = this; const { clientDirectives, inlinedScripts, compressHTML, manifest, renderers, resolve } = pipeline; const { links, scripts, styles } = await pipeline.headElements(routeData); const componentMetadata = await pipeline.componentMetadata(routeData) ?? manifest.componentMetadata; const headers = new Headers({ "Content-Type": "text/html" }); const partial = Boolean(mod.partial); const response = { status, statusText: "OK", get headers() { return headers; }, // Disallow `Astro.response.headers = new Headers` set headers(_) { throw new AstroError(AstroErrorData.AstroResponseHeadersReassigned); } }; const result = { cancelled: false, clientDirectives, inlinedScripts, componentMetadata, compressHTML, cookies, /** This function returns the `Astro` faux-global */ createAstro: (astroGlobal, props, slots) => this.createAstro(result, astroGlobal, props, slots), links, partial, pathname, renderers, resolve, response, scripts, styles, _metadata: { hasHydrationScript: false, rendererSpecificHydrationScripts: /* @__PURE__ */ new Set(), hasRenderedHead: false, renderedScripts: /* @__PURE__ */ new Set(), hasDirectives: /* @__PURE__ */ new Set(), headInTree: false, extraHead: [], propagators: /* @__PURE__ */ new Set() } }; 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) { const astroPagePartial = this.#astroPagePartial ??= this.createAstroPagePartial( result, astroStaticPartial ); 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) { const renderContext = this; const { cookies, locals, params, pipeline, request, url } = this; const { response } = result; const redirect = (path, status = 302) => { if (request[responseSentSymbol]) { throw new AstroError({ ...AstroErrorData.ResponseSentError }); } return new Response(null, { status, headers: { Location: path } }); }; return { generator: astroStaticPartial.generator, glob: astroStaticPartial.glob, cookies, get clientAddress() { return renderContext.clientAddress(); }, get currentLocale() { return renderContext.computeCurrentLocale(); }, params, get preferredLocale() { return renderContext.computePreferredLocale(); }, get preferredLocaleList() { return renderContext.computePreferredLocaleList(); }, locals, redirect, request, response, site: pipeline.site, url }; } clientAddress() { const { pipeline, request } = this; if (clientAddressSymbol in request) { return Reflect.get(request, clientAddressSymbol); } if (pipeline.adapterName) { throw new AstroError({ ...AstroErrorData.ClientAddressNotAvailable, message: AstroErrorData.ClientAddressNotAvailable.message(pipeline.adapterName) }); } else { 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; return this.#currentLocale ??= computeCurrentLocale(routeData.route, locales) ?? computeCurrentLocale(url.pathname, locales) ?? fallbackTo; } #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 };