astro
Version:
Astro is a modern site builder with web best practices, performance, and DX front-of-mind.
218 lines (217 loc) • 8.01 kB
JavaScript
import { collapseDuplicateSlashes } from "@astrojs/internal-helpers/path";
import { shouldAppendForwardSlash } from "../build/util.js";
import { originPathnameSymbol } from "../constants.js";
import { AstroError, AstroErrorData } from "../errors/index.js";
import { appendForwardSlash, joinPaths, prependForwardSlash, removeTrailingForwardSlash, trimSlashes } from "../path.js";
import { createRequest } from "../request.js";
import { validateAndDecodePathname } from "../util/pathname.js";
import { DEFAULT_404_ROUTE } from "./internal/astro-designed-error-pages.js";
import { isRoute404, isRoute500 } from "./internal/route-errors.js";
/**
* Shared logic to retrieve the rewritten route. It returns a tuple that represents:
* 1. The new `Request` object. It contains `base`
* 2.
*/
export async function findRouteToRewrite({ payload, routes, request, trailingSlash, buildFormat, base, outDir, validate }) {
let newUrl = undefined;
if (payload instanceof URL) {
newUrl = payload;
} else if (payload instanceof Request) {
newUrl = new URL(payload.url);
} else {
newUrl = new URL(collapseDuplicateSlashes(payload), new URL(request.url).origin);
}
const { pathname, resolvedUrlPathname } = normalizeRewritePathname(newUrl.pathname, base, trailingSlash, buildFormat);
newUrl.pathname = resolvedUrlPathname;
// Decode the path the same way the first route match did (see
// `validateAndDecodePathname`) instead of calling `decodeURI` once here.
// Routing has to use the exact same path that middleware checked; decoding
// one extra time here is what let an encoded path slip past middleware and
// still reach a protected route. For an already-decoded path this changes
// nothing.
const decodedPathname = validateAndDecodePathname(pathname);
// Error pages (404/500) take precedence over dynamic routes that might
// capture the same path (e.g. [locale] matching /404). See #15098.
if (isRoute404(decodedPathname)) {
const errorRoute = routes.find((route) => isRoute404(route.route));
if (errorRoute) {
return {
routeData: errorRoute,
newUrl,
pathname: decodedPathname
};
}
}
if (isRoute500(decodedPathname)) {
const errorRoute = routes.find((route) => isRoute500(route.route));
if (errorRoute) {
return {
routeData: errorRoute,
newUrl,
pathname: decodedPathname
};
}
}
let foundRoute;
// A candidate whose `getStaticPaths()` throws is skipped, and the error is
// surfaced only when no candidate owns the path. Same rules as `matchRoute`.
let firstValidationError = null;
for (const route of routes) {
if (route.pattern.test(decodedPathname)) {
// If it's a dynamic route, make sure it actually generates the pathname
// Checking for params to make sure it's a dynamic route
if (route.params && route.params.length !== 0) {
if (route.distURL && route.distURL.length !== 0) {
// Remove outDir from beginning of distURL
// Remove /index.html or .html from end of distURL and compare with pathname
// Use pathname (encoded) instead of decodedPathname because url.href is encoded
if (!route.distURL.find((url) => url.href.replace(outDir.toString(), "").replace(/(?:\/index\.html|\.html)$/, "") === trimSlashes(pathname))) {
continue;
}
} else {
// `distURL` is empty outside a build, so ask the route whether
// `getStaticPaths()` produces this pathname, as `matchRoute` does.
let ownsPathname;
try {
ownsPathname = await validate(route, decodedPathname);
} catch (error) {
firstValidationError ??= error;
continue;
}
if (!ownsPathname) continue;
}
}
foundRoute = route;
break;
}
}
if (foundRoute) {
return {
routeData: foundRoute,
newUrl,
pathname: decodedPathname
};
} else if (firstValidationError) {
throw firstValidationError;
} else {
const custom404 = routes.find((route) => isRoute404(route.route));
if (custom404) {
return {
routeData: custom404,
newUrl,
pathname
};
} else {
return {
routeData: DEFAULT_404_ROUTE,
newUrl,
pathname
};
}
}
}
/**
* Utility function that creates a new `Request` with a new URL from an old `Request`.
*
* @param newUrl The new `URL`
* @param oldRequest The old `Request`
* @param isPrerendered It needs to be the flag of the previous routeData, before the rewrite
* @param logger
* @param routePattern
*/
export function copyRequest(newUrl, oldRequest, isPrerendered, logger, routePattern) {
const canHaveBody = oldRequest.method !== "GET" && oldRequest.method !== "HEAD";
if (canHaveBody && oldRequest.bodyUsed) {
throw new AstroError(AstroErrorData.RewriteWithBodyUsed);
}
return createRequest({
url: newUrl,
method: oldRequest.method,
body: canHaveBody ? oldRequest.body : undefined,
isPrerendered,
logger,
headers: isPrerendered ? {} : oldRequest.headers,
routePattern,
init: {
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 carrying over the body to a new request
duplex: "half"
}
});
}
export function setOriginPathname(request, pathname, trailingSlash, buildFormat) {
// Handle undefined pathname
if (!pathname) {
pathname = "/";
}
// Apply trailing slash logic based on configuration
const shouldAppendSlash = shouldAppendForwardSlash(trailingSlash, buildFormat);
let finalPathname;
// Special handling for root path
if (pathname === "/") {
// Root path always keeps the slash
finalPathname = "/";
} else if (shouldAppendSlash) {
finalPathname = appendForwardSlash(pathname);
} else {
finalPathname = removeTrailingForwardSlash(pathname);
}
Reflect.set(request, originPathnameSymbol, encodeURIComponent(finalPathname));
}
export function getOriginPathname(request) {
const origin = Reflect.get(request, originPathnameSymbol);
if (origin) {
return decodeURIComponent(origin);
}
return new URL(request.url).pathname;
}
/**
* Pure function that normalizes a rewrite target pathname by stripping the base,
* handling trailing slashes, removing `.html` suffixes, and computing the final
* full URL pathname (with base re-prepended).
*/
export function normalizeRewritePathname(urlPathname, base, trailingSlash, buildFormat) {
let pathname = collapseDuplicateSlashes(urlPathname);
const shouldAppendSlash = shouldAppendForwardSlash(trailingSlash, buildFormat);
// Special handling for base path
if (base !== "/") {
// Check if this is a request to the base path
const isBasePathRequest = urlPathname === base || urlPathname === removeTrailingForwardSlash(base);
if (isBasePathRequest) {
pathname = "/";
} else if (urlPathname.startsWith(base)) {
// For non-root paths under the base
pathname = shouldAppendSlash ? appendForwardSlash(urlPathname) : removeTrailingForwardSlash(urlPathname);
pathname = pathname.slice(base.length);
}
}
// Ensure pathname starts with '/' when needed
if (!pathname.startsWith("/") && shouldAppendSlash && urlPathname.endsWith("/")) {
pathname = prependForwardSlash(pathname);
}
// If config.build.format = 'file' then pathname includes .html, so we need to remove it
if (buildFormat === "file") {
pathname = pathname.replace(/\.html$/, "");
}
// Compute the resolved URL pathname (with base re-prepended)
let resolvedUrlPathname;
if (base !== "/" && (pathname === "" || pathname === "/") && !shouldAppendSlash) {
// Special case for root path at base URL with trailingSlash: 'never'
resolvedUrlPathname = removeTrailingForwardSlash(base);
} else {
resolvedUrlPathname = joinPaths(...[base, pathname].filter(Boolean));
}
return {
pathname,
resolvedUrlPathname
};
}