3084 lines
100 KiB
JavaScript
3084 lines
100 KiB
JavaScript
import { Http2ServerResponse } from 'node:http2';
|
|
import { R as ROUTE_TYPE_HEADER, h as REROUTE_DIRECTIVE_HEADER, i as decryptString, j as createSlotValueFromString, d as renderComponent, r as renderTemplate, D as DEFAULT_404_COMPONENT, k as renderSlotToString, l as renderJSX, n as chunkToString, o as isRenderInstruction, p as originPathnameSymbol, A as ASTRO_VERSION, q as clientLocalsSymbol, t as clientAddressSymbol, u as responseSentSymbol$1, v as renderPage, w as REWRITE_DIRECTIVE_HEADER_KEY, x as REWRITE_DIRECTIVE_HEADER_VALUE, y as renderEndpoint, z as REROUTABLE_STATUS_CODES } from './astro/server_DrnzQWw3.mjs';
|
|
import { appendForwardSlash as appendForwardSlash$1, joinPaths, trimSlashes, removeTrailingForwardSlash, fileExtension, slash, prependForwardSlash as prependForwardSlash$1 } from '@astrojs/internal-helpers/path';
|
|
import { A as AstroError, q as i18nNoLocaleFoundInPath, R as ResponseSentError, s as MiddlewareNoDataOrNextCalled, t as MiddlewareNotAResponse, G as GetStaticPathsRequired, u as InvalidGetStaticPathsReturn, v as InvalidGetStaticPathsEntry, w as GetStaticPathsExpectedParams, x as GetStaticPathsInvalidRouteParam, P as PageNumberParamNotFound, y as NoMatchingStaticPathFound, z as PrerenderDynamicEndpointPathCollide, B as ReservedSlotName, C as RewriteWithBodyUsed, L as LocalsNotAnObject, H as PrerenderClientAddressNotAvailable, J as ClientAddressNotAvailable, S as StaticClientAddressNotAvailable, K as AstroResponseHeadersReassigned } from './astro/assets-service_BZIICsh3.mjs';
|
|
import { serialize, parse } from 'cookie';
|
|
import { bold, red, yellow, dim, blue } from 'kleur/colors';
|
|
import { g as getActionQueryString, d as deserializeActionResult, e as ensure404Route, D as DEFAULT_404_ROUTE, a as default404Instance, N as NOOP_MIDDLEWARE_FN } from './astro-designed-error-pages_DsfhG0WT.mjs';
|
|
import 'es-module-lexer';
|
|
import 'clsx';
|
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
import fs from 'node:fs';
|
|
import http from 'node:http';
|
|
import https from 'node:https';
|
|
import enableDestroy from 'server-destroy';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import url from 'node:url';
|
|
import send from 'send';
|
|
import buffer from 'node:buffer';
|
|
import crypto from 'node:crypto';
|
|
|
|
function shouldAppendForwardSlash(trailingSlash, buildFormat) {
|
|
switch (trailingSlash) {
|
|
case "always":
|
|
return true;
|
|
case "never":
|
|
return false;
|
|
case "ignore": {
|
|
switch (buildFormat) {
|
|
case "directory":
|
|
return true;
|
|
case "preserve":
|
|
case "file":
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function createI18nMiddleware(i18n, base, trailingSlash, format) {
|
|
if (!i18n) return (_, next) => next();
|
|
const payload = {
|
|
...i18n,
|
|
trailingSlash,
|
|
base,
|
|
format};
|
|
const _redirectToDefaultLocale = redirectToDefaultLocale(payload);
|
|
const _noFoundForNonLocaleRoute = notFound(payload);
|
|
const _requestHasLocale = requestHasLocale(payload.locales);
|
|
const _redirectToFallback = redirectToFallback(payload);
|
|
const prefixAlways = (context) => {
|
|
const url = context.url;
|
|
if (url.pathname === base + "/" || url.pathname === base) {
|
|
return _redirectToDefaultLocale(context);
|
|
} else if (!_requestHasLocale(context)) {
|
|
return _noFoundForNonLocaleRoute(context);
|
|
}
|
|
return void 0;
|
|
};
|
|
const prefixOtherLocales = (context, response) => {
|
|
let pathnameContainsDefaultLocale = false;
|
|
const url = context.url;
|
|
for (const segment of url.pathname.split("/")) {
|
|
if (normalizeTheLocale(segment) === normalizeTheLocale(i18n.defaultLocale)) {
|
|
pathnameContainsDefaultLocale = true;
|
|
break;
|
|
}
|
|
}
|
|
if (pathnameContainsDefaultLocale) {
|
|
const newLocation = url.pathname.replace(`/${i18n.defaultLocale}`, "");
|
|
response.headers.set("Location", newLocation);
|
|
return _noFoundForNonLocaleRoute(context);
|
|
}
|
|
return void 0;
|
|
};
|
|
return async (context, next) => {
|
|
const response = await next();
|
|
const type = response.headers.get(ROUTE_TYPE_HEADER);
|
|
const isReroute = response.headers.get(REROUTE_DIRECTIVE_HEADER);
|
|
if (isReroute === "no" && typeof i18n.fallback === "undefined") {
|
|
return response;
|
|
}
|
|
if (type !== "page" && type !== "fallback") {
|
|
return response;
|
|
}
|
|
if (requestIs404Or500(context.request, base)) {
|
|
return response;
|
|
}
|
|
const { currentLocale } = context;
|
|
switch (i18n.strategy) {
|
|
case "manual": {
|
|
return response;
|
|
}
|
|
case "domains-prefix-other-locales": {
|
|
if (localeHasntDomain(i18n, currentLocale)) {
|
|
const result = prefixOtherLocales(context, response);
|
|
if (result) {
|
|
return result;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case "pathname-prefix-other-locales": {
|
|
const result = prefixOtherLocales(context, response);
|
|
if (result) {
|
|
return result;
|
|
}
|
|
break;
|
|
}
|
|
case "domains-prefix-always-no-redirect": {
|
|
if (localeHasntDomain(i18n, currentLocale)) {
|
|
const result = _noFoundForNonLocaleRoute(context, response);
|
|
if (result) {
|
|
return result;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case "pathname-prefix-always-no-redirect": {
|
|
const result = _noFoundForNonLocaleRoute(context, response);
|
|
if (result) {
|
|
return result;
|
|
}
|
|
break;
|
|
}
|
|
case "pathname-prefix-always": {
|
|
const result = prefixAlways(context);
|
|
if (result) {
|
|
return result;
|
|
}
|
|
break;
|
|
}
|
|
case "domains-prefix-always": {
|
|
if (localeHasntDomain(i18n, currentLocale)) {
|
|
const result = prefixAlways(context);
|
|
if (result) {
|
|
return result;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
return _redirectToFallback(context, response);
|
|
};
|
|
}
|
|
function localeHasntDomain(i18n, currentLocale) {
|
|
for (const domainLocale of Object.values(i18n.domainLookupTable)) {
|
|
if (domainLocale === currentLocale) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function requestHasLocale(locales) {
|
|
return function(context) {
|
|
return pathHasLocale(context.url.pathname, locales);
|
|
};
|
|
}
|
|
function requestIs404Or500(request, base = "") {
|
|
const url = new URL(request.url);
|
|
return url.pathname.startsWith(`${base}/404`) || url.pathname.startsWith(`${base}/500`);
|
|
}
|
|
function pathHasLocale(path, locales) {
|
|
const segments = path.split("/");
|
|
for (const segment of segments) {
|
|
for (const locale of locales) {
|
|
if (typeof locale === "string") {
|
|
if (normalizeTheLocale(segment) === normalizeTheLocale(locale)) {
|
|
return true;
|
|
}
|
|
} else if (segment === locale.path) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
function getPathByLocale(locale, locales) {
|
|
for (const loopLocale of locales) {
|
|
if (typeof loopLocale === "string") {
|
|
if (loopLocale === locale) {
|
|
return loopLocale;
|
|
}
|
|
} else {
|
|
for (const code of loopLocale.codes) {
|
|
if (code === locale) {
|
|
return loopLocale.path;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
throw new AstroError(i18nNoLocaleFoundInPath);
|
|
}
|
|
function normalizeTheLocale(locale) {
|
|
return locale.replaceAll("_", "-").toLowerCase();
|
|
}
|
|
function toCodes(locales) {
|
|
return locales.map((loopLocale) => {
|
|
if (typeof loopLocale === "string") {
|
|
return loopLocale;
|
|
} else {
|
|
return loopLocale.codes[0];
|
|
}
|
|
});
|
|
}
|
|
function redirectToDefaultLocale({
|
|
trailingSlash,
|
|
format,
|
|
base,
|
|
defaultLocale
|
|
}) {
|
|
return function(context, statusCode) {
|
|
if (shouldAppendForwardSlash(trailingSlash, format)) {
|
|
return context.redirect(`${appendForwardSlash$1(joinPaths(base, defaultLocale))}`, statusCode);
|
|
} else {
|
|
return context.redirect(`${joinPaths(base, defaultLocale)}`, statusCode);
|
|
}
|
|
};
|
|
}
|
|
function notFound({ base, locales }) {
|
|
return function(context, response) {
|
|
if (response?.headers.get(REROUTE_DIRECTIVE_HEADER) === "no") return response;
|
|
const url = context.url;
|
|
const isRoot = url.pathname === base + "/" || url.pathname === base;
|
|
if (!(isRoot || pathHasLocale(url.pathname, locales))) {
|
|
if (response) {
|
|
response.headers.set(REROUTE_DIRECTIVE_HEADER, "no");
|
|
return new Response(response.body, {
|
|
status: 404,
|
|
headers: response.headers
|
|
});
|
|
} else {
|
|
return new Response(null, {
|
|
status: 404,
|
|
headers: {
|
|
[REROUTE_DIRECTIVE_HEADER]: "no"
|
|
}
|
|
});
|
|
}
|
|
}
|
|
return void 0;
|
|
};
|
|
}
|
|
function redirectToFallback({
|
|
fallback,
|
|
locales,
|
|
defaultLocale,
|
|
strategy,
|
|
base,
|
|
fallbackType
|
|
}) {
|
|
return async function(context, response) {
|
|
if (response.status >= 300 && fallback) {
|
|
const fallbackKeys = fallback ? Object.keys(fallback) : [];
|
|
const segments = context.url.pathname.split("/");
|
|
const urlLocale = segments.find((segment) => {
|
|
for (const locale of locales) {
|
|
if (typeof locale === "string") {
|
|
if (locale === segment) {
|
|
return true;
|
|
}
|
|
} else if (locale.path === segment) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
});
|
|
if (urlLocale && fallbackKeys.includes(urlLocale)) {
|
|
const fallbackLocale = fallback[urlLocale];
|
|
const pathFallbackLocale = getPathByLocale(fallbackLocale, locales);
|
|
let newPathname;
|
|
if (pathFallbackLocale === defaultLocale && strategy === "pathname-prefix-other-locales") {
|
|
if (context.url.pathname.includes(`${base}`)) {
|
|
newPathname = context.url.pathname.replace(`/${urlLocale}`, ``);
|
|
} else {
|
|
newPathname = context.url.pathname.replace(`/${urlLocale}`, `/`);
|
|
}
|
|
} else {
|
|
newPathname = context.url.pathname.replace(`/${urlLocale}`, `/${pathFallbackLocale}`);
|
|
}
|
|
if (fallbackType === "rewrite") {
|
|
return await context.rewrite(newPathname);
|
|
} else {
|
|
return context.redirect(newPathname);
|
|
}
|
|
}
|
|
}
|
|
return response;
|
|
};
|
|
}
|
|
|
|
const DELETED_EXPIRATION = /* @__PURE__ */ new Date(0);
|
|
const DELETED_VALUE = "deleted";
|
|
const responseSentSymbol = Symbol.for("astro.responseSent");
|
|
class AstroCookie {
|
|
constructor(value) {
|
|
this.value = value;
|
|
}
|
|
json() {
|
|
if (this.value === void 0) {
|
|
throw new Error(`Cannot convert undefined to an object.`);
|
|
}
|
|
return JSON.parse(this.value);
|
|
}
|
|
number() {
|
|
return Number(this.value);
|
|
}
|
|
boolean() {
|
|
if (this.value === "false") return false;
|
|
if (this.value === "0") return false;
|
|
return Boolean(this.value);
|
|
}
|
|
}
|
|
class AstroCookies {
|
|
#request;
|
|
#requestValues;
|
|
#outgoing;
|
|
#consumed;
|
|
constructor(request) {
|
|
this.#request = request;
|
|
this.#requestValues = null;
|
|
this.#outgoing = null;
|
|
this.#consumed = false;
|
|
}
|
|
/**
|
|
* Astro.cookies.delete(key) is used to delete a cookie. Using this method will result
|
|
* in a Set-Cookie header added to the response.
|
|
* @param key The cookie to delete
|
|
* @param options Options related to this deletion, such as the path of the cookie.
|
|
*/
|
|
delete(key, options) {
|
|
const {
|
|
// @ts-expect-error
|
|
maxAge: _ignoredMaxAge,
|
|
// @ts-expect-error
|
|
expires: _ignoredExpires,
|
|
...sanitizedOptions
|
|
} = options || {};
|
|
const serializeOptions = {
|
|
expires: DELETED_EXPIRATION,
|
|
...sanitizedOptions
|
|
};
|
|
this.#ensureOutgoingMap().set(key, [
|
|
DELETED_VALUE,
|
|
serialize(key, DELETED_VALUE, serializeOptions),
|
|
false
|
|
]);
|
|
}
|
|
/**
|
|
* Astro.cookies.get(key) is used to get a cookie value. The cookie value is read from the
|
|
* request. If you have set a cookie via Astro.cookies.set(key, value), the value will be taken
|
|
* from that set call, overriding any values already part of the request.
|
|
* @param key The cookie to get.
|
|
* @returns An object containing the cookie value as well as convenience methods for converting its value.
|
|
*/
|
|
get(key, options = void 0) {
|
|
if (this.#outgoing?.has(key)) {
|
|
let [serializedValue, , isSetValue] = this.#outgoing.get(key);
|
|
if (isSetValue) {
|
|
return new AstroCookie(serializedValue);
|
|
} else {
|
|
return void 0;
|
|
}
|
|
}
|
|
const values = this.#ensureParsed(options);
|
|
if (key in values) {
|
|
const value = values[key];
|
|
return new AstroCookie(value);
|
|
}
|
|
}
|
|
/**
|
|
* Astro.cookies.has(key) returns a boolean indicating whether this cookie is either
|
|
* part of the initial request or set via Astro.cookies.set(key)
|
|
* @param key The cookie to check for.
|
|
* @returns
|
|
*/
|
|
has(key, options = void 0) {
|
|
if (this.#outgoing?.has(key)) {
|
|
let [, , isSetValue] = this.#outgoing.get(key);
|
|
return isSetValue;
|
|
}
|
|
const values = this.#ensureParsed(options);
|
|
return !!values[key];
|
|
}
|
|
/**
|
|
* Astro.cookies.set(key, value) is used to set a cookie's value. If provided
|
|
* an object it will be stringified via JSON.stringify(value). Additionally you
|
|
* can provide options customizing how this cookie will be set, such as setting httpOnly
|
|
* in order to prevent the cookie from being read in client-side JavaScript.
|
|
* @param key The name of the cookie to set.
|
|
* @param value A value, either a string or other primitive or an object.
|
|
* @param options Options for the cookie, such as the path and security settings.
|
|
*/
|
|
set(key, value, options) {
|
|
if (this.#consumed) {
|
|
const warning = new Error(
|
|
"Astro.cookies.set() was called after the cookies had already been sent to the browser.\nThis may have happened if this method was called in an imported component.\nPlease make sure that Astro.cookies.set() is only called in the frontmatter of the main page."
|
|
);
|
|
warning.name = "Warning";
|
|
console.warn(warning);
|
|
}
|
|
let serializedValue;
|
|
if (typeof value === "string") {
|
|
serializedValue = value;
|
|
} else {
|
|
let toStringValue = value.toString();
|
|
if (toStringValue === Object.prototype.toString.call(value)) {
|
|
serializedValue = JSON.stringify(value);
|
|
} else {
|
|
serializedValue = toStringValue;
|
|
}
|
|
}
|
|
const serializeOptions = {};
|
|
if (options) {
|
|
Object.assign(serializeOptions, options);
|
|
}
|
|
this.#ensureOutgoingMap().set(key, [
|
|
serializedValue,
|
|
serialize(key, serializedValue, serializeOptions),
|
|
true
|
|
]);
|
|
if (this.#request[responseSentSymbol]) {
|
|
throw new AstroError({
|
|
...ResponseSentError
|
|
});
|
|
}
|
|
}
|
|
/**
|
|
* Merges a new AstroCookies instance into the current instance. Any new cookies
|
|
* will be added to the current instance, overwriting any existing cookies with the same name.
|
|
*/
|
|
merge(cookies) {
|
|
const outgoing = cookies.#outgoing;
|
|
if (outgoing) {
|
|
for (const [key, value] of outgoing) {
|
|
this.#ensureOutgoingMap().set(key, value);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Astro.cookies.header() returns an iterator for the cookies that have previously
|
|
* been set by either Astro.cookies.set() or Astro.cookies.delete().
|
|
* This method is primarily used by adapters to set the header on outgoing responses.
|
|
* @returns
|
|
*/
|
|
*headers() {
|
|
if (this.#outgoing == null) return;
|
|
for (const [, value] of this.#outgoing) {
|
|
yield value[1];
|
|
}
|
|
}
|
|
/**
|
|
* Behaves the same as AstroCookies.prototype.headers(),
|
|
* but allows a warning when cookies are set after the instance is consumed.
|
|
*/
|
|
static consume(cookies) {
|
|
cookies.#consumed = true;
|
|
return cookies.headers();
|
|
}
|
|
#ensureParsed(options = void 0) {
|
|
if (!this.#requestValues) {
|
|
this.#parse(options);
|
|
}
|
|
if (!this.#requestValues) {
|
|
this.#requestValues = {};
|
|
}
|
|
return this.#requestValues;
|
|
}
|
|
#ensureOutgoingMap() {
|
|
if (!this.#outgoing) {
|
|
this.#outgoing = /* @__PURE__ */ new Map();
|
|
}
|
|
return this.#outgoing;
|
|
}
|
|
#parse(options = void 0) {
|
|
const raw = this.#request.headers.get("cookie");
|
|
if (!raw) {
|
|
return;
|
|
}
|
|
this.#requestValues = parse(raw, options);
|
|
}
|
|
}
|
|
|
|
const astroCookiesSymbol = Symbol.for("astro.cookies");
|
|
function attachCookiesToResponse(response, cookies) {
|
|
Reflect.set(response, astroCookiesSymbol, cookies);
|
|
}
|
|
function getCookiesFromResponse(response) {
|
|
let cookies = Reflect.get(response, astroCookiesSymbol);
|
|
if (cookies != null) {
|
|
return cookies;
|
|
} else {
|
|
return void 0;
|
|
}
|
|
}
|
|
function* getSetCookiesFromResponse(response) {
|
|
const cookies = getCookiesFromResponse(response);
|
|
if (!cookies) {
|
|
return [];
|
|
}
|
|
for (const headerValue of AstroCookies.consume(cookies)) {
|
|
yield headerValue;
|
|
}
|
|
return [];
|
|
}
|
|
|
|
const dateTimeFormat = new Intl.DateTimeFormat([], {
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
second: "2-digit",
|
|
hour12: false
|
|
});
|
|
const levels = {
|
|
debug: 20,
|
|
info: 30,
|
|
warn: 40,
|
|
error: 50,
|
|
silent: 90
|
|
};
|
|
function log(opts, level, label, message, newLine = true) {
|
|
const logLevel = opts.level;
|
|
const dest = opts.dest;
|
|
const event = {
|
|
label,
|
|
level,
|
|
message,
|
|
newLine
|
|
};
|
|
if (!isLogLevelEnabled(logLevel, level)) {
|
|
return;
|
|
}
|
|
dest.write(event);
|
|
}
|
|
function isLogLevelEnabled(configuredLogLevel, level) {
|
|
return levels[configuredLogLevel] <= levels[level];
|
|
}
|
|
function info(opts, label, message, newLine = true) {
|
|
return log(opts, "info", label, message, newLine);
|
|
}
|
|
function warn(opts, label, message, newLine = true) {
|
|
return log(opts, "warn", label, message, newLine);
|
|
}
|
|
function error(opts, label, message, newLine = true) {
|
|
return log(opts, "error", label, message, newLine);
|
|
}
|
|
function debug(...args) {
|
|
if ("_astroGlobalDebug" in globalThis) {
|
|
globalThis._astroGlobalDebug(...args);
|
|
}
|
|
}
|
|
function getEventPrefix({ level, label }) {
|
|
const timestamp = `${dateTimeFormat.format(/* @__PURE__ */ new Date())}`;
|
|
const prefix = [];
|
|
if (level === "error" || level === "warn") {
|
|
prefix.push(bold(timestamp));
|
|
prefix.push(`[${level.toUpperCase()}]`);
|
|
} else {
|
|
prefix.push(timestamp);
|
|
}
|
|
if (label) {
|
|
prefix.push(`[${label}]`);
|
|
}
|
|
if (level === "error") {
|
|
return red(prefix.join(" "));
|
|
}
|
|
if (level === "warn") {
|
|
return yellow(prefix.join(" "));
|
|
}
|
|
if (prefix.length === 1) {
|
|
return dim(prefix[0]);
|
|
}
|
|
return dim(prefix[0]) + " " + blue(prefix.splice(1).join(" "));
|
|
}
|
|
class Logger {
|
|
options;
|
|
constructor(options) {
|
|
this.options = options;
|
|
}
|
|
info(label, message, newLine = true) {
|
|
info(this.options, label, message, newLine);
|
|
}
|
|
warn(label, message, newLine = true) {
|
|
warn(this.options, label, message, newLine);
|
|
}
|
|
error(label, message, newLine = true) {
|
|
error(this.options, label, message, newLine);
|
|
}
|
|
debug(label, ...messages) {
|
|
debug(label, ...messages);
|
|
}
|
|
level() {
|
|
return this.options.level;
|
|
}
|
|
forkIntegrationLogger(label) {
|
|
return new AstroIntegrationLogger(this.options, label);
|
|
}
|
|
}
|
|
class AstroIntegrationLogger {
|
|
options;
|
|
label;
|
|
constructor(logging, label) {
|
|
this.options = logging;
|
|
this.label = label;
|
|
}
|
|
/**
|
|
* Creates a new logger instance with a new label, but the same log options.
|
|
*/
|
|
fork(label) {
|
|
return new AstroIntegrationLogger(this.options, label);
|
|
}
|
|
info(message) {
|
|
info(this.options, this.label, message);
|
|
}
|
|
warn(message) {
|
|
warn(this.options, this.label, message);
|
|
}
|
|
error(message) {
|
|
error(this.options, this.label, message);
|
|
}
|
|
debug(message) {
|
|
debug(this.label, message);
|
|
}
|
|
}
|
|
|
|
const consoleLogDestination = {
|
|
write(event) {
|
|
let dest = console.error;
|
|
if (levels[event.level] < levels["error"]) {
|
|
dest = console.log;
|
|
}
|
|
if (event.label === "SKIP_FORMAT") {
|
|
dest(event.message);
|
|
} else {
|
|
dest(getEventPrefix(event) + " " + event.message);
|
|
}
|
|
return true;
|
|
}
|
|
};
|
|
|
|
const ACTION_API_CONTEXT_SYMBOL = Symbol.for("astro.actionAPIContext");
|
|
|
|
function hasActionPayload(locals) {
|
|
return "_actionPayload" in locals;
|
|
}
|
|
function createGetActionResult(locals) {
|
|
return (actionFn) => {
|
|
if (!hasActionPayload(locals) || actionFn.toString() !== getActionQueryString(locals._actionPayload.actionName)) {
|
|
return void 0;
|
|
}
|
|
return deserializeActionResult(locals._actionPayload.actionResult);
|
|
};
|
|
}
|
|
function createCallAction(context) {
|
|
return (baseAction, input) => {
|
|
Reflect.set(context, ACTION_API_CONTEXT_SYMBOL, true);
|
|
const action = baseAction.bind(context);
|
|
return action(input);
|
|
};
|
|
}
|
|
|
|
function parseLocale(header) {
|
|
if (header === "*") {
|
|
return [{ locale: header, qualityValue: void 0 }];
|
|
}
|
|
const result = [];
|
|
const localeValues = header.split(",").map((str) => str.trim());
|
|
for (const localeValue of localeValues) {
|
|
const split = localeValue.split(";").map((str) => str.trim());
|
|
const localeName = split[0];
|
|
const qualityValue = split[1];
|
|
if (!split) {
|
|
continue;
|
|
}
|
|
if (qualityValue && qualityValue.startsWith("q=")) {
|
|
const qualityValueAsFloat = Number.parseFloat(qualityValue.slice("q=".length));
|
|
if (Number.isNaN(qualityValueAsFloat) || qualityValueAsFloat > 1) {
|
|
result.push({
|
|
locale: localeName,
|
|
qualityValue: void 0
|
|
});
|
|
} else {
|
|
result.push({
|
|
locale: localeName,
|
|
qualityValue: qualityValueAsFloat
|
|
});
|
|
}
|
|
} else {
|
|
result.push({
|
|
locale: localeName,
|
|
qualityValue: void 0
|
|
});
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
function sortAndFilterLocales(browserLocaleList, locales) {
|
|
const normalizedLocales = toCodes(locales).map(normalizeTheLocale);
|
|
return browserLocaleList.filter((browserLocale) => {
|
|
if (browserLocale.locale !== "*") {
|
|
return normalizedLocales.includes(normalizeTheLocale(browserLocale.locale));
|
|
}
|
|
return true;
|
|
}).sort((a, b) => {
|
|
if (a.qualityValue && b.qualityValue) {
|
|
return Math.sign(b.qualityValue - a.qualityValue);
|
|
}
|
|
return 0;
|
|
});
|
|
}
|
|
function computePreferredLocale(request, locales) {
|
|
const acceptHeader = request.headers.get("Accept-Language");
|
|
let result = void 0;
|
|
if (acceptHeader) {
|
|
const browserLocaleList = sortAndFilterLocales(parseLocale(acceptHeader), locales);
|
|
const firstResult = browserLocaleList.at(0);
|
|
if (firstResult && firstResult.locale !== "*") {
|
|
for (const currentLocale of locales) {
|
|
if (typeof currentLocale === "string") {
|
|
if (normalizeTheLocale(currentLocale) === normalizeTheLocale(firstResult.locale)) {
|
|
result = currentLocale;
|
|
}
|
|
} else {
|
|
for (const currentCode of currentLocale.codes) {
|
|
if (normalizeTheLocale(currentCode) === normalizeTheLocale(firstResult.locale)) {
|
|
result = currentLocale.path;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
function computePreferredLocaleList(request, locales) {
|
|
const acceptHeader = request.headers.get("Accept-Language");
|
|
let result = [];
|
|
if (acceptHeader) {
|
|
const browserLocaleList = sortAndFilterLocales(parseLocale(acceptHeader), locales);
|
|
if (browserLocaleList.length === 1 && browserLocaleList.at(0).locale === "*") {
|
|
return locales.map((locale) => {
|
|
if (typeof locale === "string") {
|
|
return locale;
|
|
} else {
|
|
return locale.codes.at(0);
|
|
}
|
|
});
|
|
} else if (browserLocaleList.length > 0) {
|
|
for (const browserLocale of browserLocaleList) {
|
|
for (const loopLocale of locales) {
|
|
if (typeof loopLocale === "string") {
|
|
if (normalizeTheLocale(loopLocale) === normalizeTheLocale(browserLocale.locale)) {
|
|
result.push(loopLocale);
|
|
}
|
|
} else {
|
|
for (const code of loopLocale.codes) {
|
|
if (code === browserLocale.locale) {
|
|
result.push(loopLocale.path);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
function computeCurrentLocale(pathname, locales, defaultLocale) {
|
|
for (const segment of pathname.split("/")) {
|
|
for (const locale of locales) {
|
|
if (typeof locale === "string") {
|
|
if (!segment.includes(locale)) continue;
|
|
if (normalizeTheLocale(locale) === normalizeTheLocale(segment)) {
|
|
return locale;
|
|
}
|
|
} else {
|
|
if (locale.path === segment) {
|
|
return locale.codes.at(0);
|
|
} else {
|
|
for (const code of locale.codes) {
|
|
if (normalizeTheLocale(code) === normalizeTheLocale(segment)) {
|
|
return code;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for (const locale of locales) {
|
|
if (typeof locale === "string") {
|
|
if (locale === defaultLocale) {
|
|
return locale;
|
|
}
|
|
} else {
|
|
if (locale.path === defaultLocale) {
|
|
return locale.codes.at(0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async function callMiddleware(onRequest, apiContext, responseFunction) {
|
|
let nextCalled = false;
|
|
let responseFunctionPromise = void 0;
|
|
const next = async (payload) => {
|
|
nextCalled = true;
|
|
responseFunctionPromise = responseFunction(apiContext, payload);
|
|
return responseFunctionPromise;
|
|
};
|
|
let middlewarePromise = onRequest(apiContext, next);
|
|
return await Promise.resolve(middlewarePromise).then(async (value) => {
|
|
if (nextCalled) {
|
|
if (typeof value !== "undefined") {
|
|
if (value instanceof Response === false) {
|
|
throw new AstroError(MiddlewareNotAResponse);
|
|
}
|
|
return value;
|
|
} else {
|
|
if (responseFunctionPromise) {
|
|
return responseFunctionPromise;
|
|
} else {
|
|
throw new AstroError(MiddlewareNotAResponse);
|
|
}
|
|
}
|
|
} else if (typeof value === "undefined") {
|
|
throw new AstroError(MiddlewareNoDataOrNextCalled);
|
|
} else if (value instanceof Response === false) {
|
|
throw new AstroError(MiddlewareNotAResponse);
|
|
} else {
|
|
return value;
|
|
}
|
|
});
|
|
}
|
|
|
|
const FORM_CONTENT_TYPES = [
|
|
"application/x-www-form-urlencoded",
|
|
"multipart/form-data",
|
|
"text/plain"
|
|
];
|
|
function createOriginCheckMiddleware() {
|
|
return defineMiddleware((context, next) => {
|
|
const { request, url } = context;
|
|
if (request.method === "GET") {
|
|
return next();
|
|
}
|
|
const sameOrigin = (request.method === "POST" || request.method === "PUT" || request.method === "PATCH" || request.method === "DELETE") && request.headers.get("origin") === url.origin;
|
|
const hasContentType = request.headers.has("content-type");
|
|
if (hasContentType) {
|
|
const formLikeHeader = hasFormLikeHeader(request.headers.get("content-type"));
|
|
if (formLikeHeader && !sameOrigin) {
|
|
return new Response(`Cross-site ${request.method} form submissions are forbidden`, {
|
|
status: 403
|
|
});
|
|
}
|
|
} else {
|
|
if (!sameOrigin) {
|
|
return new Response(`Cross-site ${request.method} form submissions are forbidden`, {
|
|
status: 403
|
|
});
|
|
}
|
|
}
|
|
return next();
|
|
});
|
|
}
|
|
function hasFormLikeHeader(contentType) {
|
|
if (contentType) {
|
|
for (const FORM_CONTENT_TYPE of FORM_CONTENT_TYPES) {
|
|
if (contentType.toLowerCase().includes(FORM_CONTENT_TYPE)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
const VALID_PARAM_TYPES = ["string", "number", "undefined"];
|
|
function validateGetStaticPathsParameter([key, value], route) {
|
|
if (!VALID_PARAM_TYPES.includes(typeof value)) {
|
|
throw new AstroError({
|
|
...GetStaticPathsInvalidRouteParam,
|
|
message: GetStaticPathsInvalidRouteParam.message(key, value, typeof value),
|
|
location: {
|
|
file: route
|
|
}
|
|
});
|
|
}
|
|
}
|
|
function validateDynamicRouteModule(mod, {
|
|
ssr,
|
|
route
|
|
}) {
|
|
if ((!ssr || route.prerender) && !mod.getStaticPaths) {
|
|
throw new AstroError({
|
|
...GetStaticPathsRequired,
|
|
location: { file: route.component }
|
|
});
|
|
}
|
|
}
|
|
function validateGetStaticPathsResult(result, logger, route) {
|
|
if (!Array.isArray(result)) {
|
|
throw new AstroError({
|
|
...InvalidGetStaticPathsReturn,
|
|
message: InvalidGetStaticPathsReturn.message(typeof result),
|
|
location: {
|
|
file: route.component
|
|
}
|
|
});
|
|
}
|
|
result.forEach((pathObject) => {
|
|
if (typeof pathObject === "object" && Array.isArray(pathObject) || pathObject === null) {
|
|
throw new AstroError({
|
|
...InvalidGetStaticPathsEntry,
|
|
message: InvalidGetStaticPathsEntry.message(
|
|
Array.isArray(pathObject) ? "array" : typeof pathObject
|
|
)
|
|
});
|
|
}
|
|
if (pathObject.params === void 0 || pathObject.params === null || pathObject.params && Object.keys(pathObject.params).length === 0) {
|
|
throw new AstroError({
|
|
...GetStaticPathsExpectedParams,
|
|
location: {
|
|
file: route.component
|
|
}
|
|
});
|
|
}
|
|
for (const [key, val] of Object.entries(pathObject.params)) {
|
|
if (!(typeof val === "undefined" || typeof val === "string" || typeof val === "number")) {
|
|
logger.warn(
|
|
"router",
|
|
`getStaticPaths() returned an invalid path param: "${key}". A string, number or undefined value was expected, but got \`${JSON.stringify(
|
|
val
|
|
)}\`.`
|
|
);
|
|
}
|
|
if (typeof val === "string" && val === "") {
|
|
logger.warn(
|
|
"router",
|
|
`getStaticPaths() returned an invalid path param: "${key}". \`undefined\` expected for an optional param, but got empty string.`
|
|
);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function stringifyParams(params, route) {
|
|
const validatedParams = Object.entries(params).reduce((acc, next) => {
|
|
validateGetStaticPathsParameter(next, route.component);
|
|
const [key, value] = next;
|
|
if (value !== void 0) {
|
|
acc[key] = typeof value === "string" ? trimSlashes(value) : value.toString();
|
|
}
|
|
return acc;
|
|
}, {});
|
|
return route.generate(validatedParams);
|
|
}
|
|
|
|
function generatePaginateFunction(routeMatch) {
|
|
return function paginateUtility(data, args = {}) {
|
|
let { pageSize: _pageSize, params: _params, props: _props } = args;
|
|
const pageSize = _pageSize || 10;
|
|
const paramName = "page";
|
|
const additionalParams = _params || {};
|
|
const additionalProps = _props || {};
|
|
let includesFirstPageNumber;
|
|
if (routeMatch.params.includes(`...${paramName}`)) {
|
|
includesFirstPageNumber = false;
|
|
} else if (routeMatch.params.includes(`${paramName}`)) {
|
|
includesFirstPageNumber = true;
|
|
} else {
|
|
throw new AstroError({
|
|
...PageNumberParamNotFound,
|
|
message: PageNumberParamNotFound.message(paramName)
|
|
});
|
|
}
|
|
const lastPage = Math.max(1, Math.ceil(data.length / pageSize));
|
|
const result = [...Array(lastPage).keys()].map((num) => {
|
|
const pageNum = num + 1;
|
|
const start = pageSize === Infinity ? 0 : (pageNum - 1) * pageSize;
|
|
const end = Math.min(start + pageSize, data.length);
|
|
const params = {
|
|
...additionalParams,
|
|
[paramName]: includesFirstPageNumber || pageNum > 1 ? String(pageNum) : void 0
|
|
};
|
|
const current = correctIndexRoute(routeMatch.generate({ ...params }));
|
|
const next = pageNum === lastPage ? void 0 : correctIndexRoute(routeMatch.generate({ ...params, page: String(pageNum + 1) }));
|
|
const prev = pageNum === 1 ? void 0 : correctIndexRoute(
|
|
routeMatch.generate({
|
|
...params,
|
|
page: !includesFirstPageNumber && pageNum - 1 === 1 ? void 0 : String(pageNum - 1)
|
|
})
|
|
);
|
|
const first = pageNum === 1 ? void 0 : correctIndexRoute(
|
|
routeMatch.generate({
|
|
...params,
|
|
page: includesFirstPageNumber ? "1" : void 0
|
|
})
|
|
);
|
|
const last = pageNum === lastPage ? void 0 : correctIndexRoute(routeMatch.generate({ ...params, page: String(lastPage) }));
|
|
return {
|
|
params,
|
|
props: {
|
|
...additionalProps,
|
|
page: {
|
|
data: data.slice(start, end),
|
|
start,
|
|
end: end - 1,
|
|
size: pageSize,
|
|
total: data.length,
|
|
currentPage: pageNum,
|
|
lastPage,
|
|
url: { current, next, prev, first, last }
|
|
}
|
|
}
|
|
};
|
|
});
|
|
return result;
|
|
};
|
|
}
|
|
function correctIndexRoute(route) {
|
|
if (route === "") {
|
|
return "/";
|
|
}
|
|
return route;
|
|
}
|
|
|
|
async function callGetStaticPaths({
|
|
mod,
|
|
route,
|
|
routeCache,
|
|
logger,
|
|
ssr
|
|
}) {
|
|
const cached = routeCache.get(route);
|
|
if (!mod) {
|
|
throw new Error("This is an error caused by Astro and not your code. Please file an issue.");
|
|
}
|
|
if (cached?.staticPaths) {
|
|
return cached.staticPaths;
|
|
}
|
|
validateDynamicRouteModule(mod, { ssr, route });
|
|
if (ssr && !route.prerender) {
|
|
const entry = Object.assign([], { keyed: /* @__PURE__ */ new Map() });
|
|
routeCache.set(route, { ...cached, staticPaths: entry });
|
|
return entry;
|
|
}
|
|
let staticPaths = [];
|
|
if (!mod.getStaticPaths) {
|
|
throw new Error("Unexpected Error.");
|
|
}
|
|
staticPaths = await mod.getStaticPaths({
|
|
// Q: Why the cast?
|
|
// A: So users downstream can have nicer typings, we have to make some sacrifice in our internal typings, which necessitate a cast here
|
|
paginate: generatePaginateFunction(route)
|
|
});
|
|
validateGetStaticPathsResult(staticPaths, logger, route);
|
|
const keyedStaticPaths = staticPaths;
|
|
keyedStaticPaths.keyed = /* @__PURE__ */ new Map();
|
|
for (const sp of keyedStaticPaths) {
|
|
const paramsKey = stringifyParams(sp.params, route);
|
|
keyedStaticPaths.keyed.set(paramsKey, sp);
|
|
}
|
|
routeCache.set(route, { ...cached, staticPaths: keyedStaticPaths });
|
|
return keyedStaticPaths;
|
|
}
|
|
class RouteCache {
|
|
logger;
|
|
cache = {};
|
|
mode;
|
|
constructor(logger, mode = "production") {
|
|
this.logger = logger;
|
|
this.mode = mode;
|
|
}
|
|
/** Clear the cache. */
|
|
clearAll() {
|
|
this.cache = {};
|
|
}
|
|
set(route, entry) {
|
|
const key = this.key(route);
|
|
if (this.mode === "production" && this.cache[key]?.staticPaths) {
|
|
this.logger.warn(null, `Internal Warning: route cache overwritten. (${key})`);
|
|
}
|
|
this.cache[key] = entry;
|
|
}
|
|
get(route) {
|
|
return this.cache[this.key(route)];
|
|
}
|
|
key(route) {
|
|
return `${route.route}_${route.component}`;
|
|
}
|
|
}
|
|
function findPathItemByKey(staticPaths, params, route, logger) {
|
|
const paramsKey = stringifyParams(params, route);
|
|
const matchedStaticPath = staticPaths.keyed.get(paramsKey);
|
|
if (matchedStaticPath) {
|
|
return matchedStaticPath;
|
|
}
|
|
logger.debug("router", `findPathItemByKey() - Unexpected cache miss looking for ${paramsKey}`);
|
|
}
|
|
|
|
function getPattern(segments, base, addTrailingSlash) {
|
|
const pathname = segments.map((segment) => {
|
|
if (segment.length === 1 && segment[0].spread) {
|
|
return "(?:\\/(.*?))?";
|
|
} else {
|
|
return "\\/" + segment.map((part) => {
|
|
if (part.spread) {
|
|
return "(.*?)";
|
|
} else if (part.dynamic) {
|
|
return "([^/]+?)";
|
|
} else {
|
|
return part.content.normalize().replace(/\?/g, "%3F").replace(/#/g, "%23").replace(/%5B/g, "[").replace(/%5D/g, "]").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
}
|
|
}).join("");
|
|
}
|
|
}).join("");
|
|
const trailing = addTrailingSlash && segments.length ? getTrailingSlashPattern(addTrailingSlash) : "$";
|
|
let initial = "\\/";
|
|
if (addTrailingSlash === "never" && base !== "/") {
|
|
initial = "";
|
|
}
|
|
return new RegExp(`^${pathname || initial}${trailing}`);
|
|
}
|
|
function getTrailingSlashPattern(addTrailingSlash) {
|
|
if (addTrailingSlash === "always") {
|
|
return "\\/$";
|
|
}
|
|
if (addTrailingSlash === "never") {
|
|
return "$";
|
|
}
|
|
return "\\/?$";
|
|
}
|
|
|
|
const SERVER_ISLAND_ROUTE = "/_server-islands/[name]";
|
|
const SERVER_ISLAND_COMPONENT = "_server-islands.astro";
|
|
function getServerIslandRouteData(config) {
|
|
const segments = [
|
|
[{ content: "_server-islands", dynamic: false, spread: false }],
|
|
[{ content: "name", dynamic: true, spread: false }]
|
|
];
|
|
const route = {
|
|
type: "page",
|
|
component: SERVER_ISLAND_COMPONENT,
|
|
generate: () => "",
|
|
params: ["name"],
|
|
segments,
|
|
pattern: getPattern(segments, config.base, config.trailingSlash),
|
|
prerender: false,
|
|
isIndex: false,
|
|
fallbackRoutes: [],
|
|
route: SERVER_ISLAND_ROUTE
|
|
};
|
|
return route;
|
|
}
|
|
function ensureServerIslandRoute(config, routeManifest) {
|
|
if (routeManifest.routes.some((route) => route.route === "/_server-islands/[name]")) {
|
|
return;
|
|
}
|
|
routeManifest.routes.unshift(getServerIslandRouteData(config));
|
|
}
|
|
function createEndpoint(manifest) {
|
|
const page = async (result) => {
|
|
const params = result.params;
|
|
const request = result.request;
|
|
const raw = await request.text();
|
|
const data = JSON.parse(raw);
|
|
if (!params.name) {
|
|
return new Response(null, {
|
|
status: 400,
|
|
statusText: "Bad request"
|
|
});
|
|
}
|
|
const componentId = params.name;
|
|
const imp = manifest.serverIslandMap?.get(componentId);
|
|
if (!imp) {
|
|
return new Response(null, {
|
|
status: 404,
|
|
statusText: "Not found"
|
|
});
|
|
}
|
|
const key = await manifest.key;
|
|
const encryptedProps = data.encryptedProps;
|
|
const propString = await decryptString(key, encryptedProps);
|
|
const props = JSON.parse(propString);
|
|
const componentModule = await imp();
|
|
const Component = componentModule[data.componentExport];
|
|
const slots = {};
|
|
for (const prop in data.slots) {
|
|
slots[prop] = createSlotValueFromString(data.slots[prop]);
|
|
}
|
|
return renderTemplate`${renderComponent(result, "Component", Component, props, slots)}`;
|
|
};
|
|
page.isAstroComponentFactory = true;
|
|
const instance = {
|
|
default: page,
|
|
partial: true
|
|
};
|
|
return instance;
|
|
}
|
|
|
|
function injectDefaultRoutes(ssrManifest, routeManifest) {
|
|
ensure404Route(routeManifest);
|
|
ensureServerIslandRoute(ssrManifest, routeManifest);
|
|
return routeManifest;
|
|
}
|
|
function createDefaultRoutes(manifest) {
|
|
const root = new URL(manifest.hrefRoot);
|
|
return [
|
|
{
|
|
instance: default404Instance,
|
|
matchesComponent: (filePath) => filePath.href === new URL(DEFAULT_404_COMPONENT, root).href,
|
|
route: DEFAULT_404_ROUTE.route,
|
|
component: DEFAULT_404_COMPONENT
|
|
},
|
|
{
|
|
instance: createEndpoint(manifest),
|
|
matchesComponent: (filePath) => filePath.href === new URL(SERVER_ISLAND_COMPONENT, root).href,
|
|
route: SERVER_ISLAND_ROUTE,
|
|
component: SERVER_ISLAND_COMPONENT
|
|
}
|
|
];
|
|
}
|
|
|
|
class Pipeline {
|
|
constructor(logger, manifest, mode, renderers, resolve, serverLike, streaming, adapterName = manifest.adapterName, clientDirectives = manifest.clientDirectives, inlinedScripts = manifest.inlinedScripts, compressHTML = manifest.compressHTML, i18n = manifest.i18n, middleware = manifest.middleware, routeCache = new RouteCache(logger, mode), site = manifest.site ? new URL(manifest.site) : void 0, defaultRoutes = createDefaultRoutes(manifest)) {
|
|
this.logger = logger;
|
|
this.manifest = manifest;
|
|
this.mode = mode;
|
|
this.renderers = renderers;
|
|
this.resolve = resolve;
|
|
this.serverLike = serverLike;
|
|
this.streaming = streaming;
|
|
this.adapterName = adapterName;
|
|
this.clientDirectives = clientDirectives;
|
|
this.inlinedScripts = inlinedScripts;
|
|
this.compressHTML = compressHTML;
|
|
this.i18n = i18n;
|
|
this.middleware = middleware;
|
|
this.routeCache = routeCache;
|
|
this.site = site;
|
|
this.defaultRoutes = defaultRoutes;
|
|
this.internalMiddleware = [];
|
|
if (i18n?.strategy !== "manual") {
|
|
this.internalMiddleware.push(
|
|
createI18nMiddleware(i18n, manifest.base, manifest.trailingSlash, manifest.buildFormat)
|
|
);
|
|
}
|
|
}
|
|
internalMiddleware;
|
|
resolvedMiddleware = void 0;
|
|
/**
|
|
* Resolves the middleware from the manifest, and returns the `onRequest` function. If `onRequest` isn't there,
|
|
* it returns a no-op function
|
|
*/
|
|
async getMiddleware() {
|
|
if (this.resolvedMiddleware) {
|
|
return this.resolvedMiddleware;
|
|
} else if (this.middleware) {
|
|
const middlewareInstance = await this.middleware();
|
|
const onRequest = middlewareInstance.onRequest ?? NOOP_MIDDLEWARE_FN;
|
|
if (this.manifest.checkOrigin) {
|
|
this.resolvedMiddleware = sequence(createOriginCheckMiddleware(), onRequest);
|
|
} else {
|
|
this.resolvedMiddleware = onRequest;
|
|
}
|
|
return this.resolvedMiddleware;
|
|
} else {
|
|
this.resolvedMiddleware = NOOP_MIDDLEWARE_FN;
|
|
return this.resolvedMiddleware;
|
|
}
|
|
}
|
|
}
|
|
|
|
function routeIsRedirect(route) {
|
|
return route?.type === "redirect";
|
|
}
|
|
function routeIsFallback(route) {
|
|
return route?.type === "fallback";
|
|
}
|
|
|
|
const RedirectComponentInstance = {
|
|
default() {
|
|
return new Response(null, {
|
|
status: 301
|
|
});
|
|
}
|
|
};
|
|
const RedirectSinglePageBuiltModule = {
|
|
page: () => Promise.resolve(RedirectComponentInstance),
|
|
onRequest: (_, next) => next(),
|
|
renderers: []
|
|
};
|
|
|
|
async function renderRedirect(renderContext) {
|
|
const {
|
|
request: { method },
|
|
routeData
|
|
} = renderContext;
|
|
const { redirect, redirectRoute } = routeData;
|
|
const status = redirectRoute && typeof redirect === "object" ? redirect.status : method === "GET" ? 301 : 308;
|
|
const headers = { location: encodeURI(redirectRouteGenerate(renderContext)) };
|
|
return new Response(null, { status, headers });
|
|
}
|
|
function redirectRouteGenerate(renderContext) {
|
|
const {
|
|
params,
|
|
routeData: { redirect, redirectRoute }
|
|
} = renderContext;
|
|
if (typeof redirectRoute !== "undefined") {
|
|
return redirectRoute?.generate(params) || redirectRoute?.pathname || "/";
|
|
} else if (typeof redirect === "string") {
|
|
let target = redirect;
|
|
for (const param of Object.keys(params)) {
|
|
const paramValue = params[param];
|
|
target = target.replace(`[${param}]`, paramValue).replace(`[...${param}]`, paramValue);
|
|
}
|
|
return target;
|
|
} else if (typeof redirect === "undefined") {
|
|
return "/";
|
|
}
|
|
return redirect.destination;
|
|
}
|
|
|
|
async function getProps(opts) {
|
|
const { logger, mod, routeData: route, routeCache, pathname, serverLike } = opts;
|
|
if (!route || route.pathname) {
|
|
return {};
|
|
}
|
|
if (routeIsRedirect(route) || routeIsFallback(route) || route.component === DEFAULT_404_COMPONENT) {
|
|
return {};
|
|
}
|
|
const staticPaths = await callGetStaticPaths({
|
|
mod,
|
|
route,
|
|
routeCache,
|
|
logger,
|
|
ssr: serverLike
|
|
});
|
|
const params = getParams(route, pathname);
|
|
const matchedStaticPath = findPathItemByKey(staticPaths, params, route, logger);
|
|
if (!matchedStaticPath && (serverLike ? route.prerender : true)) {
|
|
throw new AstroError({
|
|
...NoMatchingStaticPathFound,
|
|
message: NoMatchingStaticPathFound.message(pathname),
|
|
hint: NoMatchingStaticPathFound.hint([route.component])
|
|
});
|
|
}
|
|
if (mod) {
|
|
validatePrerenderEndpointCollision(route, mod, params);
|
|
}
|
|
const props = matchedStaticPath?.props ? { ...matchedStaticPath.props } : {};
|
|
return props;
|
|
}
|
|
function getParams(route, pathname) {
|
|
if (!route.params.length) return {};
|
|
const paramsMatch = route.pattern.exec(decodeURIComponent(pathname));
|
|
if (!paramsMatch) return {};
|
|
const params = {};
|
|
route.params.forEach((key, i) => {
|
|
if (key.startsWith("...")) {
|
|
params[key.slice(3)] = paramsMatch[i + 1] ? paramsMatch[i + 1] : void 0;
|
|
} else {
|
|
params[key] = paramsMatch[i + 1];
|
|
}
|
|
});
|
|
return params;
|
|
}
|
|
function validatePrerenderEndpointCollision(route, mod, params) {
|
|
if (route.type === "endpoint" && mod.getStaticPaths) {
|
|
const lastSegment = route.segments[route.segments.length - 1];
|
|
const paramValues = Object.values(params);
|
|
const lastParam = paramValues[paramValues.length - 1];
|
|
if (lastSegment.length === 1 && lastSegment[0].dynamic && lastParam === void 0) {
|
|
throw new AstroError({
|
|
...PrerenderDynamicEndpointPathCollide,
|
|
message: PrerenderDynamicEndpointPathCollide.message(route.route),
|
|
hint: PrerenderDynamicEndpointPathCollide.hint(route.component),
|
|
location: {
|
|
file: route.component
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function getFunctionExpression(slot) {
|
|
if (!slot) return;
|
|
const expressions = slot?.expressions?.filter((e) => isRenderInstruction(e) === false);
|
|
if (expressions?.length !== 1) return;
|
|
return expressions[0];
|
|
}
|
|
class Slots {
|
|
#result;
|
|
#slots;
|
|
#logger;
|
|
constructor(result, slots, logger) {
|
|
this.#result = result;
|
|
this.#slots = slots;
|
|
this.#logger = logger;
|
|
if (slots) {
|
|
for (const key of Object.keys(slots)) {
|
|
if (this[key] !== void 0) {
|
|
throw new AstroError({
|
|
...ReservedSlotName,
|
|
message: ReservedSlotName.message(key)
|
|
});
|
|
}
|
|
Object.defineProperty(this, key, {
|
|
get() {
|
|
return true;
|
|
},
|
|
enumerable: true
|
|
});
|
|
}
|
|
}
|
|
}
|
|
has(name) {
|
|
if (!this.#slots) return false;
|
|
return Boolean(this.#slots[name]);
|
|
}
|
|
async render(name, args = []) {
|
|
if (!this.#slots || !this.has(name)) return;
|
|
const result = this.#result;
|
|
if (!Array.isArray(args)) {
|
|
this.#logger.warn(
|
|
null,
|
|
`Expected second parameter to be an array, received a ${typeof args}. If you're trying to pass an array as a single argument and getting unexpected results, make sure you're passing your array as a item of an array. Ex: Astro.slots.render('default', [["Hello", "World"]])`
|
|
);
|
|
} else if (args.length > 0) {
|
|
const slotValue = this.#slots[name];
|
|
const component = typeof slotValue === "function" ? await slotValue(result) : await slotValue;
|
|
const expression = getFunctionExpression(component);
|
|
if (expression) {
|
|
const slot = async () => typeof expression === "function" ? expression(...args) : expression;
|
|
return await renderSlotToString(result, slot).then((res) => {
|
|
return res;
|
|
});
|
|
}
|
|
if (typeof component === "function") {
|
|
return await renderJSX(result, component(...args)).then(
|
|
(res) => res != null ? String(res) : res
|
|
);
|
|
}
|
|
}
|
|
const content = await renderSlotToString(result, this.#slots[name]);
|
|
const outHTML = chunkToString(result, content);
|
|
return outHTML;
|
|
}
|
|
}
|
|
|
|
function sequence(...handlers) {
|
|
const filtered = handlers.filter((h) => !!h);
|
|
const length = filtered.length;
|
|
if (!length) {
|
|
return defineMiddleware((_context, next) => {
|
|
return next();
|
|
});
|
|
}
|
|
return defineMiddleware((context, next) => {
|
|
let carriedPayload = void 0;
|
|
return applyHandle(0, context);
|
|
function applyHandle(i, handleContext) {
|
|
const handle = filtered[i];
|
|
const result = handle(handleContext, async (payload) => {
|
|
if (i < length - 1) {
|
|
if (payload) {
|
|
let newRequest;
|
|
if (payload instanceof Request) {
|
|
newRequest = payload;
|
|
} else if (payload instanceof URL) {
|
|
newRequest = new Request(payload, handleContext.request);
|
|
} else {
|
|
newRequest = new Request(
|
|
new URL(payload, handleContext.url.origin),
|
|
handleContext.request
|
|
);
|
|
}
|
|
const pipeline = Reflect.get(handleContext, apiContextRoutesSymbol);
|
|
const { routeData, pathname } = await pipeline.tryRewrite(
|
|
payload,
|
|
handleContext.request
|
|
);
|
|
carriedPayload = payload;
|
|
handleContext.request = newRequest;
|
|
handleContext.url = new URL(newRequest.url);
|
|
handleContext.cookies = new AstroCookies(newRequest);
|
|
handleContext.params = getParams(routeData, pathname);
|
|
}
|
|
return applyHandle(i + 1, handleContext);
|
|
} else {
|
|
return next(payload ?? carriedPayload);
|
|
}
|
|
});
|
|
return result;
|
|
}
|
|
});
|
|
}
|
|
|
|
function defineMiddleware(fn) {
|
|
return fn;
|
|
}
|
|
|
|
function matchRoute(pathname, manifest) {
|
|
const decodedPathname = decodeURI(pathname);
|
|
return manifest.routes.find((route) => {
|
|
return route.pattern.test(decodedPathname) || route.fallbackRoutes.some((fallbackRoute) => fallbackRoute.pattern.test(decodedPathname));
|
|
});
|
|
}
|
|
function isRoute404or500(route) {
|
|
return route.pattern.test("/404") || route.pattern.test("/500");
|
|
}
|
|
|
|
function findRouteToRewrite({
|
|
payload,
|
|
routes,
|
|
request,
|
|
trailingSlash,
|
|
buildFormat,
|
|
base
|
|
}) {
|
|
let newUrl = void 0;
|
|
if (payload instanceof URL) {
|
|
newUrl = payload;
|
|
} else if (payload instanceof Request) {
|
|
newUrl = new URL(payload.url);
|
|
} else {
|
|
newUrl = new URL(payload, new URL(request.url).origin);
|
|
}
|
|
let pathname = newUrl.pathname;
|
|
if (base !== "/" && newUrl.pathname.startsWith(base)) {
|
|
pathname = shouldAppendForwardSlash(trailingSlash, buildFormat) ? appendForwardSlash$1(newUrl.pathname) : removeTrailingForwardSlash(newUrl.pathname);
|
|
pathname = pathname.slice(base.length);
|
|
}
|
|
let foundRoute;
|
|
for (const route of routes) {
|
|
if (route.pattern.test(decodeURI(pathname))) {
|
|
foundRoute = route;
|
|
break;
|
|
}
|
|
}
|
|
if (foundRoute) {
|
|
return {
|
|
routeData: foundRoute,
|
|
newUrl,
|
|
pathname
|
|
};
|
|
} else {
|
|
const custom404 = routes.find((route) => route.route === "/404");
|
|
if (custom404) {
|
|
return { routeData: custom404, newUrl, pathname };
|
|
} else {
|
|
return { routeData: DEFAULT_404_ROUTE, newUrl, pathname };
|
|
}
|
|
}
|
|
}
|
|
function copyRequest(newUrl, oldRequest) {
|
|
if (oldRequest.bodyUsed) {
|
|
throw new AstroError(RewriteWithBodyUsed);
|
|
}
|
|
return new Request(newUrl, {
|
|
method: oldRequest.method,
|
|
headers: oldRequest.headers,
|
|
body: oldRequest.body,
|
|
referrer: oldRequest.referrer,
|
|
referrerPolicy: oldRequest.referrerPolicy,
|
|
mode: oldRequest.mode,
|
|
credentials: oldRequest.credentials,
|
|
cache: oldRequest.cache,
|
|
redirect: oldRequest.redirect,
|
|
integrity: oldRequest.integrity,
|
|
signal: oldRequest.signal,
|
|
keepalive: oldRequest.keepalive,
|
|
// https://fetch.spec.whatwg.org/#dom-request-duplex
|
|
// @ts-expect-error It isn't part of the types, but undici accepts it and it allows to carry over the body to a new request
|
|
duplex: "half"
|
|
});
|
|
}
|
|
function setOriginPathname(request, pathname) {
|
|
Reflect.set(request, originPathnameSymbol, encodeURIComponent(pathname));
|
|
}
|
|
|
|
const apiContextRoutesSymbol = Symbol.for("context.routes");
|
|
class RenderContext {
|
|
constructor(pipeline, locals, middleware, pathname, request, routeData, status, cookies = new AstroCookies(request), params = getParams(routeData, pathname), url = new URL(request.url), props = {}, partial = void 0) {
|
|
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;
|
|
this.props = props;
|
|
this.partial = partial;
|
|
}
|
|
/**
|
|
* A flag that tells the render content if the rewriting was triggered
|
|
*/
|
|
isRewriting = false;
|
|
/**
|
|
* A safety net in case of loops
|
|
*/
|
|
counter = 0;
|
|
static async create({
|
|
locals = {},
|
|
middleware,
|
|
pathname,
|
|
pipeline,
|
|
request,
|
|
routeData,
|
|
status = 200,
|
|
props,
|
|
partial = void 0
|
|
}) {
|
|
const pipelineMiddleware = await pipeline.getMiddleware();
|
|
setOriginPathname(request, pathname);
|
|
return new RenderContext(
|
|
pipeline,
|
|
locals,
|
|
sequence(...pipeline.internalMiddleware, middleware ?? pipelineMiddleware),
|
|
pathname,
|
|
request,
|
|
routeData,
|
|
status,
|
|
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 } = pipeline;
|
|
const isPrerendered = !serverLike || this.routeData.prerender;
|
|
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
|
|
});
|
|
const apiContext = this.createAPIContext(props, isPrerendered);
|
|
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) {
|
|
pipeline.logger.debug("router", "Called rewriting to:", payload);
|
|
const {
|
|
routeData,
|
|
componentInstance: newComponent,
|
|
pathname,
|
|
newUrl
|
|
} = await pipeline.tryRewrite(payload, this.request);
|
|
this.routeData = routeData;
|
|
componentInstance = newComponent;
|
|
if (payload instanceof Request) {
|
|
this.request = payload;
|
|
} else {
|
|
this.request = copyRequest(newUrl, this.request);
|
|
}
|
|
this.isRewriting = true;
|
|
this.url = new URL(this.request.url);
|
|
this.cookies = new AstroCookies(this.request);
|
|
this.params = getParams(routeData, pathname);
|
|
this.pathname = pathname;
|
|
this.status = 200;
|
|
}
|
|
let response2;
|
|
switch (this.routeData.type) {
|
|
case "endpoint": {
|
|
response2 = await renderEndpoint(componentInstance, ctx, serverLike, logger);
|
|
break;
|
|
}
|
|
case "redirect":
|
|
return renderRedirect(this);
|
|
case "page": {
|
|
const result = await this.createResult(componentInstance);
|
|
try {
|
|
response2 = await renderPage(
|
|
result,
|
|
componentInstance?.default,
|
|
props,
|
|
slots,
|
|
streaming,
|
|
this.routeData
|
|
);
|
|
} catch (e) {
|
|
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;
|
|
};
|
|
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, isPrerendered) {
|
|
const context = this.createActionAPIContext();
|
|
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),
|
|
// Used internally by Actions middleware.
|
|
// TODO: discuss exposing this information from APIContext.
|
|
// middleware runs on prerendered routes in the dev server,
|
|
// so this is useful information to have.
|
|
_isPrerendered: isPrerendered
|
|
});
|
|
}
|
|
async #executeRewrite(reroutePayload) {
|
|
this.pipeline.logger.debug("router", "Calling rewrite: ", reroutePayload);
|
|
const { routeData, componentInstance, newUrl, pathname } = await this.pipeline.tryRewrite(
|
|
reroutePayload,
|
|
this.request
|
|
);
|
|
this.routeData = routeData;
|
|
if (reroutePayload instanceof Request) {
|
|
this.request = reroutePayload;
|
|
} else {
|
|
this.request = copyRequest(newUrl, this.request);
|
|
}
|
|
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;
|
|
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,
|
|
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(LocalsNotAnObject);
|
|
} else {
|
|
renderContext.locals = val;
|
|
Reflect.set(this.request, clientLocalsSymbol, val);
|
|
}
|
|
},
|
|
params,
|
|
get preferredLocale() {
|
|
return renderContext.computePreferredLocale();
|
|
},
|
|
get preferredLocaleList() {
|
|
return renderContext.computePreferredLocaleList();
|
|
},
|
|
rewrite,
|
|
request: this.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 = typeof this.partial === "boolean" ? this.partial : Boolean(mod.partial);
|
|
const response = {
|
|
status,
|
|
statusText: "OK",
|
|
get headers() {
|
|
return headers;
|
|
},
|
|
// Disallow `Astro.response.headers = new Headers`
|
|
set headers(_) {
|
|
throw new AstroError(AstroResponseHeadersReassigned);
|
|
}
|
|
};
|
|
const actionResult = hasActionPayload(this.locals) ? deserializeActionResult(this.locals._actionPayload.actionResult) : void 0;
|
|
const result = {
|
|
base: manifest.base,
|
|
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,
|
|
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(),
|
|
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) {
|
|
let astroPagePartial;
|
|
if (this.isRewriting) {
|
|
astroPagePartial = this.#astroPagePartial = this.createAstroPagePartial(
|
|
result,
|
|
astroStaticPartial
|
|
);
|
|
} else {
|
|
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, url } = this;
|
|
const { response } = result;
|
|
const redirect = (path, status = 302) => {
|
|
if (this.request[responseSentSymbol$1]) {
|
|
throw new AstroError({
|
|
...ResponseSentError
|
|
});
|
|
}
|
|
return new Response(null, { status, headers: { Location: path } });
|
|
};
|
|
const rewrite = async (reroutePayload) => {
|
|
return await this.#executeRewrite(reroutePayload);
|
|
};
|
|
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,
|
|
rewrite,
|
|
request: this.request,
|
|
response,
|
|
site: pipeline.site,
|
|
getActionResult: createGetActionResult(locals),
|
|
get callAction() {
|
|
return createCallAction(this);
|
|
},
|
|
url
|
|
};
|
|
}
|
|
clientAddress() {
|
|
const { pipeline, request } = this;
|
|
if (clientAddressSymbol in request) {
|
|
return Reflect.get(request, clientAddressSymbol);
|
|
}
|
|
if (pipeline.serverLike) {
|
|
if (request.body === null) {
|
|
throw new AstroError(PrerenderClientAddressNotAvailable);
|
|
}
|
|
if (pipeline.adapterName) {
|
|
throw new AstroError({
|
|
...ClientAddressNotAvailable,
|
|
message: ClientAddressNotAvailable.message(pipeline.adapterName)
|
|
});
|
|
}
|
|
}
|
|
throw new AstroError(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;
|
|
const pathname = routeData.pathname && !isRoute404or500(routeData) ? 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);
|
|
}
|
|
}
|
|
|
|
function getAssetsPrefix(fileExtension, assetsPrefix) {
|
|
if (!assetsPrefix) return "";
|
|
if (typeof assetsPrefix === "string") return assetsPrefix;
|
|
const dotLessFileExtension = fileExtension.slice(1);
|
|
if (assetsPrefix[dotLessFileExtension]) {
|
|
return assetsPrefix[dotLessFileExtension];
|
|
}
|
|
return assetsPrefix.fallback;
|
|
}
|
|
|
|
function createAssetLink(href, base, assetsPrefix) {
|
|
if (assetsPrefix) {
|
|
const pf = getAssetsPrefix(fileExtension(href), assetsPrefix);
|
|
return joinPaths(pf, slash(href));
|
|
} else if (base) {
|
|
return prependForwardSlash$1(joinPaths(base, slash(href)));
|
|
} else {
|
|
return href;
|
|
}
|
|
}
|
|
function createStylesheetElement(stylesheet, base, assetsPrefix) {
|
|
if (stylesheet.type === "inline") {
|
|
return {
|
|
props: {},
|
|
children: stylesheet.content
|
|
};
|
|
} else {
|
|
return {
|
|
props: {
|
|
rel: "stylesheet",
|
|
href: createAssetLink(stylesheet.src, base, assetsPrefix)
|
|
},
|
|
children: ""
|
|
};
|
|
}
|
|
}
|
|
function createStylesheetElementSet(stylesheets, base, assetsPrefix) {
|
|
return new Set(stylesheets.map((s) => createStylesheetElement(s, base, assetsPrefix)));
|
|
}
|
|
function createModuleScriptElement(script, base, assetsPrefix) {
|
|
if (script.type === "external") {
|
|
return createModuleScriptElementWithSrc(script.value, base, assetsPrefix);
|
|
} else {
|
|
return {
|
|
props: {
|
|
type: "module"
|
|
},
|
|
children: script.value
|
|
};
|
|
}
|
|
}
|
|
function createModuleScriptElementWithSrc(src, base, assetsPrefix) {
|
|
return {
|
|
props: {
|
|
type: "module",
|
|
src: createAssetLink(src, base, assetsPrefix)
|
|
},
|
|
children: ""
|
|
};
|
|
}
|
|
|
|
class AppPipeline extends Pipeline {
|
|
#manifestData;
|
|
static create(manifestData, {
|
|
logger,
|
|
manifest,
|
|
mode,
|
|
renderers,
|
|
resolve,
|
|
serverLike,
|
|
streaming,
|
|
defaultRoutes
|
|
}) {
|
|
const pipeline = new AppPipeline(
|
|
logger,
|
|
manifest,
|
|
mode,
|
|
renderers,
|
|
resolve,
|
|
serverLike,
|
|
streaming,
|
|
void 0,
|
|
void 0,
|
|
void 0,
|
|
void 0,
|
|
void 0,
|
|
void 0,
|
|
void 0,
|
|
void 0,
|
|
defaultRoutes
|
|
);
|
|
pipeline.#manifestData = manifestData;
|
|
return pipeline;
|
|
}
|
|
headElements(routeData) {
|
|
const routeInfo = this.manifest.routes.find((route) => route.routeData === routeData);
|
|
const links = /* @__PURE__ */ new Set();
|
|
const scripts = /* @__PURE__ */ new Set();
|
|
const styles = createStylesheetElementSet(routeInfo?.styles ?? []);
|
|
for (const script of routeInfo?.scripts ?? []) {
|
|
if ("stage" in script) {
|
|
if (script.stage === "head-inline") {
|
|
scripts.add({
|
|
props: {},
|
|
children: script.children
|
|
});
|
|
}
|
|
} else {
|
|
scripts.add(createModuleScriptElement(script));
|
|
}
|
|
}
|
|
return { links, styles, scripts };
|
|
}
|
|
componentMetadata() {
|
|
}
|
|
async getComponentByRoute(routeData) {
|
|
const module = await this.getModuleForRoute(routeData);
|
|
return module.page();
|
|
}
|
|
async tryRewrite(payload, request) {
|
|
const { newUrl, pathname, routeData } = findRouteToRewrite({
|
|
payload,
|
|
request,
|
|
routes: this.manifest?.routes.map((r) => r.routeData),
|
|
trailingSlash: this.manifest.trailingSlash,
|
|
buildFormat: this.manifest.buildFormat,
|
|
base: this.manifest.base
|
|
});
|
|
const componentInstance = await this.getComponentByRoute(routeData);
|
|
return { newUrl, pathname, componentInstance, routeData };
|
|
}
|
|
async getModuleForRoute(route) {
|
|
for (const defaultRoute of this.defaultRoutes) {
|
|
if (route.component === defaultRoute.component) {
|
|
return {
|
|
page: () => Promise.resolve(defaultRoute.instance),
|
|
renderers: []
|
|
};
|
|
}
|
|
}
|
|
if (route.type === "redirect") {
|
|
return RedirectSinglePageBuiltModule;
|
|
} else {
|
|
if (this.manifest.pageMap) {
|
|
const importComponentInstance = this.manifest.pageMap.get(route.component);
|
|
if (!importComponentInstance) {
|
|
throw new Error(
|
|
`Unexpectedly unable to find a component instance for route ${route.route}`
|
|
);
|
|
}
|
|
return await importComponentInstance();
|
|
} else if (this.manifest.pageModule) {
|
|
return this.manifest.pageModule;
|
|
}
|
|
throw new Error(
|
|
"Astro couldn't find the correct page to render, probably because it wasn't correctly mapped for SSR usage. This is an internal error, please file an issue."
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
class App {
|
|
#manifest;
|
|
#manifestData;
|
|
#logger = new Logger({
|
|
dest: consoleLogDestination,
|
|
level: "info"
|
|
});
|
|
#baseWithoutTrailingSlash;
|
|
#pipeline;
|
|
#adapterLogger;
|
|
#renderOptionsDeprecationWarningShown = false;
|
|
constructor(manifest, streaming = true) {
|
|
this.#manifest = manifest;
|
|
this.#manifestData = injectDefaultRoutes(manifest, {
|
|
routes: manifest.routes.map((route) => route.routeData)
|
|
});
|
|
this.#baseWithoutTrailingSlash = removeTrailingForwardSlash(this.#manifest.base);
|
|
this.#pipeline = this.#createPipeline(this.#manifestData, streaming);
|
|
this.#adapterLogger = new AstroIntegrationLogger(
|
|
this.#logger.options,
|
|
this.#manifest.adapterName
|
|
);
|
|
}
|
|
getAdapterLogger() {
|
|
return this.#adapterLogger;
|
|
}
|
|
/**
|
|
* Creates a pipeline by reading the stored manifest
|
|
*
|
|
* @param manifestData
|
|
* @param streaming
|
|
* @private
|
|
*/
|
|
#createPipeline(manifestData, streaming = false) {
|
|
return AppPipeline.create(manifestData, {
|
|
logger: this.#logger,
|
|
manifest: this.#manifest,
|
|
mode: "production",
|
|
renderers: this.#manifest.renderers,
|
|
defaultRoutes: createDefaultRoutes(this.#manifest),
|
|
resolve: async (specifier) => {
|
|
if (!(specifier in this.#manifest.entryModules)) {
|
|
throw new Error(`Unable to resolve [${specifier}]`);
|
|
}
|
|
const bundlePath = this.#manifest.entryModules[specifier];
|
|
if (bundlePath.startsWith("data:") || bundlePath.length === 0) {
|
|
return bundlePath;
|
|
} else {
|
|
return createAssetLink(bundlePath, this.#manifest.base, this.#manifest.assetsPrefix);
|
|
}
|
|
},
|
|
serverLike: true,
|
|
streaming
|
|
});
|
|
}
|
|
set setManifestData(newManifestData) {
|
|
this.#manifestData = newManifestData;
|
|
}
|
|
removeBase(pathname) {
|
|
if (pathname.startsWith(this.#manifest.base)) {
|
|
return pathname.slice(this.#baseWithoutTrailingSlash.length + 1);
|
|
}
|
|
return pathname;
|
|
}
|
|
#getPathnameFromRequest(request) {
|
|
const url = new URL(request.url);
|
|
const pathname = prependForwardSlash$1(this.removeBase(url.pathname));
|
|
return pathname;
|
|
}
|
|
match(request) {
|
|
const url = new URL(request.url);
|
|
if (this.#manifest.assets.has(url.pathname)) return void 0;
|
|
let pathname = this.#computePathnameFromDomain(request);
|
|
if (!pathname) {
|
|
pathname = prependForwardSlash$1(this.removeBase(url.pathname));
|
|
}
|
|
let routeData = matchRoute(pathname, this.#manifestData);
|
|
if (!routeData || routeData.prerender) return void 0;
|
|
return routeData;
|
|
}
|
|
#computePathnameFromDomain(request) {
|
|
let pathname = void 0;
|
|
const url = new URL(request.url);
|
|
if (this.#manifest.i18n && (this.#manifest.i18n.strategy === "domains-prefix-always" || this.#manifest.i18n.strategy === "domains-prefix-other-locales" || this.#manifest.i18n.strategy === "domains-prefix-always-no-redirect")) {
|
|
let host = request.headers.get("X-Forwarded-Host");
|
|
let protocol = request.headers.get("X-Forwarded-Proto");
|
|
if (protocol) {
|
|
protocol = protocol + ":";
|
|
} else {
|
|
protocol = url.protocol;
|
|
}
|
|
if (!host) {
|
|
host = request.headers.get("Host");
|
|
}
|
|
if (host && protocol) {
|
|
host = host.split(":")[0];
|
|
try {
|
|
let locale;
|
|
const hostAsUrl = new URL(`${protocol}//${host}`);
|
|
for (const [domainKey, localeValue] of Object.entries(
|
|
this.#manifest.i18n.domainLookupTable
|
|
)) {
|
|
const domainKeyAsUrl = new URL(domainKey);
|
|
if (hostAsUrl.host === domainKeyAsUrl.host && hostAsUrl.protocol === domainKeyAsUrl.protocol) {
|
|
locale = localeValue;
|
|
break;
|
|
}
|
|
}
|
|
if (locale) {
|
|
pathname = prependForwardSlash$1(
|
|
joinPaths(normalizeTheLocale(locale), this.removeBase(url.pathname))
|
|
);
|
|
if (url.pathname.endsWith("/")) {
|
|
pathname = appendForwardSlash$1(pathname);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
this.#logger.error(
|
|
"router",
|
|
`Astro tried to parse ${protocol}//${host} as an URL, but it threw a parsing error. Check the X-Forwarded-Host and X-Forwarded-Proto headers.`
|
|
);
|
|
this.#logger.error("router", `Error: ${e}`);
|
|
}
|
|
}
|
|
}
|
|
return pathname;
|
|
}
|
|
async render(request, routeDataOrOptions, maybeLocals) {
|
|
let routeData;
|
|
let locals;
|
|
let clientAddress;
|
|
let addCookieHeader;
|
|
if (routeDataOrOptions && ("addCookieHeader" in routeDataOrOptions || "clientAddress" in routeDataOrOptions || "locals" in routeDataOrOptions || "routeData" in routeDataOrOptions)) {
|
|
if ("addCookieHeader" in routeDataOrOptions) {
|
|
addCookieHeader = routeDataOrOptions.addCookieHeader;
|
|
}
|
|
if ("clientAddress" in routeDataOrOptions) {
|
|
clientAddress = routeDataOrOptions.clientAddress;
|
|
}
|
|
if ("routeData" in routeDataOrOptions) {
|
|
routeData = routeDataOrOptions.routeData;
|
|
}
|
|
if ("locals" in routeDataOrOptions) {
|
|
locals = routeDataOrOptions.locals;
|
|
}
|
|
} else {
|
|
routeData = routeDataOrOptions;
|
|
locals = maybeLocals;
|
|
if (routeDataOrOptions || locals) {
|
|
this.#logRenderOptionsDeprecationWarning();
|
|
}
|
|
}
|
|
if (routeData) {
|
|
this.#logger.debug(
|
|
"router",
|
|
"The adapter " + this.#manifest.adapterName + " provided a custom RouteData for ",
|
|
request.url
|
|
);
|
|
this.#logger.debug("router", "RouteData:\n" + routeData);
|
|
}
|
|
if (locals) {
|
|
if (typeof locals !== "object") {
|
|
const error = new AstroError(LocalsNotAnObject);
|
|
this.#logger.error(null, error.stack);
|
|
return this.#renderError(request, { status: 500, error });
|
|
}
|
|
Reflect.set(request, clientLocalsSymbol, locals);
|
|
}
|
|
if (clientAddress) {
|
|
Reflect.set(request, clientAddressSymbol, clientAddress);
|
|
}
|
|
if (!routeData) {
|
|
routeData = this.match(request);
|
|
this.#logger.debug("router", "Astro matched the following route for " + request.url);
|
|
this.#logger.debug("router", "RouteData:\n" + routeData);
|
|
}
|
|
if (!routeData) {
|
|
this.#logger.debug("router", "Astro hasn't found routes that match " + request.url);
|
|
this.#logger.debug("router", "Here's the available routes:\n", this.#manifestData);
|
|
return this.#renderError(request, { locals, status: 404 });
|
|
}
|
|
const pathname = this.#getPathnameFromRequest(request);
|
|
const defaultStatus = this.#getDefaultStatusCode(routeData, pathname);
|
|
let response;
|
|
try {
|
|
const mod = await this.#pipeline.getModuleForRoute(routeData);
|
|
const renderContext = await RenderContext.create({
|
|
pipeline: this.#pipeline,
|
|
locals,
|
|
pathname,
|
|
request,
|
|
routeData,
|
|
status: defaultStatus
|
|
});
|
|
response = await renderContext.render(await mod.page());
|
|
} catch (err) {
|
|
this.#logger.error(null, err.stack || err.message || String(err));
|
|
return this.#renderError(request, { locals, status: 500, error: err });
|
|
}
|
|
if (REROUTABLE_STATUS_CODES.includes(response.status) && response.headers.get(REROUTE_DIRECTIVE_HEADER) !== "no") {
|
|
return this.#renderError(request, {
|
|
locals,
|
|
response,
|
|
status: response.status,
|
|
// We don't have an error to report here. Passing null means we pass nothing intentionally
|
|
// while undefined means there's no error
|
|
error: response.status === 500 ? null : void 0
|
|
});
|
|
}
|
|
if (response.headers.has(REROUTE_DIRECTIVE_HEADER)) {
|
|
response.headers.delete(REROUTE_DIRECTIVE_HEADER);
|
|
}
|
|
if (addCookieHeader) {
|
|
for (const setCookieHeaderValue of App.getSetCookieFromResponse(response)) {
|
|
response.headers.append("set-cookie", setCookieHeaderValue);
|
|
}
|
|
}
|
|
Reflect.set(response, responseSentSymbol$1, true);
|
|
return response;
|
|
}
|
|
#logRenderOptionsDeprecationWarning() {
|
|
if (this.#renderOptionsDeprecationWarningShown) return;
|
|
this.#logger.warn(
|
|
"deprecated",
|
|
`The adapter ${this.#manifest.adapterName} is using a deprecated signature of the 'app.render()' method. From Astro 4.0, locals and routeData are provided as properties on an optional object to this method. Using the old signature will cause an error in Astro 5.0. See https://github.com/withastro/astro/pull/9199 for more information.`
|
|
);
|
|
this.#renderOptionsDeprecationWarningShown = true;
|
|
}
|
|
setCookieHeaders(response) {
|
|
return getSetCookiesFromResponse(response);
|
|
}
|
|
/**
|
|
* Reads all the cookies written by `Astro.cookie.set()` onto the passed response.
|
|
* For example,
|
|
* ```ts
|
|
* for (const cookie_ of App.getSetCookieFromResponse(response)) {
|
|
* const cookie: string = cookie_
|
|
* }
|
|
* ```
|
|
* @param response The response to read cookies from.
|
|
* @returns An iterator that yields key-value pairs as equal-sign-separated strings.
|
|
*/
|
|
static getSetCookieFromResponse = getSetCookiesFromResponse;
|
|
/**
|
|
* If it is a known error code, try sending the according page (e.g. 404.astro / 500.astro).
|
|
* This also handles pre-rendered /404 or /500 routes
|
|
*/
|
|
async #renderError(request, {
|
|
locals,
|
|
status,
|
|
response: originalResponse,
|
|
skipMiddleware = false,
|
|
error
|
|
}) {
|
|
const errorRoutePath = `/${status}${this.#manifest.trailingSlash === "always" ? "/" : ""}`;
|
|
const errorRouteData = matchRoute(errorRoutePath, this.#manifestData);
|
|
const url = new URL(request.url);
|
|
if (errorRouteData) {
|
|
if (errorRouteData.prerender) {
|
|
const maybeDotHtml = errorRouteData.route.endsWith(`/${status}`) ? ".html" : "";
|
|
const statusURL = new URL(
|
|
`${this.#baseWithoutTrailingSlash}/${status}${maybeDotHtml}`,
|
|
url
|
|
);
|
|
if (statusURL.toString() !== request.url) {
|
|
const response2 = await fetch(statusURL.toString());
|
|
const override = { status };
|
|
return this.#mergeResponses(response2, originalResponse, override);
|
|
}
|
|
}
|
|
const mod = await this.#pipeline.getModuleForRoute(errorRouteData);
|
|
try {
|
|
const renderContext = await RenderContext.create({
|
|
locals,
|
|
pipeline: this.#pipeline,
|
|
middleware: skipMiddleware ? NOOP_MIDDLEWARE_FN : void 0,
|
|
pathname: this.#getPathnameFromRequest(request),
|
|
request,
|
|
routeData: errorRouteData,
|
|
status,
|
|
props: { error }
|
|
});
|
|
const response2 = await renderContext.render(await mod.page());
|
|
return this.#mergeResponses(response2, originalResponse);
|
|
} catch {
|
|
if (skipMiddleware === false) {
|
|
return this.#renderError(request, {
|
|
locals,
|
|
status,
|
|
response: originalResponse,
|
|
skipMiddleware: true
|
|
});
|
|
}
|
|
}
|
|
}
|
|
const response = this.#mergeResponses(new Response(null, { status }), originalResponse);
|
|
Reflect.set(response, responseSentSymbol$1, true);
|
|
return response;
|
|
}
|
|
#mergeResponses(newResponse, originalResponse, override) {
|
|
if (!originalResponse) {
|
|
if (override !== void 0) {
|
|
return new Response(newResponse.body, {
|
|
status: override.status,
|
|
statusText: newResponse.statusText,
|
|
headers: newResponse.headers
|
|
});
|
|
}
|
|
return newResponse;
|
|
}
|
|
const status = override?.status ? override.status : originalResponse.status === 200 ? newResponse.status : originalResponse.status;
|
|
try {
|
|
originalResponse.headers.delete("Content-type");
|
|
} catch {
|
|
}
|
|
return new Response(newResponse.body, {
|
|
status,
|
|
statusText: status === 200 ? newResponse.statusText : originalResponse.statusText,
|
|
// If you're looking at here for possible bugs, it means that it's not a bug.
|
|
// With the middleware, users can meddle with headers, and we should pass to the 404/500.
|
|
// If users see something weird, it's because they are setting some headers they should not.
|
|
//
|
|
// Although, we don't want it to replace the content-type, because the error page must return `text/html`
|
|
headers: new Headers([
|
|
...Array.from(newResponse.headers),
|
|
...Array.from(originalResponse.headers)
|
|
])
|
|
});
|
|
}
|
|
#getDefaultStatusCode(routeData, pathname) {
|
|
if (!routeData.pattern.test(pathname)) {
|
|
for (const fallbackRoute of routeData.fallbackRoutes) {
|
|
if (fallbackRoute.pattern.test(pathname)) {
|
|
return 302;
|
|
}
|
|
}
|
|
}
|
|
const route = removeTrailingForwardSlash(routeData.route);
|
|
if (route.endsWith("/404")) return 404;
|
|
if (route.endsWith("/500")) return 500;
|
|
return 200;
|
|
}
|
|
}
|
|
|
|
const createOutgoingHttpHeaders = (headers) => {
|
|
if (!headers) {
|
|
return void 0;
|
|
}
|
|
const nodeHeaders = Object.fromEntries(headers.entries());
|
|
if (Object.keys(nodeHeaders).length === 0) {
|
|
return void 0;
|
|
}
|
|
if (headers.has("set-cookie")) {
|
|
const cookieHeaders = headers.getSetCookie();
|
|
if (cookieHeaders.length > 1) {
|
|
nodeHeaders["set-cookie"] = cookieHeaders;
|
|
}
|
|
}
|
|
return nodeHeaders;
|
|
};
|
|
|
|
function apply() {
|
|
if (!globalThis.crypto) {
|
|
Object.defineProperty(globalThis, "crypto", {
|
|
value: crypto.webcrypto
|
|
});
|
|
}
|
|
if (!globalThis.File) {
|
|
Object.defineProperty(globalThis, "File", {
|
|
value: buffer.File
|
|
});
|
|
}
|
|
}
|
|
|
|
class NodeApp extends App {
|
|
match(req) {
|
|
if (!(req instanceof Request)) {
|
|
req = NodeApp.createRequest(req, {
|
|
skipBody: true
|
|
});
|
|
}
|
|
return super.match(req);
|
|
}
|
|
render(req, routeDataOrOptions, maybeLocals) {
|
|
if (!(req instanceof Request)) {
|
|
req = NodeApp.createRequest(req);
|
|
}
|
|
return super.render(req, routeDataOrOptions, maybeLocals);
|
|
}
|
|
/**
|
|
* Converts a NodeJS IncomingMessage into a web standard Request.
|
|
* ```js
|
|
* import { NodeApp } from 'astro/app/node';
|
|
* import { createServer } from 'node:http';
|
|
*
|
|
* const server = createServer(async (req, res) => {
|
|
* const request = NodeApp.createRequest(req);
|
|
* const response = await app.render(request);
|
|
* await NodeApp.writeResponse(response, res);
|
|
* })
|
|
* ```
|
|
*/
|
|
static createRequest(req, { skipBody = false } = {}) {
|
|
const isEncrypted = "encrypted" in req.socket && req.socket.encrypted;
|
|
const getFirstForwardedValue = (multiValueHeader) => {
|
|
return multiValueHeader?.toString()?.split(",").map((e) => e.trim())?.[0];
|
|
};
|
|
const forwardedProtocol = getFirstForwardedValue(req.headers["x-forwarded-proto"]);
|
|
const protocol = forwardedProtocol ?? (isEncrypted ? "https" : "http");
|
|
const forwardedHostname = getFirstForwardedValue(req.headers["x-forwarded-host"]);
|
|
const hostname = forwardedHostname ?? req.headers.host ?? req.headers[":authority"];
|
|
const port = getFirstForwardedValue(req.headers["x-forwarded-port"]);
|
|
const portInHostname = typeof hostname === "string" && /:\d+$/.test(hostname);
|
|
const hostnamePort = portInHostname ? hostname : `${hostname}${port ? `:${port}` : ""}`;
|
|
const url = `${protocol}://${hostnamePort}${req.url}`;
|
|
const options = {
|
|
method: req.method || "GET",
|
|
headers: makeRequestHeaders(req)
|
|
};
|
|
const bodyAllowed = options.method !== "HEAD" && options.method !== "GET" && skipBody === false;
|
|
if (bodyAllowed) {
|
|
Object.assign(options, makeRequestBody(req));
|
|
}
|
|
const request = new Request(url, options);
|
|
const forwardedClientIp = getFirstForwardedValue(req.headers["x-forwarded-for"]);
|
|
const clientIp = forwardedClientIp || req.socket?.remoteAddress;
|
|
if (clientIp) {
|
|
Reflect.set(request, clientAddressSymbol, clientIp);
|
|
}
|
|
return request;
|
|
}
|
|
/**
|
|
* Streams a web-standard Response into a NodeJS Server Response.
|
|
* ```js
|
|
* import { NodeApp } from 'astro/app/node';
|
|
* import { createServer } from 'node:http';
|
|
*
|
|
* const server = createServer(async (req, res) => {
|
|
* const request = NodeApp.createRequest(req);
|
|
* const response = await app.render(request);
|
|
* await NodeApp.writeResponse(response, res);
|
|
* })
|
|
* ```
|
|
* @param source WhatWG Response
|
|
* @param destination NodeJS ServerResponse
|
|
*/
|
|
static async writeResponse(source, destination) {
|
|
const { status, headers, body, statusText } = source;
|
|
if (!(destination instanceof Http2ServerResponse)) {
|
|
destination.statusMessage = statusText;
|
|
}
|
|
destination.writeHead(status, createOutgoingHttpHeaders(headers));
|
|
if (!body) return destination.end();
|
|
try {
|
|
const reader = body.getReader();
|
|
destination.on("close", () => {
|
|
reader.cancel().catch((err) => {
|
|
console.error(
|
|
`There was an uncaught error in the middle of the stream while rendering ${destination.req.url}.`,
|
|
err
|
|
);
|
|
});
|
|
});
|
|
let result = await reader.read();
|
|
while (!result.done) {
|
|
destination.write(result.value);
|
|
result = await reader.read();
|
|
}
|
|
destination.end();
|
|
} catch (err) {
|
|
destination.write("Internal server error", () => {
|
|
err instanceof Error ? destination.destroy(err) : destination.destroy();
|
|
});
|
|
}
|
|
}
|
|
}
|
|
function makeRequestHeaders(req) {
|
|
const headers = new Headers();
|
|
for (const [name, value] of Object.entries(req.headers)) {
|
|
if (value === void 0) {
|
|
continue;
|
|
}
|
|
if (Array.isArray(value)) {
|
|
for (const item of value) {
|
|
headers.append(name, item);
|
|
}
|
|
} else {
|
|
headers.append(name, value);
|
|
}
|
|
}
|
|
return headers;
|
|
}
|
|
function makeRequestBody(req) {
|
|
if (req.body !== void 0) {
|
|
if (typeof req.body === "string" && req.body.length > 0) {
|
|
return { body: Buffer.from(req.body) };
|
|
}
|
|
if (typeof req.body === "object" && req.body !== null && Object.keys(req.body).length > 0) {
|
|
return { body: Buffer.from(JSON.stringify(req.body)) };
|
|
}
|
|
if (typeof req.body === "object" && req.body !== null && typeof req.body[Symbol.asyncIterator] !== "undefined") {
|
|
return asyncIterableToBodyProps(req.body);
|
|
}
|
|
}
|
|
return asyncIterableToBodyProps(req);
|
|
}
|
|
function asyncIterableToBodyProps(iterable) {
|
|
return {
|
|
// Node uses undici for the Request implementation. Undici accepts
|
|
// a non-standard async iterable for the body.
|
|
// @ts-expect-error
|
|
body: iterable,
|
|
// The duplex property is required when using a ReadableStream or async
|
|
// iterable for the body. The type definitions do not include the duplex
|
|
// property because they are not up-to-date.
|
|
duplex: "half"
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Creates a Node.js http listener for on-demand rendered pages, compatible with http.createServer and Connect middleware.
|
|
* If the next callback is provided, it will be called if the request does not have a matching route.
|
|
* Intended to be used in both standalone and middleware mode.
|
|
*/
|
|
function createAppHandler(app) {
|
|
/**
|
|
* Keep track of the current request path using AsyncLocalStorage.
|
|
* Used to log unhandled rejections with a helpful message.
|
|
*/
|
|
const als = new AsyncLocalStorage();
|
|
const logger = app.getAdapterLogger();
|
|
process.on('unhandledRejection', (reason) => {
|
|
const requestUrl = als.getStore();
|
|
logger.error(`Unhandled rejection while rendering ${requestUrl}`);
|
|
console.error(reason);
|
|
});
|
|
return async (req, res, next, locals) => {
|
|
let request;
|
|
try {
|
|
request = NodeApp.createRequest(req);
|
|
}
|
|
catch (err) {
|
|
logger.error(`Could not render ${req.url}`);
|
|
console.error(err);
|
|
res.statusCode = 500;
|
|
res.end('Internal Server Error');
|
|
return;
|
|
}
|
|
const routeData = app.match(request);
|
|
if (routeData) {
|
|
const response = await als.run(request.url, () => app.render(request, {
|
|
addCookieHeader: true,
|
|
locals,
|
|
routeData,
|
|
}));
|
|
await NodeApp.writeResponse(response, res);
|
|
}
|
|
else if (next) {
|
|
return next();
|
|
}
|
|
else {
|
|
const response = await app.render(req);
|
|
await NodeApp.writeResponse(response, res);
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Creates a middleware that can be used with Express, Connect, etc.
|
|
*
|
|
* Similar to `createAppHandler` but can additionally be placed in the express
|
|
* chain as an error middleware.
|
|
*
|
|
* https://expressjs.com/en/guide/using-middleware.html#middleware.error-handling
|
|
*/
|
|
function createMiddleware(app) {
|
|
const handler = createAppHandler(app);
|
|
const logger = app.getAdapterLogger();
|
|
// using spread args because express trips up if the function's
|
|
// stringified body includes req, res, next, locals directly
|
|
return async (...args) => {
|
|
// assume normal invocation at first
|
|
const [req, res, next, locals] = args;
|
|
// short circuit if it is an error invocation
|
|
if (req instanceof Error) {
|
|
const error = req;
|
|
if (next) {
|
|
return next(error);
|
|
// biome-ignore lint/style/noUselessElse: <explanation>
|
|
}
|
|
else {
|
|
throw error;
|
|
}
|
|
}
|
|
try {
|
|
await handler(req, res, next, locals);
|
|
}
|
|
catch (err) {
|
|
logger.error(`Could not render ${req.url}`);
|
|
console.error(err);
|
|
if (!res.headersSent) {
|
|
// biome-ignore lint/style/noUnusedTemplateLiteral: <explanation>
|
|
res.writeHead(500, `Server error`);
|
|
res.end();
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
async function logListeningOn(logger, server, options) {
|
|
await new Promise((resolve) => server.once('listening', resolve));
|
|
const protocol = server instanceof https.Server ? 'https' : 'http';
|
|
// Allow to provide host value at runtime
|
|
const host = getResolvedHostForHttpServer(process.env.HOST !== undefined && process.env.HOST !== '' ? process.env.HOST : options.host);
|
|
const { port } = server.address();
|
|
const address = getNetworkAddress(protocol, host, port);
|
|
if (host === undefined) {
|
|
logger.info(`Server listening on \n local: ${address.local[0]} \t\n network: ${address.network[0]}\n`);
|
|
}
|
|
else {
|
|
logger.info(`Server listening on ${address.local[0]}`);
|
|
}
|
|
}
|
|
function getResolvedHostForHttpServer(host) {
|
|
if (host === false) {
|
|
// Use a secure default
|
|
return 'localhost';
|
|
// biome-ignore lint/style/noUselessElse: <explanation>
|
|
}
|
|
else if (host === true) {
|
|
// If passed --host in the CLI without arguments
|
|
return undefined; // undefined typically means 0.0.0.0 or :: (listen on all IPs)
|
|
// biome-ignore lint/style/noUselessElse: <explanation>
|
|
}
|
|
else {
|
|
return host;
|
|
}
|
|
}
|
|
const wildcardHosts = new Set(['0.0.0.0', '::', '0000:0000:0000:0000:0000:0000:0000:0000']);
|
|
// this code from vite https://github.com/vitejs/vite/blob/d09bbd093a4b893e78f0bbff5b17c7cf7821f403/packages/vite/src/node/utils.ts#L892-L914
|
|
function getNetworkAddress(
|
|
// biome-ignore lint/style/useDefaultParameterLast: <explanation>
|
|
protocol = 'http', hostname, port, base) {
|
|
const NetworkAddress = {
|
|
local: [],
|
|
network: [],
|
|
};
|
|
// biome-ignore lint/complexity/noForEach: <explanation>
|
|
Object.values(os.networkInterfaces())
|
|
.flatMap((nInterface) => nInterface ?? [])
|
|
.filter((detail) =>
|
|
// biome-ignore lint/complexity/useOptionalChain: <explanation>
|
|
detail &&
|
|
detail.address &&
|
|
(detail.family === 'IPv4' ||
|
|
// @ts-expect-error Node 18.0 - 18.3 returns number
|
|
detail.family === 4))
|
|
.forEach((detail) => {
|
|
let host = detail.address.replace('127.0.0.1', hostname === undefined || wildcardHosts.has(hostname) ? 'localhost' : hostname);
|
|
// ipv6 host
|
|
if (host.includes(':')) {
|
|
host = `[${host}]`;
|
|
}
|
|
const url = `${protocol}://${host}:${port}${''}`;
|
|
if (detail.address.includes('127.0.0.1')) {
|
|
NetworkAddress.local.push(url);
|
|
}
|
|
else {
|
|
NetworkAddress.network.push(url);
|
|
}
|
|
});
|
|
return NetworkAddress;
|
|
}
|
|
|
|
// check for a dot followed by a extension made up of lowercase characters
|
|
const isSubresourceRegex = /.+\.[a-z]+$/i;
|
|
/**
|
|
* Creates a Node.js http listener for static files and prerendered pages.
|
|
* In standalone mode, the static handler is queried first for the static files.
|
|
* If one matching the request path is not found, it relegates to the SSR handler.
|
|
* Intended to be used only in the standalone mode.
|
|
*/
|
|
function createStaticHandler(app, options) {
|
|
const client = resolveClientDir(options);
|
|
/**
|
|
* @param ssr The SSR handler to be called if the static handler does not find a matching file.
|
|
*/
|
|
return (req, res, ssr) => {
|
|
if (req.url) {
|
|
const [urlPath, urlQuery] = req.url.split('?');
|
|
const filePath = path.join(client, app.removeBase(urlPath));
|
|
let pathname;
|
|
let isDirectory = false;
|
|
try {
|
|
isDirectory = fs.lstatSync(filePath).isDirectory();
|
|
}
|
|
catch { }
|
|
const { trailingSlash = 'ignore' } = options;
|
|
const hasSlash = urlPath.endsWith('/');
|
|
switch (trailingSlash) {
|
|
case 'never':
|
|
// biome-ignore lint/suspicious/noDoubleEquals: <explanation>
|
|
if (isDirectory && urlPath != '/' && hasSlash) {
|
|
// biome-ignore lint/style/useTemplate: <explanation>
|
|
// biome-ignore lint/suspicious/noFallthroughSwitchClause: <explanation>
|
|
pathname = urlPath.slice(0, -1) + (urlQuery ? '?' + urlQuery : '');
|
|
res.statusCode = 301;
|
|
res.setHeader('Location', pathname);
|
|
return res.end();
|
|
// biome-ignore lint/style/noUselessElse: <explanation>
|
|
}
|
|
else
|
|
pathname = urlPath;
|
|
// intentionally fall through
|
|
case 'ignore':
|
|
{
|
|
if (isDirectory && !hasSlash) {
|
|
// biome-ignore lint/style/useTemplate: <explanation>
|
|
pathname = urlPath + '/index.html';
|
|
}
|
|
else
|
|
pathname = urlPath;
|
|
}
|
|
break;
|
|
case 'always':
|
|
// trailing slash is not added to "subresources"
|
|
if (!hasSlash && !isSubresourceRegex.test(urlPath)) {
|
|
// biome-ignore lint/style/useTemplate: <explanation>
|
|
pathname = urlPath + '/' + (urlQuery ? '?' + urlQuery : '');
|
|
res.statusCode = 301;
|
|
res.setHeader('Location', pathname);
|
|
return res.end();
|
|
// biome-ignore lint/style/noUselessElse: <explanation>
|
|
}
|
|
else
|
|
pathname = urlPath;
|
|
break;
|
|
}
|
|
// app.removeBase sometimes returns a path without a leading slash
|
|
pathname = prependForwardSlash(app.removeBase(pathname));
|
|
const stream = send(req, pathname, {
|
|
root: client,
|
|
dotfiles: pathname.startsWith('/.well-known/') ? 'allow' : 'deny',
|
|
});
|
|
let forwardError = false;
|
|
stream.on('error', (err) => {
|
|
if (forwardError) {
|
|
console.error(err.toString());
|
|
res.writeHead(500);
|
|
res.end('Internal server error');
|
|
return;
|
|
}
|
|
// File not found, forward to the SSR handler
|
|
ssr();
|
|
});
|
|
stream.on('headers', (_res) => {
|
|
// assets in dist/_astro are hashed and should get the immutable header
|
|
if (pathname.startsWith(`/${options.assets}/`)) {
|
|
// This is the "far future" cache header, used for static files whose name includes their digest hash.
|
|
// 1 year (31,536,000 seconds) is convention.
|
|
// Taken from https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#immutable
|
|
_res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
|
|
}
|
|
});
|
|
stream.on('file', () => {
|
|
forwardError = true;
|
|
});
|
|
stream.pipe(res);
|
|
}
|
|
else {
|
|
ssr();
|
|
}
|
|
};
|
|
}
|
|
function resolveClientDir(options) {
|
|
const clientURLRaw = new URL(options.client);
|
|
const serverURLRaw = new URL(options.server);
|
|
const rel = path.relative(url.fileURLToPath(serverURLRaw), url.fileURLToPath(clientURLRaw));
|
|
// walk up the parent folders until you find the one that is the root of the server entry folder. This is how we find the client folder relatively.
|
|
const serverFolder = path.basename(options.server);
|
|
let serverEntryFolderURL = path.dirname(import.meta.url);
|
|
while (!serverEntryFolderURL.endsWith(serverFolder)) {
|
|
serverEntryFolderURL = path.dirname(serverEntryFolderURL);
|
|
}
|
|
// biome-ignore lint/style/useTemplate: <explanation>
|
|
const serverEntryURL = serverEntryFolderURL + '/entry.mjs';
|
|
const clientURL = new URL(appendForwardSlash(rel), serverEntryURL);
|
|
const client = url.fileURLToPath(clientURL);
|
|
return client;
|
|
}
|
|
function prependForwardSlash(pth) {
|
|
// biome-ignore lint/style/useTemplate: <explanation>
|
|
return pth.startsWith('/') ? pth : '/' + pth;
|
|
}
|
|
function appendForwardSlash(pth) {
|
|
// biome-ignore lint/style/useTemplate: <explanation>
|
|
return pth.endsWith('/') ? pth : pth + '/';
|
|
}
|
|
|
|
// Used to get Host Value at Runtime
|
|
const hostOptions = (host) => {
|
|
if (typeof host === 'boolean') {
|
|
return host ? '0.0.0.0' : 'localhost';
|
|
}
|
|
return host;
|
|
};
|
|
function standalone(app, options) {
|
|
const port = process.env.PORT ? Number(process.env.PORT) : (options.port ?? 8080);
|
|
const host = process.env.HOST ?? hostOptions(options.host);
|
|
const handler = createStandaloneHandler(app, options);
|
|
const server = createServer(handler, host, port);
|
|
server.server.listen(port, host);
|
|
if (process.env.ASTRO_NODE_LOGGING !== 'disabled') {
|
|
logListeningOn(app.getAdapterLogger(), server.server, options);
|
|
}
|
|
return {
|
|
server,
|
|
done: server.closed(),
|
|
};
|
|
}
|
|
// also used by server entrypoint
|
|
function createStandaloneHandler(app, options) {
|
|
const appHandler = createAppHandler(app);
|
|
const staticHandler = createStaticHandler(app, options);
|
|
return (req, res) => {
|
|
try {
|
|
// validate request path
|
|
// biome-ignore lint/style/noNonNullAssertion: <explanation>
|
|
decodeURI(req.url);
|
|
}
|
|
catch {
|
|
res.writeHead(400);
|
|
res.end('Bad request.');
|
|
return;
|
|
}
|
|
staticHandler(req, res, () => appHandler(req, res));
|
|
};
|
|
}
|
|
// also used by preview entrypoint
|
|
function createServer(listener, host, port) {
|
|
let httpServer;
|
|
if (process.env.SERVER_CERT_PATH && process.env.SERVER_KEY_PATH) {
|
|
httpServer = https.createServer({
|
|
key: fs.readFileSync(process.env.SERVER_KEY_PATH),
|
|
cert: fs.readFileSync(process.env.SERVER_CERT_PATH),
|
|
}, listener);
|
|
}
|
|
else {
|
|
httpServer = http.createServer(listener);
|
|
}
|
|
enableDestroy(httpServer);
|
|
// Resolves once the server is closed
|
|
const closed = new Promise((resolve, reject) => {
|
|
httpServer.addListener('close', resolve);
|
|
httpServer.addListener('error', reject);
|
|
});
|
|
const previewable = {
|
|
host,
|
|
port,
|
|
closed() {
|
|
return closed;
|
|
},
|
|
async stop() {
|
|
await new Promise((resolve, reject) => {
|
|
httpServer.destroy((err) => (err ? reject(err) : resolve(undefined)));
|
|
});
|
|
},
|
|
};
|
|
return {
|
|
server: httpServer,
|
|
...previewable,
|
|
};
|
|
}
|
|
|
|
// This needs to run first because some internals depend on `crypto`
|
|
apply();
|
|
// Won't throw if the virtual module is not available because it's not supported in
|
|
// the users's astro version or if astro:env is not enabled in the project
|
|
await import('./astro/env-setup_Cr6XTFvb.mjs')
|
|
.then((mod) => mod.setGetEnv((key) => process.env[key]))
|
|
.catch(() => { });
|
|
function createExports(manifest, options) {
|
|
const app = new NodeApp(manifest);
|
|
options.trailingSlash = manifest.trailingSlash;
|
|
return {
|
|
options: options,
|
|
handler: options.mode === 'middleware' ? createMiddleware(app) : createStandaloneHandler(app, options),
|
|
startServer: () => standalone(app, options),
|
|
};
|
|
}
|
|
function start(manifest, options) {
|
|
if (options.mode !== 'standalone' || process.env.ASTRO_NODE_AUTOSTART === 'disabled') {
|
|
return;
|
|
}
|
|
const app = new NodeApp(manifest);
|
|
standalone(app, options);
|
|
}
|
|
|
|
const serverEntrypointModule = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
|
|
__proto__: null,
|
|
createExports,
|
|
start
|
|
}, Symbol.toStringTag, { value: 'Module' }));
|
|
|
|
export { start as a, createExports as c, serverEntrypointModule as s };
|