UNPKG

astro

Version:

Astro is a modern site builder with web best practices, performance, and DX front-of-mind.

377 lines (376 loc) 13 kB
import { prependForwardSlash, removeTrailingForwardSlash, stripRequestBase } from "@astrojs/internal-helpers/path"; import { matchPattern } from "@astrojs/internal-helpers/remote"; import { computePathnameFromDomain } from "../i18n/domain.js"; import { ASTRO_ERROR_HEADER, clientAddressSymbol } from "../constants.js"; import { getSetCookiesFromResponse } from "../cookies/index.js"; import { AstroError, AstroErrorData } from "../errors/index.js"; import { AstroIntegrationLogger } from "../logger/core.js"; import { DefaultFetchHandler } from "../fetch/default-handler.js"; import { getUsedFeatures, FetchFeatures } from "../fetch/features.js"; import { FetchState } from "../fetch/fetch-state.js"; import { renderErrorPage } from "../errors/handler.js"; import { getLogger, getResolvedLogger } from "../logger/manifest-logger.js"; import { handleRequest } from "../routing/handler.js"; import { getDefaultStatusCode } from "../routing/helpers.js"; import { matchRequest } from "../routing/match-request.js"; import { getRouteTable, matchRoute, updateRouteTable } from "../routing/route-table.js"; import { validateAndDecodePathname } from "../util/pathname.js"; import { setRenderOptions } from "./render-options.js"; class BaseApp { manifest; #adapterLogger; baseWithoutTrailingSlash; /** * The streaming flag passed to the constructor, surfaced through the * protected `resolveStreaming()` hook and fed into the internal * `FetchState` facade hooks on the fast path. */ #streaming; /** * The handler that turns incoming `Request` objects into `Response`s. * Defaults to a `DefaultFetchHandler` pinned to this app and can be * overridden via `setFetchHandler` — typically by the bundled * entrypoint after importing `virtual:astro:fetchable`. */ #fetchHandler; #errorHandler; /** * Whether a custom fetch handler (from `src/fetch.ts`) has been set * via `setFetchHandler`. When false, the `DefaultFetchHandler` is * in use and all features are implicitly active. */ #hasCustomFetchHandler = false; /** * Whether the missing-feature check has already run. We only want * to warn once — after the first request in dev, or at build end. */ #featureCheckDone = false; get logger() { return getLogger(this.manifest); } /** * Route data derived from the manifest, used for route matching. Reads and * writes go through the single per-manifest route table, so HMR updates are * visible to every consumer at once. */ get manifestData() { return getRouteTable(this.manifest); } set manifestData(routesList) { updateRouteTable(this.manifest, routesList.routes); } get adapterLogger() { const currentOptions = this.logger.options; if (!this.#adapterLogger || this.#adapterLogger.options !== currentOptions) { this.#adapterLogger = new AstroIntegrationLogger(currentOptions, this.manifest.adapterName); } return this.#adapterLogger; } constructor(manifest, streaming = true) { this.manifest = manifest; this.baseWithoutTrailingSlash = removeTrailingForwardSlash(manifest.base); this.#streaming = streaming; getRouteTable(manifest); getLogger(manifest); this.#fetchHandler = new DefaultFetchHandler(this); this.#errorHandler = this.createErrorHandler(); } /** * Resolves the user-configured logger destination from the manifest and * returns the logger. Lazy and only resolves once; safe to call before * the first render (adapters use this to log startup messages through * the configured destination). */ getLogger() { return getResolvedLogger(this.manifest); } /** * The streaming flag fed into the internal `FetchState` facade hooks on * the fast path. Returns the constructor flag by * default; `BuildApp` overrides this to return `undefined` so streaming * falls through to the environment default (`manifest.serverLike`). */ resolveStreaming() { return this.#streaming; } /** * Override the fetch handler used to dispatch requests. Entrypoints * call this with the default export of `virtual:astro:fetchable` to * plug in a user-authored handler from `src/fetch.ts`. */ setFetchHandler(handler) { this.#fetchHandler = handler; this.#hasCustomFetchHandler = !(handler instanceof DefaultFetchHandler); } /** * Returns the error handler used by this app. The default is a thin * bridge over the functional error API — strategy selection (production * default / dev / build) is environment-driven inside `renderErrorPage`. * External subclasses can override this to customize error rendering. */ createErrorHandler() { return { renderError: (request, options) => renderErrorPage(this.manifest, request, options) }; } /** * Resets the cached adapter logger so it picks up a new logger instance. * Used by BuildApp when the logger is replaced via setOptions(). */ resetAdapterLogger() { this.#adapterLogger = void 0; } getAllowedDomains() { return this.manifest.allowedDomains; } matchesAllowedDomains(forwardedHost, protocol) { return BaseApp.validateForwardedHost(forwardedHost, this.manifest.allowedDomains, protocol); } static validateForwardedHost(forwardedHost, allowedDomains, protocol) { if (!allowedDomains || allowedDomains.length === 0) { return false; } try { const testUrl = new URL(`${protocol || "https"}://${forwardedHost}`); return allowedDomains.some((pattern) => { return matchPattern(testUrl, pattern); }); } catch { return false; } } set setManifestData(newManifestData) { updateRouteTable(this.manifest, newManifestData.routes); } removeBase(pathname) { return stripRequestBase(pathname, this.manifest.base); } /** * Fully decodes a pathname, falling back to a single decode and then the raw pathname * when validation fails. Adapter matching runs before `render()`, so it must not throw * for request input that render-time validation handles. */ safeDecodePathname(pathname) { try { return validateAndDecodePathname(pathname); } catch (e) { this.adapterLogger.debug(e.toString()); try { return decodeURI(pathname); } catch { return pathname; } } } /** * Extracts the base-stripped, decoded pathname from a request. * Used by adapters to compute the pathname for dev-mode route matching. */ getPathnameFromRequest(request) { const url = new URL(request.url); const pathname = prependForwardSlash(this.removeBase(url.pathname)); return this.safeDecodePathname(pathname); } /** * Given a `Request`, it returns the `RouteData` that matches its `pathname`. By default, prerendered * routes aren't returned, even if they are matched. * * When `allowPrerenderedRoutes` is `true`, the function returns matched prerendered routes too. * @param request * @param allowPrerenderedRoutes */ match(request, allowPrerenderedRoutes = false) { return matchRequest(this.manifest, request, allowPrerenderedRoutes); } /** * A matching route function to use in the development server. * Contrary to the `.match` function, this function resolves props and params, returning the correct * route based on the priority, segments. It also returns the correct, resolved pathname. * @param pathname */ devMatch(pathname) { pathname; return void 0; } computePathnameFromDomain(request) { return computePathnameFromDomain( request, new URL(request.url), this.manifest.i18n, this.manifest.base, this.manifest.trailingSlash, this.logger ); } async render(request, { addCookieHeader = false, clientAddress = Reflect.get(request, clientAddressSymbol), locals, prerenderedErrorPageFetch = fetch, routeData, waitUntil } = {}) { await getResolvedLogger(this.manifest); if (routeData) { this.logger.debug( "router", "The adapter " + this.manifest.adapterName + " provided a custom RouteData for ", request.url ); this.logger.debug("router", "RouteData"); this.logger.debug("router", routeData); } if (locals) { if (typeof locals !== "object") { const error = new AstroError(AstroErrorData.LocalsNotAnObject); this.logger.error(null, error.stack); return this.renderError(request, { addCookieHeader, clientAddress, prerenderedErrorPageFetch, // If locals are invalid, we don't want to include them when // rendering the error page locals: void 0, routeData, waitUntil, status: 500, error }); } } if (!routeData) { const domainPathname = this.computePathnameFromDomain(request); if (domainPathname) { routeData = matchRoute(this.manifest, this.safeDecodePathname(domainPathname)); } } const resolvedOptions = { addCookieHeader, clientAddress, prerenderedErrorPageFetch, locals, routeData, waitUntil }; let response; if (this.#fetchHandler instanceof DefaultFetchHandler) { response = await handleRequest( new FetchState(this.manifest, request, resolvedOptions, { streaming: this.resolveStreaming(), renderError: (req, opts) => this.renderError(req, opts), logRequest: (payload) => this.logThisRequest(payload) }) ); } else { setRenderOptions(request, resolvedOptions); response = await this.#fetchHandler.fetch(request); } this.#warnMissingFeatures(); if (response.headers.get(ASTRO_ERROR_HEADER)) { response.headers.delete(ASTRO_ERROR_HEADER); return this.renderError(request, { addCookieHeader, clientAddress, prerenderedErrorPageFetch, locals, routeData, waitUntil, response, status: response.status, error: response.status === 500 ? null : void 0 }); } return response; } 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. * * Delegates to the app's configured `ErrorHandler`. To customize behavior * for a specific environment, override `createErrorHandler()` rather than * this method. */ async renderError(request, options) { return this.#errorHandler.renderError(request, options); } /** * One-shot check: after the first request with a custom `src/fetch.ts`, * compare `usedFeatures` against the manifest and warn about any * configured features the user's pipeline doesn't call. */ #warnMissingFeatures() { if (this.#featureCheckDone || !this.#hasCustomFetchHandler) return; this.#featureCheckDone = true; const manifest = this.manifest; const missing = []; const used = getUsedFeatures(this.manifest); if (manifest.routes.some((r) => r.routeData.type === "redirect") && !(used & FetchFeatures.redirects)) { missing.push("redirects"); } if (manifest.sessionConfig && !(used & FetchFeatures.sessions)) { missing.push("sessions"); } if (manifest.actions && !(used & FetchFeatures.actions)) { missing.push("actions"); } if (manifest.middleware && !(used & FetchFeatures.middleware)) { missing.push("middleware"); } if (manifest.i18n && manifest.i18n.strategy !== "manual" && !(used & FetchFeatures.i18n)) { missing.push("i18n"); } if (manifest.cacheConfig && !(used & FetchFeatures.cache)) { missing.push("cache"); } for (const feature of missing) { this.logger.warn( "router", `Your project uses ${feature}, but your custom src/fetch.ts does not call the ${feature}() handler. This feature will not work unless your fetch handler calls it.` ); } } getDefaultStatusCode(routeData, pathname) { return getDefaultStatusCode(this.manifest, routeData, pathname); } getManifest() { return this.manifest; } logThisRequest({ pathname, method, statusCode, isRewrite, timeStart }) { const timeEnd = performance.now(); this.logRequest({ pathname, method, statusCode, isRewrite, reqTime: timeEnd - timeStart }); } } export { BaseApp };