mirror of
https://github.com/sern-handler/website
synced 2026-06-22 15:52:30 +00:00
331 lines
10 KiB
JavaScript
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
|
|
};
|