astro
Version:
Astro is a modern site builder with web best practices, performance, and DX front-of-mind.
110 lines (109 loc) • 5.63 kB
JavaScript
import { DEFAULT_404_COMPONENT } from "../constants.js";
import { AstroError, AstroErrorData } from "../errors/index.js";
import { routeHasHtmlExtension, routeIsFallback, routeIsRedirect } from "../routing/helpers.js";
import { callGetStaticPaths, completeStaticPaths, findPathItemByKey } from "./route-cache.js";
export async function getProps(opts) {
const { logger, mod, routeData: route, routeCache, pathname, serverLike, base, trailingSlash } = opts;
// If there's no route, or if there's a pathname (e.g. a static `src/pages/normal.astro` file),
// then we know for sure they don't have params and props, return a fallback value.
if (!route || route.pathname) {
return {};
}
if (routeIsRedirect(route) || routeIsFallback(route) || route.component === DEFAULT_404_COMPONENT) {
return {};
}
// During build, the route cache should already be populated.
// During development, the route cache is filled on-demand and may be empty.
const getStaticPathsOptions = {
mod,
route,
routeCache,
ssr: serverLike,
base,
trailingSlash
};
const staticPaths = await callGetStaticPaths(getStaticPathsOptions);
// The pathname used here comes from the server, which already encoded.
// Since we decided to not mess up with encoding anymore, we need to decode them back so the parameters can match
// the ones expected from the users
const params = getParams(route, pathname);
let matchedStaticPath = findPathItemByKey(staticPaths, params, route, logger, trailingSlash);
if (!matchedStaticPath) {
const allStaticPaths = await completeStaticPaths(getStaticPathsOptions);
if (allStaticPaths) {
matchedStaticPath = findPathItemByKey(allStaticPaths, params, route, logger, trailingSlash);
}
}
if (!matchedStaticPath && route.origin !== "internal" && (serverLike ? route.prerender : true)) {
throw new AstroError({
...AstroErrorData.NoMatchingStaticPathFound,
message: AstroErrorData.NoMatchingStaticPathFound.message(pathname),
hint: AstroErrorData.NoMatchingStaticPathFound.hint([route.component])
});
}
if (mod) {
validatePrerenderEndpointCollision(route, mod, params);
}
const props = matchedStaticPath?.props ? { ...matchedStaticPath.props } : {};
return props;
}
/**
* When given a route with the pattern `/[x]/[y]/[z]/svelte`, and a pathname `/a/b/c/svelte`,
* returns the params object: { x: "a", y: "b", z: "c" }.
*/
export function getParams(route, pathname) {
if (!route.params.length) return {};
// The RegExp pattern expects a decoded string, but the pathname is encoded
// when the URL contains non-English characters.
// A `.html` suffix is only meaningful to strip when it isn't a static part of the route
// definition itself (e.g. `[slug].html.astro`), otherwise dynamic params like `[id]` would
// greedily capture the `.html` that is implied or injected for page routes.
const hasHtmlSuffix = pathname.endsWith(".html") && !routeHasHtmlExtension(route);
// Page routes always strip `.html` up front (e.g. `id = '42'` instead of `id = '42.html'`).
// Non-page routes (endpoints) match the original pathname first — see the fallback below.
const path = hasHtmlSuffix && route.type === "page" ? pathname.slice(0, -".html".length) : pathname;
const allPatterns = [route, ...route.fallbackRoutes].map((r) => r.pattern);
let paramsMatch = allPatterns.map((pattern) => pattern.exec(path)).find((x) => x);
// For non-page routes, if the original pathname didn't match, fall back to stripping
// `.html` / `/index.html` to stay consistent with the dev route matcher, which also strips
// these suffixes when retrying (see `dev.ts`). Without this, requests like
// `/api/items/123/status.html` would match a dynamic endpoint route but fail to extract
// params, causing a "Missing parameter" error. Endpoints that genuinely capture `.html` in
// a param (e.g. `[path]` matching `/file.html`) already matched above, so never reach here.
if (!paramsMatch && hasHtmlSuffix && route.type !== "page") {
const strippedPath = pathname.endsWith("/index.html") ? pathname.slice(0, -"/index.html".length) || "/" : pathname.slice(0, -".html".length);
paramsMatch = allPatterns.map((pattern) => pattern.exec(strippedPath)).find((x) => x);
}
if (!paramsMatch) return {};
const params = {};
route.params.forEach((key, i) => {
if (key.startsWith("...")) {
params[key.slice(3)] = paramsMatch[i + 1] ? paramsMatch[i + 1] : undefined;
} else {
params[key] = paramsMatch[i + 1];
}
});
return params;
}
/**
* If we have an endpoint at `src/pages/api/[slug].ts` that's prerendered, and the `slug`
* is `undefined`, throw an error as we can't generate the `/api` file and `/api` directory
* at the same time. Using something like `[slug].json.ts` instead will work.
*/
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];
// Check last segment is solely `[slug]` or `[...slug]` case (dynamic). Make sure it's not
// `foo[slug].js` by checking segment length === 1. Also check here if that param is undefined.
if (lastSegment.length === 1 && lastSegment[0].dynamic && lastParam === undefined) {
throw new AstroError({
...AstroErrorData.PrerenderDynamicEndpointPathCollide,
message: AstroErrorData.PrerenderDynamicEndpointPathCollide.message(route.route),
hint: AstroErrorData.PrerenderDynamicEndpointPathCollide.hint(route.component),
location: { file: route.component }
});
}
}
}