Files
website/node_modules/astro/dist/core/render-context.js
2024-05-06 17:15:30 -04:00

331 lines
10 KiB
JavaScript

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
};