diff options
-rw-r--r-- | packages/astro/src/core/render/context.ts | 4 | ||||
-rw-r--r-- | packages/astro/src/core/render/core.ts | 100 | ||||
-rw-r--r-- | packages/astro/src/core/render/environment.ts | 12 | ||||
-rw-r--r-- | packages/astro/src/core/render/index.ts | 8 | ||||
-rw-r--r-- | packages/astro/src/core/render/params-and-props.ts | 92 | ||||
-rw-r--r-- | packages/astro/src/core/render/result.ts | 12 | ||||
-rw-r--r-- | packages/astro/src/vite-plugin-astro-server/route.ts | 29 |
7 files changed, 138 insertions, 119 deletions
diff --git a/packages/astro/src/core/render/context.ts b/packages/astro/src/core/render/context.ts index a43650a55..e8ca1017e 100644 --- a/packages/astro/src/core/render/context.ts +++ b/packages/astro/src/core/render/context.ts @@ -8,8 +8,8 @@ import type { SSRResult, } from '../../@types/astro'; import { AstroError, AstroErrorData } from '../errors/index.js'; -import { getParamsAndPropsOrThrow } from './core.js'; import type { Environment } from './environment'; +import { getParamsAndProps } from './params-and-props.js'; const clientLocalsSymbol = Symbol.for('astro.locals'); @@ -47,7 +47,7 @@ export async function createRenderContext( const url = new URL(request.url); const origin = options.origin ?? url.origin; const pathname = options.pathname ?? url.pathname; - const [params, props] = await getParamsAndPropsOrThrow({ + const [params, props] = await getParamsAndProps({ mod: options.mod as any, route: options.route, routeCache: options.env.routeCache, diff --git a/packages/astro/src/core/render/core.ts b/packages/astro/src/core/render/core.ts index 91b668479..201b91292 100644 --- a/packages/astro/src/core/render/core.ts +++ b/packages/astro/src/core/render/core.ts @@ -1,108 +1,10 @@ -import type { AstroCookies, ComponentInstance, Params, Props, RouteData } from '../../@types/astro'; +import type { AstroCookies, ComponentInstance } from '../../@types/astro'; import { renderPage as runtimeRenderPage } from '../../runtime/server/index.js'; import { attachToResponse } from '../cookies/index.js'; -import { AstroError, AstroErrorData } from '../errors/index.js'; -import type { LogOptions } from '../logger/core.js'; import { redirectRouteGenerate, redirectRouteStatus, routeIsRedirect } from '../redirects/index.js'; -import { getParams } from '../routing/params.js'; import type { RenderContext } from './context.js'; import type { Environment } from './environment.js'; import { createResult } from './result.js'; -import { callGetStaticPaths, findPathItemByKey, RouteCache } from './route-cache.js'; - -interface GetParamsAndPropsOptions { - mod: ComponentInstance; - route?: RouteData | undefined; - routeCache: RouteCache; - pathname: string; - logging: LogOptions; - ssr: boolean; -} - -export const enum GetParamsAndPropsError { - NoMatchingStaticPath, -} - -/** - * It retrieves `Params` and `Props`, or throws an error - * if they are not correctly retrieved. - */ -export async function getParamsAndPropsOrThrow( - options: GetParamsAndPropsOptions -): Promise<[Params, Props]> { - let paramsAndPropsResp = await getParamsAndProps(options); - if (paramsAndPropsResp === GetParamsAndPropsError.NoMatchingStaticPath) { - throw new AstroError({ - ...AstroErrorData.NoMatchingStaticPathFound, - message: AstroErrorData.NoMatchingStaticPathFound.message(options.pathname), - hint: options.route?.component - ? AstroErrorData.NoMatchingStaticPathFound.hint([options.route?.component]) - : '', - }); - } - return paramsAndPropsResp; -} - -export async function getParamsAndProps( - opts: GetParamsAndPropsOptions -): Promise<[Params, Props] | GetParamsAndPropsError> { - const { logging, mod, route, routeCache, pathname, ssr } = opts; - // Handle dynamic routes - let params: Params = {}; - let pageProps: Props; - if (route && !route.pathname) { - if (route.params.length) { - // The RegExp pattern expects a decoded string, but the pathname is encoded - // when the URL contains non-English characters. - const paramsMatch = route.pattern.exec(decodeURIComponent(pathname)); - if (paramsMatch) { - params = getParams(route.params)(paramsMatch); - - // 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. - 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, - }, - }); - } - } - } - } - let routeCacheEntry = routeCache.get(route); - // During build, the route cache should already be populated. - // During development, the route cache is filled on-demand and may be empty. - // TODO(fks): Can we refactor getParamsAndProps() to receive routeCacheEntry - // as a prop, and not do a live lookup/populate inside this lower function call. - if (!routeCacheEntry) { - routeCacheEntry = await callGetStaticPaths({ mod, route, isValidate: true, logging, ssr }); - routeCache.set(route, routeCacheEntry); - } - const matchedStaticPath = findPathItemByKey(routeCacheEntry.staticPaths, params, route); - if (!matchedStaticPath && (ssr ? route.prerender : true)) { - return GetParamsAndPropsError.NoMatchingStaticPath; - } - // Note: considered using Object.create(...) for performance - // Since this doesn't inherit an object's properties, this caused some odd user-facing behavior. - // Ex. console.log(Astro.props) -> {}, but console.log(Astro.props.property) -> 'expected value' - // Replaced with a simple spread as a compromise - pageProps = matchedStaticPath?.props ? { ...matchedStaticPath.props } : {}; - } else { - pageProps = {}; - } - return [params, pageProps]; -} export type RenderPage = { mod: ComponentInstance; diff --git a/packages/astro/src/core/render/environment.ts b/packages/astro/src/core/render/environment.ts index 7cd1ae3d8..84c2e43be 100644 --- a/packages/astro/src/core/render/environment.ts +++ b/packages/astro/src/core/render/environment.ts @@ -10,9 +10,15 @@ import { RouteCache } from './route-cache.js'; * Thus they can be created once and passed through to renderPage on each request. */ export interface Environment { + /** + * Used to provide better error messages for `Astro.clientAddress` + */ adapterName?: string; /** logging options */ logging: LogOptions; + /** + * Used to support `Astro.__renderMarkdown` for legacy `<Markdown />` component + */ markdown: MarkdownRenderingOptions; /** "development" or "production" */ mode: RuntimeMode; @@ -20,7 +26,13 @@ export interface Environment { clientDirectives: Map<string, string>; resolve: (s: string) => Promise<string>; routeCache: RouteCache; + /** + * Used for `Astro.site` + */ site?: string; + /** + * Value of Astro config's `output` option, true if "server" or "hybrid" + */ ssr: boolean; streaming: boolean; } diff --git a/packages/astro/src/core/render/index.ts b/packages/astro/src/core/render/index.ts index 372454f26..a48643507 100644 --- a/packages/astro/src/core/render/index.ts +++ b/packages/astro/src/core/render/index.ts @@ -1,11 +1,7 @@ export { createRenderContext } from './context.js'; export type { RenderContext } from './context.js'; -export { - getParamsAndProps, - GetParamsAndPropsError, - getParamsAndPropsOrThrow, - renderPage, -} from './core.js'; +export { renderPage } from './core.js'; export type { Environment } from './environment'; export { createBasicEnvironment, createEnvironment } from './environment.js'; +export { getParamsAndProps } from './params-and-props.js'; export { loadRenderer, loadRenderers } from './renderer.js'; diff --git a/packages/astro/src/core/render/params-and-props.ts b/packages/astro/src/core/render/params-and-props.ts new file mode 100644 index 000000000..33b50eac9 --- /dev/null +++ b/packages/astro/src/core/render/params-and-props.ts @@ -0,0 +1,92 @@ +import type { ComponentInstance, Params, Props, RouteData } from '../../@types/astro'; +import { AstroError, AstroErrorData } from '../errors/index.js'; +import type { LogOptions } from '../logger/core.js'; +import { getParams } from '../routing/params.js'; +import { callGetStaticPaths, findPathItemByKey, RouteCache } from './route-cache.js'; + +interface GetParamsAndPropsOptions { + mod: ComponentInstance; + route?: RouteData | undefined; + routeCache: RouteCache; + pathname: string; + logging: LogOptions; + ssr: boolean; +} + +export async function getParamsAndProps(opts: GetParamsAndPropsOptions): Promise<[Params, Props]> { + const { logging, mod, route, routeCache, pathname, ssr } = 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 [{}, {}]; + } + + // This is a dynamic route, start getting the params + const params = getRouteParams(route, pathname) ?? {}; + + validatePrerenderEndpointCollision(route, mod, params); + + let routeCacheEntry = routeCache.get(route); + // During build, the route cache should already be populated. + // During development, the route cache is filled on-demand and may be empty. + // TODO(fks): Can we refactor getParamsAndProps() to receive routeCacheEntry + // as a prop, and not do a live lookup/populate inside this lower function call. + if (!routeCacheEntry) { + routeCacheEntry = await callGetStaticPaths({ mod, route, isValidate: true, logging, ssr }); + routeCache.set(route, routeCacheEntry); + } + + const matchedStaticPath = findPathItemByKey(routeCacheEntry.staticPaths, params, route); + if (!matchedStaticPath && (ssr ? route.prerender : true)) { + throw new AstroError({ + ...AstroErrorData.NoMatchingStaticPathFound, + message: AstroErrorData.NoMatchingStaticPathFound.message(pathname), + hint: AstroErrorData.NoMatchingStaticPathFound.hint([route.component]), + }); + } + + const props: Props = matchedStaticPath?.props ? { ...matchedStaticPath.props } : {}; + + return [params, props]; +} + +function getRouteParams(route: RouteData, pathname: string): Params | undefined { + if (route.params.length) { + // The RegExp pattern expects a decoded string, but the pathname is encoded + // when the URL contains non-English characters. + const paramsMatch = route.pattern.exec(decodeURIComponent(pathname)); + if (paramsMatch) { + return getParams(route.params)(paramsMatch); + } + } +} + +/** + * 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: RouteData, + mod: ComponentInstance, + params: 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, + }, + }); + } + } +} diff --git a/packages/astro/src/core/render/result.ts b/packages/astro/src/core/render/result.ts index 27ac2ca1c..905593b6a 100644 --- a/packages/astro/src/core/render/result.ts +++ b/packages/astro/src/core/render/result.ts @@ -24,10 +24,19 @@ const clientAddressSymbol = Symbol.for('astro.clientAddress'); const responseSentSymbol = Symbol.for('astro.responseSent'); export interface CreateResultArgs { + /** + * Used to provide better error messages for `Astro.clientAddress` + */ adapterName: string | undefined; + /** + * Value of Astro config's `output` option, true if "server" or "hybrid" + */ ssr: boolean; logging: LogOptions; origin: string; + /** + * Used to support `Astro.__renderMarkdown` for legacy `<Markdown />` component + */ markdown: MarkdownRenderingOptions; mode: RuntimeMode; params: Params; @@ -36,6 +45,9 @@ export interface CreateResultArgs { renderers: SSRLoadedRenderer[]; clientDirectives: Map<string, string>; resolve: (s: string) => Promise<string>; + /** + * Used for `Astro.site` + */ site: string | undefined; links?: Set<SSRElement>; scripts?: Set<SSRElement>; diff --git a/packages/astro/src/vite-plugin-astro-server/route.ts b/packages/astro/src/vite-plugin-astro-server/route.ts index aa3342a6c..ae8abace7 100644 --- a/packages/astro/src/vite-plugin-astro-server/route.ts +++ b/packages/astro/src/vite-plugin-astro-server/route.ts @@ -4,12 +4,12 @@ import type { ComponentInstance, ManifestData, RouteData } from '../@types/astro import { attachToResponse } from '../core/cookies/index.js'; import { call as callEndpoint } from '../core/endpoint/dev/index.js'; import { throwIfRedirectNotAllowed } from '../core/endpoint/index.js'; -import { AstroErrorData } from '../core/errors/index.js'; +import { AstroErrorData, isAstroError } from '../core/errors/index.js'; import { warn } from '../core/logger/core.js'; import { loadMiddleware } from '../core/middleware/loadMiddleware.js'; import type { DevelopmentEnvironment, SSROptions } from '../core/render/dev/index'; import { preload, renderPage } from '../core/render/dev/index.js'; -import { getParamsAndProps, GetParamsAndPropsError } from '../core/render/index.js'; +import { getParamsAndProps } from '../core/render/index.js'; import { createRequest } from '../core/request.js'; import { matchAllRoutes } from '../core/routing/index.js'; import { getSortedPreloadedMatches } from '../prerender/routing.js'; @@ -50,16 +50,15 @@ export async function matchRoute( for await (const { preloadedComponent, route: maybeRoute, filePath } of preloadedMatches) { // attempt to get static paths // if this fails, we have a bad URL match! - const paramsAndPropsRes = await getParamsAndProps({ - mod: preloadedComponent, - route: maybeRoute, - routeCache, - pathname: pathname, - logging, - ssr: isServerLikeOutput(settings.config), - }); - - if (paramsAndPropsRes !== GetParamsAndPropsError.NoMatchingStaticPath) { + try { + await getParamsAndProps({ + mod: preloadedComponent, + route: maybeRoute, + routeCache, + pathname: pathname, + logging, + ssr: isServerLikeOutput(settings.config), + }); return { route: maybeRoute, filePath, @@ -67,6 +66,12 @@ export async function matchRoute( preloadedComponent, mod: preloadedComponent, }; + } catch (e) { + // Ignore error for no matching static paths + if (isAstroError(e) && e.title === AstroErrorData.NoMatchingStaticPathFound.title) { + continue; + } + throw e; } } |