From f67c356e2b8a200e00f23a8e42a28cc0ca686177 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Mon, 22 Apr 2024 10:49:59 +0100 Subject: [PATCH 01/18] feat: implement reroute in dev (#10818) * chore: implement reroute in dev * chore: revert naming change * chore: conditionally create the new request * chore: handle error * remove only * remove only * chore: add tests and remove logs * chore: fix regression * chore: fix regression route matching * chore: remove unwanted test --- packages/astro/src/@types/astro.ts | 25 +++- packages/astro/src/core/app/pipeline.ts | 15 +- packages/astro/src/core/app/types.ts | 2 + packages/astro/src/core/base-pipeline.ts | 19 +++ packages/astro/src/core/build/generate.ts | 1 + packages/astro/src/core/build/pipeline.ts | 19 ++- .../src/core/build/plugins/plugin-manifest.ts | 1 + packages/astro/src/core/config/schema.ts | 2 + .../src/core/middleware/callMiddleware.ts | 13 +- packages/astro/src/core/middleware/index.ts | 14 +- .../astro/src/core/middleware/sequence.ts | 9 +- packages/astro/src/core/render-context.ts | 138 +++++++++++++++--- packages/astro/src/prerender/routing.ts | 2 +- .../src/vite-plugin-astro-server/pipeline.ts | 86 +++++++++-- .../src/vite-plugin-astro-server/plugin.ts | 4 +- .../src/vite-plugin-astro-server/route.ts | 66 ++++----- .../middleware-virtual/astro.config.mjs | 3 + .../fixtures/middleware-virtual/package.json | 8 + .../middleware-virtual/src/middleware.js | 6 + .../middleware-virtual/src/pages/index.astro | 13 ++ .../test/fixtures/reroute/astro.config.mjs | 8 + .../astro/test/fixtures/reroute/package.json | 8 + .../reroute/src/pages/blog/hello/index.astro | 11 ++ .../reroute/src/pages/blog/salut/index.astro | 11 ++ .../fixtures/reroute/src/pages/index.astro | 10 ++ .../fixtures/reroute/src/pages/reroute.astro | 11 ++ packages/astro/test/reroute.test.js | 42 ++++++ .../test/units/routing/route-matching.test.js | 2 +- .../vite-plugin-astro-server/request.test.js | 2 +- 29 files changed, 465 insertions(+), 86 deletions(-) create mode 100644 packages/astro/test/fixtures/middleware-virtual/astro.config.mjs create mode 100644 packages/astro/test/fixtures/middleware-virtual/package.json create mode 100644 packages/astro/test/fixtures/middleware-virtual/src/middleware.js create mode 100644 packages/astro/test/fixtures/middleware-virtual/src/pages/index.astro create mode 100644 packages/astro/test/fixtures/reroute/astro.config.mjs create mode 100644 packages/astro/test/fixtures/reroute/package.json create mode 100644 packages/astro/test/fixtures/reroute/src/pages/blog/hello/index.astro create mode 100644 packages/astro/test/fixtures/reroute/src/pages/blog/salut/index.astro create mode 100644 packages/astro/test/fixtures/reroute/src/pages/index.astro create mode 100644 packages/astro/test/fixtures/reroute/src/pages/reroute.astro create mode 100644 packages/astro/test/reroute.test.js diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 0cff203cf227..cc9c2054166a 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -250,6 +250,10 @@ export interface AstroGlobal< * [Astro reference](https://docs.astro.build/en/guides/server-side-rendering/) */ redirect: AstroSharedContext['redirect']; + /** + * TODO add documentation + */ + reroute: AstroSharedContext['reroute']; /** * The element allows a component to reference itself recursively. * @@ -1922,6 +1926,18 @@ export interface AstroUserConfig { origin?: boolean; }; }; + + /** + * @docs + * @name experimental.rerouting + * @type {boolean} + * @default `false` + * @version 4.6.0 + * @description + * + * TODO + */ + rerouting: boolean; }; } @@ -2491,6 +2507,11 @@ interface AstroSharedContext< */ redirect(path: string, status?: ValidRedirectStatus): Response; + /** + * TODO: add documentation + */ + reroute(reroutePayload: ReroutePayload): Promise; + /** * Object accessed via Astro middleware */ @@ -2799,7 +2820,9 @@ export interface AstroIntegration { }; } -export type MiddlewareNext = () => Promise; +export type ReroutePayload = string | URL | Request; + +export type MiddlewareNext = (reroutePayload?: ReroutePayload) => Promise; export type MiddlewareHandler = ( context: APIContext, next: MiddlewareNext diff --git a/packages/astro/src/core/app/pipeline.ts b/packages/astro/src/core/app/pipeline.ts index b1c615a1eb36..0f124a18eee7 100644 --- a/packages/astro/src/core/app/pipeline.ts +++ b/packages/astro/src/core/app/pipeline.ts @@ -1,4 +1,10 @@ -import type { RouteData, SSRElement, SSRResult } from '../../@types/astro.js'; +import type { + ComponentInstance, + ReroutePayload, + RouteData, + SSRElement, + SSRResult, +} from '../../@types/astro.js'; import { Pipeline } from '../base-pipeline.js'; import { createModuleScriptElement, createStylesheetElementSet } from '../render/ssr-element.js'; @@ -41,4 +47,11 @@ export class AppPipeline extends Pipeline { } componentMetadata() {} + getComponentByRoute(_routeData: RouteData): Promise { + throw new Error('unimplemented'); + } + + tryReroute(_reroutePayload: ReroutePayload): Promise<[RouteData, ComponentInstance]> { + throw new Error('unimplemented'); + } } diff --git a/packages/astro/src/core/app/types.ts b/packages/astro/src/core/app/types.ts index fd56c6f1068f..6b327fd6fefa 100644 --- a/packages/astro/src/core/app/types.ts +++ b/packages/astro/src/core/app/types.ts @@ -65,6 +65,8 @@ export type SSRManifest = { i18n: SSRManifestI18n | undefined; middleware: MiddlewareHandler; checkOrigin: boolean; + // TODO: remove once the experimental flag is removed + reroutingEnabled: boolean; }; export type SSRManifestI18n = { diff --git a/packages/astro/src/core/base-pipeline.ts b/packages/astro/src/core/base-pipeline.ts index 832823db35fa..4f6c82553995 100644 --- a/packages/astro/src/core/base-pipeline.ts +++ b/packages/astro/src/core/base-pipeline.ts @@ -1,5 +1,7 @@ import type { + ComponentInstance, MiddlewareHandler, + ReroutePayload, RouteData, RuntimeMode, SSRLoadedRenderer, @@ -59,6 +61,23 @@ export abstract class Pipeline { abstract headElements(routeData: RouteData): Promise | HeadElements; abstract componentMetadata(routeData: RouteData): Promise | void; + + /** + * It attempts to retrieve the `RouteData` that matches the input `url`, and the component that belongs to the `RouteData`. + * + * ## Errors + * + * - if not `RouteData` is found + * + * @param {ReroutePayload} reroutePayload + */ + abstract tryReroute(reroutePayload: ReroutePayload): Promise<[RouteData, ComponentInstance]>; + + /** + * Tells the pipeline how to retrieve a component give a `RouteData` + * @param routeData + */ + abstract getComponentByRoute(routeData: RouteData): Promise; } // eslint-disable-next-line @typescript-eslint/no-empty-interface diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index ffe799f6e7e4..117e14eaeddd 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -615,6 +615,7 @@ function createBuildManifest( i18n: i18nManifest, buildFormat: settings.config.build.format, middleware, + reroutingEnabled: settings.config.experimental.rerouting, checkOrigin: settings.config.experimental.security?.csrfProtection?.origin ?? false, }; } diff --git a/packages/astro/src/core/build/pipeline.ts b/packages/astro/src/core/build/pipeline.ts index a78c8eaf893c..fbea1b1eeebb 100644 --- a/packages/astro/src/core/build/pipeline.ts +++ b/packages/astro/src/core/build/pipeline.ts @@ -1,4 +1,11 @@ -import type { RouteData, SSRLoadedRenderer, SSRResult } from '../../@types/astro.js'; +import type { + RouteData, + SSRLoadedRenderer, + SSRResult, + MiddlewareHandler, + ReroutePayload, + ComponentInstance, +} from '../../@types/astro.js'; import { getOutputDirectory } from '../../prerender/utils.js'; import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js'; import type { SSRManifest } from '../app/types.js'; @@ -22,6 +29,8 @@ import { getVirtualModulePageNameFromPath } from './plugins/util.js'; import { ASTRO_PAGE_EXTENSION_POST_PATTERN } from './plugins/util.js'; import type { PageBuildData, StaticBuildOptions } from './types.js'; import { i18nHasFallback } from './util.js'; +import { defineMiddleware } from '../middleware/index.js'; +import { undefined } from 'zod'; /** * The build pipeline is responsible to gather the files emitted by the SSR build and generate the pages by executing these files. @@ -227,4 +236,12 @@ export class BuildPipeline extends Pipeline { return pages; } + + getComponentByRoute(_routeData: RouteData): Promise { + throw new Error('unimplemented'); + } + + tryReroute(_reroutePayload: ReroutePayload): Promise<[RouteData, ComponentInstance]> { + throw new Error('unimplemented'); + } } diff --git a/packages/astro/src/core/build/plugins/plugin-manifest.ts b/packages/astro/src/core/build/plugins/plugin-manifest.ts index 498ccdbb544b..8e5d77ca4ba4 100644 --- a/packages/astro/src/core/build/plugins/plugin-manifest.ts +++ b/packages/astro/src/core/build/plugins/plugin-manifest.ts @@ -277,5 +277,6 @@ function buildManifest( i18n: i18nManifest, buildFormat: settings.config.build.format, checkOrigin: settings.config.experimental.security?.csrfProtection?.origin ?? false, + reroutingEnabled: settings.config.experimental.rerouting, }; } diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts index 303846f7608f..879589b4b87d 100644 --- a/packages/astro/src/core/config/schema.ts +++ b/packages/astro/src/core/config/schema.ts @@ -87,6 +87,7 @@ const ASTRO_CONFIG_DEFAULTS = { globalRoutePriority: false, i18nDomains: false, security: {}, + rerouting: false, }, } satisfies AstroUserConfig & { server: { open: boolean } }; @@ -525,6 +526,7 @@ export const AstroConfigSchema = z.object({ .optional() .default(ASTRO_CONFIG_DEFAULTS.experimental.security), i18nDomains: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.i18nDomains), + rerouting: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.rerouting), }) .strict( `Invalid or outdated experimental feature.\nCheck for incorrect spelling or outdated Astro version.\nSee https://docs.astro.build/en/reference/configuration-reference/#experimental-flags for a list of all current experiments.` diff --git a/packages/astro/src/core/middleware/callMiddleware.ts b/packages/astro/src/core/middleware/callMiddleware.ts index 0133c13d032d..5a0456680a6c 100644 --- a/packages/astro/src/core/middleware/callMiddleware.ts +++ b/packages/astro/src/core/middleware/callMiddleware.ts @@ -1,4 +1,9 @@ -import type { APIContext, MiddlewareHandler, MiddlewareNext } from '../../@types/astro.js'; +import type { + APIContext, + MiddlewareHandler, + MiddlewareNext, + ReroutePayload, +} from '../../@types/astro.js'; import { AstroError, AstroErrorData } from '../errors/index.js'; /** @@ -38,13 +43,13 @@ import { AstroError, AstroErrorData } from '../errors/index.js'; export async function callMiddleware( onRequest: MiddlewareHandler, apiContext: APIContext, - responseFunction: () => Promise | Response + responseFunction: (reroutePayload?: ReroutePayload) => Promise | Response ): Promise { let nextCalled = false; let responseFunctionPromise: Promise | Response | undefined = undefined; - const next: MiddlewareNext = async () => { + const next: MiddlewareNext = async (payload) => { nextCalled = true; - responseFunctionPromise = responseFunction(); + responseFunctionPromise = responseFunction(payload); return responseFunctionPromise; }; diff --git a/packages/astro/src/core/middleware/index.ts b/packages/astro/src/core/middleware/index.ts index cb9304bffbe1..cabbbc9cb9b3 100644 --- a/packages/astro/src/core/middleware/index.ts +++ b/packages/astro/src/core/middleware/index.ts @@ -1,17 +1,14 @@ -import type { APIContext, MiddlewareHandler, Params } from '../../@types/astro.js'; +import type { APIContext, MiddlewareHandler, Params, ReroutePayload } from '../../@types/astro.js'; import { computeCurrentLocale, computePreferredLocale, computePreferredLocaleList, } from '../../i18n/utils.js'; -import { ASTRO_VERSION } from '../constants.js'; +import { ASTRO_VERSION, clientLocalsSymbol, clientAddressSymbol } from '../constants.js'; import { AstroCookies } from '../cookies/index.js'; import { AstroError, AstroErrorData } from '../errors/index.js'; import { sequence } from './sequence.js'; -const clientAddressSymbol = Symbol.for('astro.clientAddress'); -const clientLocalsSymbol = Symbol.for('astro.locals'); - function defineMiddleware(fn: MiddlewareHandler) { return fn; } @@ -49,6 +46,12 @@ function createContext({ const url = new URL(request.url); const route = url.pathname; + // TODO verify that this function works in an edge middleware environment + const reroute = (_reroutePayload: ReroutePayload) => { + // return dummy response + return Promise.resolve(new Response(null)); + }; + return { cookies: new AstroCookies(request), request, @@ -56,6 +59,7 @@ function createContext({ site: undefined, generator: `Astro v${ASTRO_VERSION}`, props: {}, + reroute, redirect(path, status) { return new Response(null, { status: status || 302, diff --git a/packages/astro/src/core/middleware/sequence.ts b/packages/astro/src/core/middleware/sequence.ts index 9a68963945ec..5a0842d8f842 100644 --- a/packages/astro/src/core/middleware/sequence.ts +++ b/packages/astro/src/core/middleware/sequence.ts @@ -1,4 +1,4 @@ -import type { APIContext, MiddlewareHandler } from '../../@types/astro.js'; +import type { APIContext, MiddlewareHandler, ReroutePayload } from '../../@types/astro.js'; import { defineMiddleware } from './index.js'; // From SvelteKit: https://github.com/sveltejs/kit/blob/master/packages/kit/src/exports/hooks/sequence.js @@ -10,10 +10,9 @@ export function sequence(...handlers: MiddlewareHandler[]): MiddlewareHandler { const filtered = handlers.filter((h) => !!h); const length = filtered.length; if (!length) { - const handler: MiddlewareHandler = defineMiddleware((context, next) => { + return defineMiddleware((context, next) => { return next(); }); - return handler; } return defineMiddleware((context, next) => { @@ -24,11 +23,11 @@ export function sequence(...handlers: MiddlewareHandler[]): MiddlewareHandler { // @ts-expect-error // SAFETY: Usually `next` always returns something in user land, but in `sequence` we are actually // doing a loop over all the `next` functions, and eventually we call the last `next` that returns the `Response`. - const result = handle(handleContext, async () => { + const result = handle(handleContext, async (payload: ReroutePayload) => { if (i < length - 1) { return applyHandle(i + 1, handleContext); } else { - return next(); + return next(payload); } }); return result; diff --git a/packages/astro/src/core/render-context.ts b/packages/astro/src/core/render-context.ts index 5cfc8ef2ede3..215b678f7867 100644 --- a/packages/astro/src/core/render-context.ts +++ b/packages/astro/src/core/render-context.ts @@ -4,6 +4,8 @@ import type { AstroGlobalPartial, ComponentInstance, MiddlewareHandler, + MiddlewareNext, + ReroutePayload, RouteData, SSRResult, } from '../@types/astro.js'; @@ -39,14 +41,23 @@ export class RenderContext { public locals: App.Locals, readonly middleware: MiddlewareHandler, readonly pathname: string, - readonly request: Request, - readonly routeData: RouteData, + public request: Request, + public routeData: RouteData, public status: number, - readonly cookies = new AstroCookies(request), - readonly params = getParams(routeData, pathname), - readonly url = new URL(request.url) + protected cookies = new AstroCookies(request), + public params = getParams(routeData, pathname), + protected url = new URL(request.url) ) {} + /** + * A flag that tells the render content if the rerouting was triggered + */ + isRerouting = false; + /** + * A safety net in case of loops + */ + counter = 0; + static create({ locals = {}, middleware, @@ -56,7 +67,7 @@ export class RenderContext { routeData, status = 200, }: Pick & - Partial>) { + Partial>): RenderContext { return new RenderContext( pipeline, locals, @@ -80,11 +91,11 @@ export class RenderContext { * - fallback */ async render(componentInstance: ComponentInstance | undefined): Promise { - const { cookies, middleware, pathname, pipeline, routeData } = this; + const { cookies, middleware, pathname, pipeline } = this; const { logger, routeCache, serverLike, streaming } = pipeline; const props = await getProps({ mod: componentInstance, - routeData, + routeData: this.routeData, routeCache, pathname, logger, @@ -92,8 +103,37 @@ export class RenderContext { }); const apiContext = this.createAPIContext(props); - const lastNext = async () => { - switch (routeData.type) { + 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: 'Loop Detected', + }); + } + const lastNext: MiddlewareNext = async (payload) => { + if (payload) { + if (this.pipeline.manifest.reroutingEnabled) { + try { + const [routeData, component] = await pipeline.tryReroute(payload); + this.routeData = routeData; + componentInstance = component; + } catch (e) { + return new Response('Not found', { + status: 404, + statusText: 'Not found', + }); + } finally { + this.isRerouting = true; + } + } else { + this.pipeline.logger.warn( + 'router', + 'You tried to use the routing feature without enabling it via experimental flag. This is not allowed.' + ); + } + } + switch (this.routeData.type) { case 'endpoint': return renderEndpoint(componentInstance as any, apiContext, serverLike, logger); case 'redirect': @@ -108,7 +148,7 @@ export class RenderContext { props, {}, streaming, - routeData + this.routeData ); } catch (e) { // If there is an error in the page's frontmatter or instantiation of the RenderTemplate fails midway, @@ -119,7 +159,11 @@ export class RenderContext { // Signal to the i18n middleware to maybe act on this response response.headers.set(ROUTE_TYPE_HEADER, 'page'); // Signal to the error-page-rerouting infra to let this response pass through to avoid loops - if (routeData.route === '/404' || routeData.route === '/500') { + if ( + this.routeData.route === '/404' || + this.routeData.route === '/500' || + this.isRerouting + ) { response.headers.set(REROUTE_DIRECTIVE_HEADER, 'no'); } return response; @@ -130,7 +174,9 @@ export class RenderContext { } }; - const response = await callMiddleware(middleware, apiContext, lastNext); + const response = this.isRerouting + ? await lastNext() + : await callMiddleware(middleware, apiContext, lastNext); if (response.headers.get(ROUTE_TYPE_HEADER)) { response.headers.delete(ROUTE_TYPE_HEADER); } @@ -143,10 +189,36 @@ export class RenderContext { createAPIContext(props: APIContext['props']): APIContext { const renderContext = this; - const { cookies, params, pipeline, request, url } = this; + const { cookies, params, pipeline, url } = this; const generator = `Astro v${ASTRO_VERSION}`; const redirect = (path: string, status = 302) => new Response(null, { status, headers: { Location: path } }); + + const reroute = async (reroutePayload: ReroutePayload) => { + try { + const [routeData, component] = await pipeline.tryReroute(reroutePayload); + this.routeData = routeData; + if (reroutePayload instanceof Request) { + this.request = reroutePayload; + } else { + this.request = new Request( + new URL(routeData.pathname ?? routeData.route, this.url.origin), + this.request + ); + } + this.url = new URL(this.request.url); + this.cookies = new AstroCookies(this.request); + this.params = getParams(routeData, url.toString()); + this.isRerouting = true; + return await this.render(component); + } catch (e) { + return new Response('Not found', { + status: 404, + statusText: 'Not found', + }); + } + }; + return { cookies, get clientAddress() { @@ -167,7 +239,7 @@ export class RenderContext { renderContext.locals = val; // we also put it on the original Request object, // where the adapter might be expecting to read it after the response. - Reflect.set(request, clientLocalsSymbol, val); + Reflect.set(this.request, clientLocalsSymbol, val); } }, params, @@ -179,7 +251,8 @@ export class RenderContext { }, props, redirect, - request, + reroute, + request: this.request, site: pipeline.site, url, }; @@ -294,11 +367,11 @@ export class RenderContext { astroStaticPartial: AstroGlobalPartial ): Omit { const renderContext = this; - const { cookies, locals, params, pipeline, request, url } = this; + const { cookies, locals, params, pipeline, url } = this; const { response } = result; const redirect = (path: string, status = 302) => { // If the response is already sent, error as we cannot proceed with the redirect. - if ((request as any)[responseSentSymbol]) { + if ((this.request as any)[responseSentSymbol]) { throw new AstroError({ ...AstroErrorData.ResponseSentError, }); @@ -306,6 +379,32 @@ export class RenderContext { return new Response(null, { status, headers: { Location: path } }); }; + const reroute = async (reroutePayload: ReroutePayload) => { + try { + const [routeData, component] = await pipeline.tryReroute(reroutePayload); + this.routeData = routeData; + if (reroutePayload instanceof Request) { + this.request = reroutePayload; + } else { + this.request = new Request( + new URL(routeData.pathname ?? routeData.route, this.url.origin), + this.request + ); + } + this.url = new URL(this.request.url); + this.cookies = new AstroCookies(this.request); + this.params = getParams(routeData, url.toString()); + this.isRerouting = true; + return await this.render(component); + } catch (e) { + return new Response('Not found', { + status: 404, + statusText: 'Not found', + }); + } + }; + + return { generator: astroStaticPartial.generator, glob: astroStaticPartial.glob, @@ -325,7 +424,8 @@ export class RenderContext { }, locals, redirect, - request, + reroute, + request: this.request, response, site: pipeline.site, url, diff --git a/packages/astro/src/prerender/routing.ts b/packages/astro/src/prerender/routing.ts index e6c09dd70279..cbdddff5c8cb 100644 --- a/packages/astro/src/prerender/routing.ts +++ b/packages/astro/src/prerender/routing.ts @@ -54,7 +54,7 @@ async function preloadAndSetPrerenderStatus({ continue; } - const preloadedComponent = await pipeline.preload(filePath); + const preloadedComponent = await pipeline.preload(route, filePath); // gets the prerender metadata set by the `astro:scanner` vite plugin const prerenderStatus = getPrerenderStatus({ diff --git a/packages/astro/src/vite-plugin-astro-server/pipeline.ts b/packages/astro/src/vite-plugin-astro-server/pipeline.ts index 7ccc63638284..2b967762cc0e 100644 --- a/packages/astro/src/vite-plugin-astro-server/pipeline.ts +++ b/packages/astro/src/vite-plugin-astro-server/pipeline.ts @@ -1,8 +1,10 @@ -import url from 'node:url'; +import { fileURLToPath } from 'node:url'; import type { AstroSettings, ComponentInstance, DevToolbarMetadata, + ManifestData, + ReroutePayload, RouteData, SSRElement, SSRLoadedRenderer, @@ -15,7 +17,7 @@ import { enhanceViteSSRError } from '../core/errors/dev/index.js'; import { AggregateError, CSSError, MarkdownError } from '../core/errors/index.js'; import type { Logger } from '../core/logger/core.js'; import type { ModuleLoader } from '../core/module-loader/index.js'; -import { Pipeline, loadRenderer } from '../core/render/index.js'; +import { loadRenderer, Pipeline } from '../core/render/index.js'; import { isPage, isServerLikeOutput, resolveIdToUrl, viteID } from '../core/util.js'; import { PAGE_SCRIPT_ID } from '../vite-plugin-scripts/index.js'; import { getStylesForURL } from './css.js'; @@ -29,6 +31,13 @@ export class DevPipeline extends Pipeline { // so it needs to be mutable here unlike in other environments override renderers = new Array(); + manifestData: ManifestData | undefined; + + componentInterner: WeakMap = new WeakMap< + RouteData, + ComponentInstance + >(); + private constructor( readonly loader: ModuleLoader, readonly logger: Logger, @@ -43,13 +52,18 @@ export class DevPipeline extends Pipeline { super(logger, manifest, mode, [], resolve, serverLike, streaming); } - static create({ - loader, - logger, - manifest, - settings, - }: Pick) { - return new DevPipeline(loader, logger, manifest, settings); + static create( + manifestData: ManifestData, + { + loader, + logger, + manifest, + settings, + }: Pick + ) { + const pipeline = new DevPipeline(loader, logger, manifest, settings); + pipeline.manifestData = manifestData; + return pipeline; } async headElements(routeData: RouteData): Promise { @@ -80,7 +94,7 @@ export class DevPipeline extends Pipeline { scripts.add({ props: { type: 'module', src }, children: '' }); const additionalMetadata: DevToolbarMetadata['__astro_dev_toolbar__'] = { - root: url.fileURLToPath(settings.config.root), + root: fileURLToPath(settings.config.root), version: ASTRO_VERSION, latestAstroVersion: settings.latestAstroVersion, debugInfo: await getInfoOutput({ userConfig: settings.config, print: false }), @@ -135,7 +149,7 @@ export class DevPipeline extends Pipeline { return getComponentMetadata(filePath, loader); } - async preload(filePath: URL) { + async preload(routeData: RouteData, filePath: URL) { const { loader } = this; if (filePath.href === new URL(DEFAULT_404_COMPONENT, this.config.root).href) { return { default: default404Page } as any as ComponentInstance; @@ -148,7 +162,9 @@ export class DevPipeline extends Pipeline { try { // Load the module from the Vite SSR Runtime. - return (await loader.import(viteID(filePath))) as ComponentInstance; + const componentInstance = (await loader.import(viteID(filePath))) as ComponentInstance; + this.componentInterner.set(routeData, componentInstance); + return componentInstance; } catch (error) { // If the error came from Markdown or CSS, we already handled it and there's no need to enhance it if (MarkdownError.is(error) || CSSError.is(error) || AggregateError.is(error)) { @@ -161,5 +177,51 @@ export class DevPipeline extends Pipeline { clearRouteCache() { this.routeCache.clearAll(); + this.componentInterner = new WeakMap(); + } + + async getComponentByRoute(routeData: RouteData): Promise { + const component = this.componentInterner.get(routeData); + if (component) { + return component; + } else { + const filePath = new URL(`./${routeData.component}`, this.config.root); + return await this.preload(routeData, filePath); + } + } + + async tryReroute(payload: ReroutePayload): Promise<[RouteData, ComponentInstance]> { + let foundRoute; + if (!this.manifestData) { + throw new Error('Missing manifest data'); + } + + for (const route of this.manifestData.routes) { + if (payload instanceof URL) { + if (route.pattern.test(payload.pathname)) { + foundRoute = route; + break; + } + } else if (payload instanceof Request) { + // TODO: handle request, if needed + } else { + if (route.pattern.test(decodeURI(payload))) { + foundRoute = route; + break; + } + } + } + + if (foundRoute) { + const componentInstance = await this.getComponentByRoute(foundRoute); + return [foundRoute, componentInstance]; + } else { + // TODO: handle error properly + throw new Error('Route not found'); + } + } + + setManifestData(manifestData: ManifestData) { + this.manifestData = manifestData; } } diff --git a/packages/astro/src/vite-plugin-astro-server/plugin.ts b/packages/astro/src/vite-plugin-astro-server/plugin.ts index 082de6bcebf0..10b9ff463768 100644 --- a/packages/astro/src/vite-plugin-astro-server/plugin.ts +++ b/packages/astro/src/vite-plugin-astro-server/plugin.ts @@ -35,10 +35,10 @@ export default function createVitePluginAstroServer({ configureServer(viteServer) { const loader = createViteLoader(viteServer); const manifest = createDevelopmentManifest(settings); - const pipeline = DevPipeline.create({ loader, logger, manifest, settings }); let manifestData: ManifestData = ensure404Route( createRouteManifest({ settings, fsMod }, logger) ); + const pipeline = DevPipeline.create(manifestData, { loader, logger, manifest, settings }); const controller = createController({ loader }); const localStorage = new AsyncLocalStorage(); @@ -47,6 +47,7 @@ export default function createVitePluginAstroServer({ pipeline.clearRouteCache(); if (needsManifestRebuild) { manifestData = ensure404Route(createRouteManifest({ settings }, logger)); + pipeline.setManifestData(manifestData); } } // Rebuild route manifest on file change, if needed. @@ -144,6 +145,7 @@ export function createDevelopmentManifest(settings: AstroSettings): SSRManifest inlinedScripts: new Map(), i18n: i18nManifest, checkOrigin: settings.config.experimental.security?.csrfProtection?.origin ?? false, + reroutingEnabled: settings.config.experimental.rerouting, middleware(_, next) { return next(); }, diff --git a/packages/astro/src/vite-plugin-astro-server/route.ts b/packages/astro/src/vite-plugin-astro-server/route.ts index 21053420a754..e62bfe34ed37 100644 --- a/packages/astro/src/vite-plugin-astro-server/route.ts +++ b/packages/astro/src/vite-plugin-astro-server/route.ts @@ -114,7 +114,7 @@ export async function matchRoute( if (custom404) { const filePath = new URL(`./${custom404.component}`, config.root); - const preloadedComponent = await pipeline.preload(filePath); + const preloadedComponent = await pipeline.preload(custom404, filePath); return { route: custom404, @@ -197,40 +197,38 @@ export async function handleRoute({ if (!pathNameHasLocale && pathname !== '/') { return handle404Response(origin, incomingRequest, incomingResponse); } - request = createRequest({ - base: config.base, - url, - headers: incomingRequest.headers, - logger, - // no route found, so we assume the default for rendering the 404 page - staticLike: config.output === 'static' || config.output === 'hybrid', - }); - route = { - component: '', - generate(_data: any): string { - return ''; - }, - params: [], - // Disable eslint as we only want to generate an empty RegExp - // eslint-disable-next-line prefer-regex-literals - pattern: new RegExp(''), - prerender: false, - segments: [], - type: 'fallback', - route: '', - fallbackRoutes: [], - isIndex: false, - }; - renderContext = RenderContext.create({ - pipeline: pipeline, - pathname, - middleware, - request, - routeData: route, - }); - } else { - return handle404Response(origin, incomingRequest, incomingResponse); } + request = createRequest({ + base: config.base, + url, + headers: incomingRequest.headers, + logger, + // no route found, so we assume the default for rendering the 404 page + staticLike: config.output === 'static' || config.output === 'hybrid', + }); + route = { + component: '', + generate(_data: any): string { + return ''; + }, + params: [], + // Disable eslint as we only want to generate an empty RegExp + // eslint-disable-next-line prefer-regex-literals + pattern: new RegExp(''), + prerender: false, + segments: [], + type: 'fallback', + route: '', + fallbackRoutes: [], + isIndex: false, + }; + renderContext = RenderContext.create({ + pipeline: pipeline, + pathname, + middleware, + request, + routeData: route, + }); } else { const filePath: URL | undefined = matchedRoute.filePath; const { preloadedComponent } = matchedRoute; diff --git a/packages/astro/test/fixtures/middleware-virtual/astro.config.mjs b/packages/astro/test/fixtures/middleware-virtual/astro.config.mjs new file mode 100644 index 000000000000..bc095ecddb69 --- /dev/null +++ b/packages/astro/test/fixtures/middleware-virtual/astro.config.mjs @@ -0,0 +1,3 @@ +import { defineConfig } from "astro/config"; + +export default defineConfig({}) diff --git a/packages/astro/test/fixtures/middleware-virtual/package.json b/packages/astro/test/fixtures/middleware-virtual/package.json new file mode 100644 index 000000000000..7cfbeb721047 --- /dev/null +++ b/packages/astro/test/fixtures/middleware-virtual/package.json @@ -0,0 +1,8 @@ +{ + "name": "@test/middleware-virtual", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "workspace:*" + } +} diff --git a/packages/astro/test/fixtures/middleware-virtual/src/middleware.js b/packages/astro/test/fixtures/middleware-virtual/src/middleware.js new file mode 100644 index 000000000000..55004a00cfdb --- /dev/null +++ b/packages/astro/test/fixtures/middleware-virtual/src/middleware.js @@ -0,0 +1,6 @@ +import { defineMiddleware } from 'astro:middleware'; + +export const onRequest = defineMiddleware(async (context, next) => { + console.log('[MIDDLEWARE] in ' + context.url.toString()); + return next(); +}); diff --git a/packages/astro/test/fixtures/middleware-virtual/src/pages/index.astro b/packages/astro/test/fixtures/middleware-virtual/src/pages/index.astro new file mode 100644 index 000000000000..9bd31f5fde27 --- /dev/null +++ b/packages/astro/test/fixtures/middleware-virtual/src/pages/index.astro @@ -0,0 +1,13 @@ +--- +const data = Astro.locals; +--- + + + + Index + + + +Index + + diff --git a/packages/astro/test/fixtures/reroute/astro.config.mjs b/packages/astro/test/fixtures/reroute/astro.config.mjs new file mode 100644 index 000000000000..af736916179e --- /dev/null +++ b/packages/astro/test/fixtures/reroute/astro.config.mjs @@ -0,0 +1,8 @@ +import { defineConfig } from 'astro/config'; + +// https://astro.build/config +export default defineConfig({ + experimental: { + rerouting: true + } +}); diff --git a/packages/astro/test/fixtures/reroute/package.json b/packages/astro/test/fixtures/reroute/package.json new file mode 100644 index 000000000000..ed64e57a97e0 --- /dev/null +++ b/packages/astro/test/fixtures/reroute/package.json @@ -0,0 +1,8 @@ +{ + "name": "@test/reroute", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "workspace:*" + } +} diff --git a/packages/astro/test/fixtures/reroute/src/pages/blog/hello/index.astro b/packages/astro/test/fixtures/reroute/src/pages/blog/hello/index.astro new file mode 100644 index 000000000000..07a8544aecb1 --- /dev/null +++ b/packages/astro/test/fixtures/reroute/src/pages/blog/hello/index.astro @@ -0,0 +1,11 @@ +--- +return Astro.reroute(new URL("../../", Astro.url)) +--- + + + Blog hello + + +

Blog hello

+ + diff --git a/packages/astro/test/fixtures/reroute/src/pages/blog/salut/index.astro b/packages/astro/test/fixtures/reroute/src/pages/blog/salut/index.astro new file mode 100644 index 000000000000..373653afd7e0 --- /dev/null +++ b/packages/astro/test/fixtures/reroute/src/pages/blog/salut/index.astro @@ -0,0 +1,11 @@ +--- +return Astro.reroute(new Request(new URL("../../", Astro.url))) +--- + + + Blog hello + + +

Blog hello

+ + diff --git a/packages/astro/test/fixtures/reroute/src/pages/index.astro b/packages/astro/test/fixtures/reroute/src/pages/index.astro new file mode 100644 index 000000000000..727a45a65758 --- /dev/null +++ b/packages/astro/test/fixtures/reroute/src/pages/index.astro @@ -0,0 +1,10 @@ +--- +--- + + + Index + + +

Index

+ + diff --git a/packages/astro/test/fixtures/reroute/src/pages/reroute.astro b/packages/astro/test/fixtures/reroute/src/pages/reroute.astro new file mode 100644 index 000000000000..8396946f144b --- /dev/null +++ b/packages/astro/test/fixtures/reroute/src/pages/reroute.astro @@ -0,0 +1,11 @@ +--- +return Astro.reroute("/") +--- + + + Reroute + + +

Reroute

+ + diff --git a/packages/astro/test/reroute.test.js b/packages/astro/test/reroute.test.js new file mode 100644 index 000000000000..8740d318fdc8 --- /dev/null +++ b/packages/astro/test/reroute.test.js @@ -0,0 +1,42 @@ +import { describe, it, before, after } from 'node:test'; +import { loadFixture } from './test-utils.js'; +import { load as cheerioLoad } from 'cheerio'; +import assert from 'node:assert/strict'; + +describe('Dev reroute', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + let devServer; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/reroute/', + }); + devServer = await fixture.startDevServer(); + }); + + after(async () => { + await devServer.stop(); + }); + + it('the render the index page when navigating /reroute ', async () => { + const html = await fixture.fetch('/reroute').then((res) => res.text()); + const $ = cheerioLoad(html); + + assert.equal($('h1').text(), 'Index'); + }); + + it('the render the index page when navigating /blog/hello ', async () => { + const html = await fixture.fetch('/blog/hello').then((res) => res.text()); + const $ = cheerioLoad(html); + + assert.equal($('h1').text(), 'Index'); + }); + + it('the render the index page when navigating /blog/salut ', async () => { + const html = await fixture.fetch('/blog/hello').then((res) => res.text()); + const $ = cheerioLoad(html); + + assert.equal($('h1').text(), 'Index'); + }); +}); diff --git a/packages/astro/test/units/routing/route-matching.test.js b/packages/astro/test/units/routing/route-matching.test.js index b2f27d8c9f80..5eafa6c80aea 100644 --- a/packages/astro/test/units/routing/route-matching.test.js +++ b/packages/astro/test/units/routing/route-matching.test.js @@ -146,7 +146,7 @@ describe('Route matching', () => { const loader = createViteLoader(container.viteServer); const manifest = createDevelopmentManifest(container.settings); - pipeline = DevPipeline.create({ loader, logger: defaultLogger, manifest, settings }); + pipeline = DevPipeline.create(undefined, { loader, logger: defaultLogger, manifest, settings }); manifestData = createRouteManifest( { cwd: fileURLToPath(root), diff --git a/packages/astro/test/units/vite-plugin-astro-server/request.test.js b/packages/astro/test/units/vite-plugin-astro-server/request.test.js index 7ea587f97e2f..f976a9d30b50 100644 --- a/packages/astro/test/units/vite-plugin-astro-server/request.test.js +++ b/packages/astro/test/units/vite-plugin-astro-server/request.test.js @@ -22,7 +22,7 @@ async function createDevPipeline(overrides = {}) { const loader = overrides.loader ?? createLoader(); const manifest = createDevelopmentManifest(settings); - return DevPipeline.create({ loader, logger: defaultLogger, manifest, settings }); + return DevPipeline.create(undefined, { loader, logger: defaultLogger, manifest, settings }); } describe('vite-plugin-astro-server', () => { From 319bd051dddcf2618652fc143ae66f6d6ea0e9d1 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Mon, 22 Apr 2024 14:54:12 +0100 Subject: [PATCH 02/18] feat: reroute in SSG (#10843) * feat: rerouting in ssg * linting --- packages/astro/src/core/build/generate.ts | 77 ++-------- packages/astro/src/core/build/pipeline.ts | 144 ++++++++++++++++-- packages/astro/src/core/render-context.ts | 4 + .../src/vite-plugin-astro-server/pipeline.ts | 6 +- .../src/vite-plugin-astro-server/route.ts | 1 + .../reroute/src/pages/dynamic/[id].astro | 21 +++ .../reroute/src/pages/spread/[...id].astro | 20 +++ .../astro/test/i18n-routing-manual.test.js | 2 - packages/astro/test/reroute.test.js | 64 +++++++- 9 files changed, 255 insertions(+), 84 deletions(-) create mode 100644 packages/astro/test/fixtures/reroute/src/pages/dynamic/[id].astro create mode 100644 packages/astro/test/fixtures/reroute/src/pages/spread/[...id].astro diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index 117e14eaeddd..5d5401e5c9c0 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -35,24 +35,14 @@ import { getOutputDirectory } from '../../prerender/utils.js'; import type { SSRManifestI18n } from '../app/types.js'; import { NoPrerenderedRoutesWithDomains } from '../errors/errors-data.js'; import { AstroError, AstroErrorData } from '../errors/index.js'; -import { routeIsFallback } from '../redirects/helpers.js'; -import { - RedirectSinglePageBuiltModule, - getRedirectLocationOrThrow, - routeIsRedirect, -} from '../redirects/index.js'; +import { getRedirectLocationOrThrow, routeIsRedirect } from '../redirects/index.js'; import { RenderContext } from '../render-context.js'; import { callGetStaticPaths } from '../render/route-cache.js'; import { createRequest } from '../request.js'; import { matchRoute } from '../routing/match.js'; import { getOutputFilename, isServerLikeOutput } from '../util.js'; import { getOutDirWithinCwd, getOutFile, getOutFolder } from './common.js'; -import { - cssOrder, - getEntryFilePathFromComponentPath, - getPageDataByComponent, - mergeInlineCss, -} from './internal.js'; +import { cssOrder, getPageDataByComponent, mergeInlineCss } from './internal.js'; import { BuildPipeline } from './pipeline.js'; import type { PageBuildData, @@ -66,46 +56,6 @@ function createEntryURL(filePath: string, outFolder: URL) { return new URL('./' + filePath + `?time=${Date.now()}`, outFolder); } -async function getEntryForRedirectRoute( - route: RouteData, - internals: BuildInternals, - outFolder: URL -): Promise { - if (route.type !== 'redirect') { - throw new Error(`Expected a redirect route.`); - } - if (route.redirectRoute) { - const filePath = getEntryFilePathFromComponentPath(internals, route.redirectRoute.component); - if (filePath) { - const url = createEntryURL(filePath, outFolder); - const ssrEntryPage: SinglePageBuiltModule = await import(url.toString()); - return ssrEntryPage; - } - } - - return RedirectSinglePageBuiltModule; -} - -async function getEntryForFallbackRoute( - route: RouteData, - internals: BuildInternals, - outFolder: URL -): Promise { - if (route.type !== 'fallback') { - throw new Error(`Expected a redirect route.`); - } - if (route.redirectRoute) { - const filePath = getEntryFilePathFromComponentPath(internals, route.redirectRoute.component); - if (filePath) { - const url = createEntryURL(filePath, outFolder); - const ssrEntryPage: SinglePageBuiltModule = await import(url.toString()); - return ssrEntryPage; - } - } - - return RedirectSinglePageBuiltModule; -} - // Gives back a facadeId that is relative to the root. // ie, src/pages/index.astro instead of /Users/name..../src/pages/index.astro export function rootRelativeFacadeId(facadeId: string, settings: AstroSettings): string { @@ -185,14 +135,15 @@ export async function generatePages(options: StaticBuildOptions, internals: Buil }); } - const ssrEntryURLPage = createEntryURL(filePath, outFolder); - const ssrEntryPage = await import(ssrEntryURLPage.toString()); + const ssrEntryPage = await pipeline.retrieveSsrEntry(pageData.route, filePath); if (options.settings.adapter?.adapterFeatures?.functionPerRoute) { // forcing to use undefined, so we fail in an expected way if the module is not even there. + // @ts-expect-error When building for `functionPerRoute`, the module exports a `pageModule` function instead const ssrEntry = ssrEntryPage?.pageModule; if (ssrEntry) { await generatePage(pageData, ssrEntry, builtPaths, pipeline); } else { + const ssrEntryURLPage = createEntryURL(filePath, outFolder); throw new Error( `Unable to find the manifest for the module ${ssrEntryURLPage.toString()}. This is unexpected and likely a bug in Astro, please report.` ); @@ -205,18 +156,8 @@ export async function generatePages(options: StaticBuildOptions, internals: Buil } } else { for (const [pageData, filePath] of pagesToGenerate) { - if (routeIsRedirect(pageData.route)) { - const entry = await getEntryForRedirectRoute(pageData.route, internals, outFolder); - await generatePage(pageData, entry, builtPaths, pipeline); - } else if (routeIsFallback(pageData.route)) { - const entry = await getEntryForFallbackRoute(pageData.route, internals, outFolder); - await generatePage(pageData, entry, builtPaths, pipeline); - } else { - const ssrEntryURLPage = createEntryURL(filePath, outFolder); - const entry: SinglePageBuiltModule = await import(ssrEntryURLPage.toString()); - - await generatePage(pageData, entry, builtPaths, pipeline); - } + const entry = await pipeline.retrieveSsrEntry(pageData.route, filePath); + await generatePage(pageData, entry, builtPaths, pipeline); } } logger.info( @@ -232,12 +173,12 @@ export async function generatePages(options: StaticBuildOptions, internals: Buil .map((x) => x.transforms.size) .reduce((a, b) => a + b, 0); const cpuCount = os.cpus().length; - const assetsCreationpipeline = await prepareAssetsGenerationEnv(pipeline, totalCount); + const assetsCreationPipeline = await prepareAssetsGenerationEnv(pipeline, totalCount); const queue = new PQueue({ concurrency: Math.max(cpuCount, 1) }); const assetsTimer = performance.now(); for (const [originalPath, transforms] of staticImageList) { - await generateImagesForPath(originalPath, transforms, assetsCreationpipeline, queue); + await generateImagesForPath(originalPath, transforms, assetsCreationPipeline, queue); } await queue.onIdle(); diff --git a/packages/astro/src/core/build/pipeline.ts b/packages/astro/src/core/build/pipeline.ts index fbea1b1eeebb..5acc14682ea5 100644 --- a/packages/astro/src/core/build/pipeline.ts +++ b/packages/astro/src/core/build/pipeline.ts @@ -1,10 +1,9 @@ import type { + ComponentInstance, + ReroutePayload, RouteData, SSRLoadedRenderer, SSRResult, - MiddlewareHandler, - ReroutePayload, - ComponentInstance, } from '../../@types/astro.js'; import { getOutputDirectory } from '../../prerender/utils.js'; import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js'; @@ -20,22 +19,42 @@ import { isServerLikeOutput } from '../util.js'; import { type BuildInternals, cssOrder, + getEntryFilePathFromComponentPath, getPageDataByComponent, mergeInlineCss, } from './internal.js'; import { ASTRO_PAGE_MODULE_ID, ASTRO_PAGE_RESOLVED_MODULE_ID } from './plugins/plugin-pages.js'; import { RESOLVED_SPLIT_MODULE_ID } from './plugins/plugin-ssr.js'; -import { getVirtualModulePageNameFromPath } from './plugins/util.js'; -import { ASTRO_PAGE_EXTENSION_POST_PATTERN } from './plugins/util.js'; -import type { PageBuildData, StaticBuildOptions } from './types.js'; +import { + ASTRO_PAGE_EXTENSION_POST_PATTERN, + getVirtualModulePageNameFromPath, +} from './plugins/util.js'; +import type { PageBuildData, SinglePageBuiltModule, StaticBuildOptions } from './types.js'; import { i18nHasFallback } from './util.js'; -import { defineMiddleware } from '../middleware/index.js'; -import { undefined } from 'zod'; +import { RedirectSinglePageBuiltModule } from '../redirects/index.js'; +import { getOutDirWithinCwd } from './common.js'; /** * The build pipeline is responsible to gather the files emitted by the SSR build and generate the pages by executing these files. */ export class BuildPipeline extends Pipeline { + #componentsInterner: WeakMap = new WeakMap< + RouteData, + SinglePageBuiltModule + >(); + /** + * This cache is needed to map a single `RouteData` to its file path. + * @private + */ + #routesByFilePath: WeakMap = new WeakMap(); + + get outFolder() { + const ssr = isServerLikeOutput(this.settings.config); + return ssr + ? this.settings.config.build.server + : getOutDirWithinCwd(this.settings.config.outDir); + } + private constructor( readonly internals: BuildInternals, readonly manifest: SSRManifest, @@ -234,14 +253,115 @@ export class BuildPipeline extends Pipeline { } } + for (const [buildData, filePath] of pages.entries()) { + this.#routesByFilePath.set(buildData.route, filePath); + } + return pages; } - getComponentByRoute(_routeData: RouteData): Promise { - throw new Error('unimplemented'); + async getComponentByRoute(routeData: RouteData): Promise { + if (this.#componentsInterner.has(routeData)) { + // SAFETY: checked before + const entry = this.#componentsInterner.get(routeData)!; + return await entry.page(); + } else { + // SAFETY: the pipeline calls `retrieveRoutesToGenerate`, which is in charge to fill the cache. + const filePath = this.#routesByFilePath.get(routeData)!; + const module = await this.retrieveSsrEntry(routeData, filePath); + return module.page(); + } } - tryReroute(_reroutePayload: ReroutePayload): Promise<[RouteData, ComponentInstance]> { - throw new Error('unimplemented'); + async tryReroute(payload: ReroutePayload): Promise<[RouteData, ComponentInstance]> { + let foundRoute: RouteData | undefined; + // options.manifest is the actual type that contains the information + for (const route of this.options.manifest.routes) { + if (payload instanceof URL) { + if (route.pattern.test(payload.pathname)) { + foundRoute = route; + break; + } + } else if (payload instanceof Request) { + const url = new URL(payload.url); + if (route.pattern.test(url.pathname)) { + foundRoute = route; + break; + } + } else { + if (route.pattern.test(decodeURI(payload))) { + foundRoute = route; + break; + } + } + } + if (foundRoute) { + const componentInstance = await this.getComponentByRoute(foundRoute); + return [foundRoute, componentInstance]; + } else { + throw new Error('Route not found'); + } } + + async retrieveSsrEntry(route: RouteData, filePath: string): Promise { + if (this.#componentsInterner.has(route)) { + // SAFETY: it is checked inside the if + return this.#componentsInterner.get(route)!; + } + let entry; + if (routeIsRedirect(route)) { + entry = await this.#getEntryForRedirectRoute(route, this.internals, this.outFolder); + } else if (routeIsFallback(route)) { + entry = await this.#getEntryForFallbackRoute(route, this.internals, this.outFolder); + } else { + const ssrEntryURLPage = createEntryURL(filePath, this.outFolder); + entry = await import(ssrEntryURLPage.toString()); + } + this.#componentsInterner.set(route, entry); + return entry; + } + + async #getEntryForFallbackRoute( + route: RouteData, + internals: BuildInternals, + outFolder: URL + ): Promise { + if (route.type !== 'fallback') { + throw new Error(`Expected a redirect route.`); + } + if (route.redirectRoute) { + const filePath = getEntryFilePathFromComponentPath(internals, route.redirectRoute.component); + if (filePath) { + const url = createEntryURL(filePath, outFolder); + const ssrEntryPage: SinglePageBuiltModule = await import(url.toString()); + return ssrEntryPage; + } + } + + return RedirectSinglePageBuiltModule; + } + + async #getEntryForRedirectRoute( + route: RouteData, + internals: BuildInternals, + outFolder: URL + ): Promise { + if (route.type !== 'redirect') { + throw new Error(`Expected a redirect route.`); + } + if (route.redirectRoute) { + const filePath = getEntryFilePathFromComponentPath(internals, route.redirectRoute.component); + if (filePath) { + const url = createEntryURL(filePath, outFolder); + const ssrEntryPage: SinglePageBuiltModule = await import(url.toString()); + return ssrEntryPage; + } + } + + return RedirectSinglePageBuiltModule; + } +} + +function createEntryURL(filePath: string, outFolder: URL) { + return new URL('./' + filePath + `?time=${Date.now()}`, outFolder); } diff --git a/packages/astro/src/core/render-context.ts b/packages/astro/src/core/render-context.ts index 215b678f7867..0eb7df01dc44 100644 --- a/packages/astro/src/core/render-context.ts +++ b/packages/astro/src/core/render-context.ts @@ -195,6 +195,7 @@ export class RenderContext { new Response(null, { status, headers: { Location: path } }); const reroute = async (reroutePayload: ReroutePayload) => { + pipeline.logger.debug('router', 'Called rerouting to:', reroutePayload); try { const [routeData, component] = await pipeline.tryReroute(reroutePayload); this.routeData = routeData; @@ -212,6 +213,7 @@ export class RenderContext { this.isRerouting = true; return await this.render(component); } catch (e) { + pipeline.logger.debug('router', 'Routing failed.', e); return new Response('Not found', { status: 404, statusText: 'Not found', @@ -381,6 +383,7 @@ export class RenderContext { const reroute = async (reroutePayload: ReroutePayload) => { try { + pipeline.logger.debug('router', 'Calling rerouting: ', reroutePayload); const [routeData, component] = await pipeline.tryReroute(reroutePayload); this.routeData = routeData; if (reroutePayload instanceof Request) { @@ -397,6 +400,7 @@ export class RenderContext { this.isRerouting = true; return await this.render(component); } catch (e) { + pipeline.logger.debug('router', 'Rerouting failed, returning a 404.', e); return new Response('Not found', { status: 404, statusText: 'Not found', diff --git a/packages/astro/src/vite-plugin-astro-server/pipeline.ts b/packages/astro/src/vite-plugin-astro-server/pipeline.ts index 2b967762cc0e..6176786fb289 100644 --- a/packages/astro/src/vite-plugin-astro-server/pipeline.ts +++ b/packages/astro/src/vite-plugin-astro-server/pipeline.ts @@ -203,7 +203,11 @@ export class DevPipeline extends Pipeline { break; } } else if (payload instanceof Request) { - // TODO: handle request, if needed + const url = new URL(payload.url); + if (route.pattern.test(url.pathname)) { + foundRoute = route; + break; + } } else { if (route.pattern.test(decodeURI(payload))) { foundRoute = route; diff --git a/packages/astro/src/vite-plugin-astro-server/route.ts b/packages/astro/src/vite-plugin-astro-server/route.ts index e62bfe34ed37..85bf969f9db9 100644 --- a/packages/astro/src/vite-plugin-astro-server/route.ts +++ b/packages/astro/src/vite-plugin-astro-server/route.ts @@ -222,6 +222,7 @@ export async function handleRoute({ fallbackRoutes: [], isIndex: false, }; + renderContext = RenderContext.create({ pipeline: pipeline, pathname, diff --git a/packages/astro/test/fixtures/reroute/src/pages/dynamic/[id].astro b/packages/astro/test/fixtures/reroute/src/pages/dynamic/[id].astro new file mode 100644 index 000000000000..fc1b64fbba1a --- /dev/null +++ b/packages/astro/test/fixtures/reroute/src/pages/dynamic/[id].astro @@ -0,0 +1,21 @@ +--- + +export function getStaticPaths() { + return [ + { params: { id: 'hello' } }, + ]; +} + + +return Astro.reroute("/") + +--- + + + + Dynamic [id].astro + + +

/dynamic/[id].astro

+ + diff --git a/packages/astro/test/fixtures/reroute/src/pages/spread/[...id].astro b/packages/astro/test/fixtures/reroute/src/pages/spread/[...id].astro new file mode 100644 index 000000000000..1169380cab62 --- /dev/null +++ b/packages/astro/test/fixtures/reroute/src/pages/spread/[...id].astro @@ -0,0 +1,20 @@ +--- +export function getStaticPaths() { + return [ + { params: { id: 'hello' } }, + ]; +} + +return Astro.reroute("/") + +--- + + + + + Spread [...id].astro + + +

/spread/[...id].astro

+ + diff --git a/packages/astro/test/i18n-routing-manual.test.js b/packages/astro/test/i18n-routing-manual.test.js index d664b3797889..1feaf963348c 100644 --- a/packages/astro/test/i18n-routing-manual.test.js +++ b/packages/astro/test/i18n-routing-manual.test.js @@ -58,8 +58,6 @@ describe('Dev server manual routing', () => { describe('SSG manual routing', () => { /** @type {import('./test-utils').Fixture} */ let fixture; - /** @type {import('./test-utils').DevServer} */ - let devServer; before(async () => { fixture = await loadFixture({ diff --git a/packages/astro/test/reroute.test.js b/packages/astro/test/reroute.test.js index 8740d318fdc8..e90f4e6723ce 100644 --- a/packages/astro/test/reroute.test.js +++ b/packages/astro/test/reroute.test.js @@ -34,7 +34,69 @@ describe('Dev reroute', () => { }); it('the render the index page when navigating /blog/salut ', async () => { - const html = await fixture.fetch('/blog/hello').then((res) => res.text()); + const html = await fixture.fetch('/blog/salut').then((res) => res.text()); + const $ = cheerioLoad(html); + + assert.equal($('h1').text(), 'Index'); + }); + + it('the render the index page when navigating dynamic route /dynamic/[id] ', async () => { + const html = await fixture.fetch('/dynamic/hello').then((res) => res.text()); + const $ = cheerioLoad(html); + + assert.equal($('h1').text(), 'Index'); + }); + + it('the render the index page when navigating spread route /spread/[...spread] ', async () => { + const html = await fixture.fetch('/spread/hello').then((res) => res.text()); + const $ = cheerioLoad(html); + + assert.equal($('h1').text(), 'Index'); + }); +}); + +describe('Build reroute', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/reroute/', + }); + await fixture.build(); + }); + + it('the render the index page when navigating /reroute ', async () => { + const html = await fixture.readFile('/reroute/index.html'); + const $ = cheerioLoad(html); + + assert.equal($('h1').text(), 'Index'); + }); + + it('the render the index page when navigating /blog/hello ', async () => { + const html = await fixture.readFile('/blog/hello/index.html'); + const $ = cheerioLoad(html); + + assert.equal($('h1').text(), 'Index'); + }); + + it('the render the index page when navigating /blog/salut ', async () => { + const html = await fixture.readFile('/blog/salut/index.html'); + + const $ = cheerioLoad(html); + + assert.equal($('h1').text(), 'Index'); + }); + + it('the render the index page when navigating dynamic route /dynamic/[id] ', async () => { + const html = await fixture.readFile('/dynamic/hello/index.html'); + const $ = cheerioLoad(html); + + assert.equal($('h1').text(), 'Index'); + }); + + it('the render the index page when navigating spread route /spread/[...spread] ', async () => { + const html = await fixture.readFile('/spread/hello/index.html'); const $ = cheerioLoad(html); assert.equal($('h1').text(), 'Index'); From 2ee5817c58286b8af0a127be7fd7784b67159b15 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Mon, 22 Apr 2024 16:25:03 +0100 Subject: [PATCH 03/18] feat: reroute for SSR (#10845) * feat: implement reroute in dev (#10818) * chore: implement reroute in dev * chore: revert naming change * chore: conditionally create the new request * chore: handle error * remove only * remove only * chore: add tests and remove logs * chore: fix regression * chore: fix regression route matching * chore: remove unwanted test * feat: reroute in SSG (#10843) * feat: rerouting in ssg * linting * feat: rerouting in ssg * linting * feat: reroute for SSR * fix rebase * fix merge issue --- packages/astro/src/core/app/index.ts | 52 ++-------- packages/astro/src/core/app/pipeline.ts | 115 ++++++++++++++++++---- packages/astro/src/core/render-context.ts | 1 - packages/astro/test/reroute.test.js | 63 ++++++++++++ 4 files changed, 166 insertions(+), 65 deletions(-) diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index 116151610e1c..1ba5d9479833 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -1,13 +1,6 @@ -import type { - ComponentInstance, - ManifestData, - RouteData, - SSRManifest, -} from '../../@types/astro.js'; +import type { ManifestData, RouteData, SSRManifest } from '../../@types/astro.js'; import { normalizeTheLocale } from '../../i18n/index.js'; -import type { SinglePageBuiltModule } from '../build/types.js'; import { - DEFAULT_404_COMPONENT, REROUTABLE_STATUS_CODES, REROUTE_DIRECTIVE_HEADER, clientAddressSymbol, @@ -26,7 +19,6 @@ import { prependForwardSlash, removeTrailingForwardSlash, } from '../path.js'; -import { RedirectSinglePageBuiltModule } from '../redirects/index.js'; import { RenderContext } from '../render-context.js'; import { createAssetLink } from '../render/ssr-element.js'; import { ensure404Route } from '../routing/astro-designed-error-pages.js'; @@ -96,7 +88,7 @@ export class App { routes: manifest.routes.map((route) => route.routeData), }); this.#baseWithoutTrailingSlash = removeTrailingForwardSlash(this.#manifest.base); - this.#pipeline = this.#createPipeline(streaming); + this.#pipeline = this.#createPipeline(this.#manifestData, streaming); this.#adapterLogger = new AstroIntegrationLogger( this.#logger.options, this.#manifest.adapterName @@ -110,10 +102,11 @@ export class App { /** * Creates a pipeline by reading the stored manifest * + * @param manifestData * @param streaming * @private */ - #createPipeline(streaming = false) { + #createPipeline(manifestData: ManifestData, streaming = false) { if (this.#manifest.checkOrigin) { this.#manifest.middleware = sequence( createOriginCheckMiddleware(), @@ -121,7 +114,7 @@ export class App { ); } - return AppPipeline.create({ + return AppPipeline.create(manifestData, { logger: this.#logger, manifest: this.#manifest, mode: 'production', @@ -309,7 +302,7 @@ export class App { } const pathname = this.#getPathnameFromRequest(request); const defaultStatus = this.#getDefaultStatusCode(routeData, pathname); - const mod = await this.#getModuleForRoute(routeData); + const mod = await this.#pipeline.getModuleForRoute(routeData); let response; try { @@ -405,7 +398,7 @@ export class App { return this.#mergeResponses(response, originalResponse, override); } - const mod = await this.#getModuleForRoute(errorRouteData); + const mod = await this.#pipeline.getModuleForRoute(errorRouteData); try { const renderContext = RenderContext.create({ locals, @@ -493,35 +486,4 @@ export class App { if (route.endsWith('/500')) return 500; return 200; } - - async #getModuleForRoute(route: RouteData): Promise { - if (route.component === DEFAULT_404_COMPONENT) { - return { - page: async () => - ({ default: () => new Response(null, { status: 404 }) }) as ComponentInstance, - 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}` - ); - } - const pageModule = await importComponentInstance(); - return pageModule; - } else if (this.#manifest.pageModule) { - const importComponentInstance = this.#manifest.pageModule; - return importComponentInstance; - } else { - 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." - ); - } - } - } } diff --git a/packages/astro/src/core/app/pipeline.ts b/packages/astro/src/core/app/pipeline.ts index 0f124a18eee7..8b00a1c4fe7d 100644 --- a/packages/astro/src/core/app/pipeline.ts +++ b/packages/astro/src/core/app/pipeline.ts @@ -1,27 +1,46 @@ import type { - ComponentInstance, - ReroutePayload, + ManifestData, RouteData, SSRElement, SSRResult, + ComponentInstance, + ReroutePayload, } from '../../@types/astro.js'; import { Pipeline } from '../base-pipeline.js'; +import { DEFAULT_404_COMPONENT } from '../constants.js'; +import { RedirectSinglePageBuiltModule } from '../redirects/component.js'; import { createModuleScriptElement, createStylesheetElementSet } from '../render/ssr-element.js'; +import type { SinglePageBuiltModule } from '../build/types.js'; export class AppPipeline extends Pipeline { - static create({ - logger, - manifest, - mode, - renderers, - resolve, - serverLike, - streaming, - }: Pick< - AppPipeline, - 'logger' | 'manifest' | 'mode' | 'renderers' | 'resolve' | 'serverLike' | 'streaming' - >) { - return new AppPipeline(logger, manifest, mode, renderers, resolve, serverLike, streaming); + #manifestData: ManifestData | undefined; + + static create( + manifestData: ManifestData, + { + logger, + manifest, + mode, + renderers, + resolve, + serverLike, + streaming, + }: Pick< + AppPipeline, + 'logger' | 'manifest' | 'mode' | 'renderers' | 'resolve' | 'serverLike' | 'streaming' + > + ) { + const pipeline = new AppPipeline( + logger, + manifest, + mode, + renderers, + resolve, + serverLike, + streaming + ); + pipeline.#manifestData = manifestData; + return pipeline; } headElements(routeData: RouteData): Pick { @@ -47,11 +66,69 @@ export class AppPipeline extends Pipeline { } componentMetadata() {} - getComponentByRoute(_routeData: RouteData): Promise { - throw new Error('unimplemented'); + async getComponentByRoute(routeData: RouteData): Promise { + const module = await this.getModuleForRoute(routeData); + return module.page(); } - tryReroute(_reroutePayload: ReroutePayload): Promise<[RouteData, ComponentInstance]> { - throw new Error('unimplemented'); + async tryReroute(payload: ReroutePayload): Promise<[RouteData, ComponentInstance]> { + let foundRoute; + + for (const route of this.#manifestData!.routes) { + if (payload instanceof URL) { + if (route.pattern.test(payload.pathname)) { + foundRoute = route; + break; + } + } else if (payload instanceof Request) { + const url = new URL(payload.url); + if (route.pattern.test(url.pathname)) { + foundRoute = route; + break; + } + } else { + if (route.pattern.test(decodeURI(payload))) { + foundRoute = route; + break; + } + } + } + + if (foundRoute) { + const componentInstance = await this.getComponentByRoute(foundRoute); + return [foundRoute, componentInstance]; + } else { + // TODO: handle error properly + throw new Error('Route not found'); + } + } + + async getModuleForRoute(route: RouteData): Promise { + if (route.component === DEFAULT_404_COMPONENT) { + return { + page: async () => + ({ default: () => new Response(null, { status: 404 }) }) as ComponentInstance, + 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; + } else { + 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." + ); + } + } } } diff --git a/packages/astro/src/core/render-context.ts b/packages/astro/src/core/render-context.ts index 0eb7df01dc44..bec6feba3a25 100644 --- a/packages/astro/src/core/render-context.ts +++ b/packages/astro/src/core/render-context.ts @@ -408,7 +408,6 @@ export class RenderContext { } }; - return { generator: astroStaticPartial.generator, glob: astroStaticPartial.glob, diff --git a/packages/astro/test/reroute.test.js b/packages/astro/test/reroute.test.js index e90f4e6723ce..ba0ea41bc31a 100644 --- a/packages/astro/test/reroute.test.js +++ b/packages/astro/test/reroute.test.js @@ -2,6 +2,7 @@ import { describe, it, before, after } from 'node:test'; import { loadFixture } from './test-utils.js'; import { load as cheerioLoad } from 'cheerio'; import assert from 'node:assert/strict'; +import testAdapter from './test-adapter.js'; describe('Dev reroute', () => { /** @type {import('./test-utils').Fixture} */ @@ -102,3 +103,65 @@ describe('Build reroute', () => { assert.equal($('h1').text(), 'Index'); }); }); + +describe('SSR reroute', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + let app; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/reroute/', + output: 'server', + adapter: testAdapter(), + }); + await fixture.build(); + app = await fixture.loadTestAdapterApp(); + }); + + it('the render the index page when navigating /reroute ', async () => { + const request = new Request('http://example.com/reroute'); + const response = await app.render(request); + const html = await response.text(); + const $ = cheerioLoad(html); + + assert.equal($('h1').text(), 'Index'); + }); + + it('the render the index page when navigating /blog/hello ', async () => { + const request = new Request('http://example.com/blog/hello'); + const response = await app.render(request); + const html = await response.text(); + const $ = cheerioLoad(html); + + assert.equal($('h1').text(), 'Index'); + }); + + it('the render the index page when navigating /blog/salut ', async () => { + const request = new Request('http://example.com/blog/salut'); + const response = await app.render(request); + const html = await response.text(); + + const $ = cheerioLoad(html); + + assert.equal($('h1').text(), 'Index'); + }); + + it('the render the index page when navigating dynamic route /dynamic/[id] ', async () => { + const request = new Request('http://example.com/dynamic/hello'); + const response = await app.render(request); + const html = await response.text(); + const $ = cheerioLoad(html); + + assert.equal($('h1').text(), 'Index'); + }); + + it('the render the index page when navigating spread route /spread/[...spread] ', async () => { + const request = new Request('http://example.com/spread/hello'); + const response = await app.render(request); + const html = await response.text(); + const $ = cheerioLoad(html); + + assert.equal($('h1').text(), 'Index'); + }); +}); From 5621a91601ba0b223192df2ad75f4efb7579125a Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Tue, 23 Apr 2024 17:52:39 +0100 Subject: [PATCH 04/18] feat: rerouting in the middleware (#10853) * feat: implement reroute in dev (#10818) * chore: implement reroute in dev * chore: revert naming change * chore: conditionally create the new request * chore: handle error * remove only * remove only * chore: add tests and remove logs * chore: fix regression * chore: fix regression route matching * chore: remove unwanted test * feat: reroute in SSG (#10843) * feat: rerouting in ssg * linting * feat: rerouting in ssg * linting * feat: reroute for SSR * fix rebase * fix merge issue * feat: implement the `next(payload)` feature for rerouting * chore: revert code * chore: fix code * Apply suggestions from code review Co-authored-by: Bjorn Lu --------- Co-authored-by: Bjorn Lu --- packages/astro/src/@types/astro.ts | 5 +++ .../src/core/middleware/callMiddleware.ts | 8 +++-- .../astro/src/core/middleware/sequence.ts | 26 +++++++++++++-- packages/astro/src/core/render-context.ts | 8 ++--- .../test/fixtures/reroute/astro.config.mjs | 3 +- .../test/fixtures/reroute/src/middleware.js | 33 +++++++++++++++++++ .../reroute/src/pages/auth/base.astro | 10 ++++++ .../reroute/src/pages/auth/dashboard.astro | 10 ++++++ .../reroute/src/pages/auth/settings.astro | 10 ++++++ .../fixtures/reroute/src/pages/index.astro | 2 ++ packages/astro/test/reroute.test.js | 33 +++++++++++++++++++ 11 files changed, 138 insertions(+), 10 deletions(-) create mode 100644 packages/astro/test/fixtures/reroute/src/middleware.js create mode 100644 packages/astro/test/fixtures/reroute/src/pages/auth/base.astro create mode 100644 packages/astro/test/fixtures/reroute/src/pages/auth/dashboard.astro create mode 100644 packages/astro/test/fixtures/reroute/src/pages/auth/settings.astro diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index cc9c2054166a..67b89ba48f2e 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -2626,6 +2626,11 @@ export interface APIContext< */ redirect: AstroSharedContext['redirect']; + /** + * TODO: docs + */ + reroute: AstroSharedContext['reroute']; + /** * An object that middlewares can use to store extra information related to the request. * diff --git a/packages/astro/src/core/middleware/callMiddleware.ts b/packages/astro/src/core/middleware/callMiddleware.ts index 5a0456680a6c..755291be39cb 100644 --- a/packages/astro/src/core/middleware/callMiddleware.ts +++ b/packages/astro/src/core/middleware/callMiddleware.ts @@ -43,13 +43,17 @@ import { AstroError, AstroErrorData } from '../errors/index.js'; export async function callMiddleware( onRequest: MiddlewareHandler, apiContext: APIContext, - responseFunction: (reroutePayload?: ReroutePayload) => Promise | Response + responseFunction: ( + apiContext: APIContext, + reroutePayload?: ReroutePayload + ) => Promise | Response ): Promise { let nextCalled = false; let responseFunctionPromise: Promise | Response | undefined = undefined; const next: MiddlewareNext = async (payload) => { nextCalled = true; - responseFunctionPromise = responseFunction(payload); + // We need to pass the APIContext pass to `callMiddleware` because it can be mutated across middleware functions + responseFunctionPromise = responseFunction(apiContext, payload); return responseFunctionPromise; }; diff --git a/packages/astro/src/core/middleware/sequence.ts b/packages/astro/src/core/middleware/sequence.ts index 5a0842d8f842..3782bc30befa 100644 --- a/packages/astro/src/core/middleware/sequence.ts +++ b/packages/astro/src/core/middleware/sequence.ts @@ -1,5 +1,6 @@ import type { APIContext, MiddlewareHandler, ReroutePayload } from '../../@types/astro.js'; import { defineMiddleware } from './index.js'; +import { AstroCookies } from '../cookies/cookies.js'; // From SvelteKit: https://github.com/sveltejs/kit/blob/master/packages/kit/src/exports/hooks/sequence.js /** @@ -10,12 +11,16 @@ export function sequence(...handlers: MiddlewareHandler[]): MiddlewareHandler { const filtered = handlers.filter((h) => !!h); const length = filtered.length; if (!length) { - return defineMiddleware((context, next) => { + return defineMiddleware((_context, next) => { return next(); }); } return defineMiddleware((context, next) => { + /** + * This variable is used to carry the rerouting payload across middleware functions. + */ + let carriedPayload: ReroutePayload | undefined = undefined; return applyHandle(0, context); function applyHandle(i: number, handleContext: APIContext) { @@ -25,9 +30,26 @@ export function sequence(...handlers: MiddlewareHandler[]): MiddlewareHandler { // doing a loop over all the `next` functions, and eventually we call the last `next` that returns the `Response`. const result = handle(handleContext, async (payload: ReroutePayload) => { 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 + ); + } + carriedPayload = payload; + handleContext.request = newRequest; + handleContext.url = new URL(newRequest.url); + handleContext.cookies = new AstroCookies(newRequest); + } return applyHandle(i + 1, handleContext); } else { - return next(payload); + return next(payload ?? carriedPayload); } }); return result; diff --git a/packages/astro/src/core/render-context.ts b/packages/astro/src/core/render-context.ts index bec6feba3a25..efada3e215a7 100644 --- a/packages/astro/src/core/render-context.ts +++ b/packages/astro/src/core/render-context.ts @@ -111,7 +111,7 @@ export class RenderContext { statusText: 'Loop Detected', }); } - const lastNext: MiddlewareNext = async (payload) => { + const lastNext = async (ctx: APIContext, payload?: ReroutePayload) => { if (payload) { if (this.pipeline.manifest.reroutingEnabled) { try { @@ -135,7 +135,7 @@ export class RenderContext { } switch (this.routeData.type) { case 'endpoint': - return renderEndpoint(componentInstance as any, apiContext, serverLike, logger); + return renderEndpoint(componentInstance as any, ctx, serverLike, logger); case 'redirect': return renderRedirect(this); case 'page': { @@ -174,9 +174,7 @@ export class RenderContext { } }; - const response = this.isRerouting - ? await lastNext() - : await callMiddleware(middleware, apiContext, lastNext); + const response = await callMiddleware(middleware, apiContext, lastNext); if (response.headers.get(ROUTE_TYPE_HEADER)) { response.headers.delete(ROUTE_TYPE_HEADER); } diff --git a/packages/astro/test/fixtures/reroute/astro.config.mjs b/packages/astro/test/fixtures/reroute/astro.config.mjs index af736916179e..6d20ec89a31d 100644 --- a/packages/astro/test/fixtures/reroute/astro.config.mjs +++ b/packages/astro/test/fixtures/reroute/astro.config.mjs @@ -4,5 +4,6 @@ import { defineConfig } from 'astro/config'; export default defineConfig({ experimental: { rerouting: true - } + }, + site: "https://example.com" }); diff --git a/packages/astro/test/fixtures/reroute/src/middleware.js b/packages/astro/test/fixtures/reroute/src/middleware.js new file mode 100644 index 000000000000..0cf03c1d7713 --- /dev/null +++ b/packages/astro/test/fixtures/reroute/src/middleware.js @@ -0,0 +1,33 @@ +import { sequence } from 'astro:middleware'; + +let contextReroute = false; + +export const first = async (context, next) => { + if (context.url.pathname.includes('/auth')) { + } + + return next(); +}; + +export const second = async (context, next) => { + if (context.url.pathname.includes('/auth')) { + if (context.url.pathname.includes('/auth/dashboard')) { + contextReroute = true; + return await context.reroute('/'); + } + if (context.url.pathname.includes('/auth/base')) { + return await next('/'); + } + } + return next(); +}; + +export const third = async (context, next) => { + // just making sure that we are testing the change in context coming from `next()` + if (context.url.pathname.startsWith('/') && contextReroute === false) { + context.locals.auth = 'Third function called'; + } + return next(); +}; + +export const onRequest = sequence(first, second, third); diff --git a/packages/astro/test/fixtures/reroute/src/pages/auth/base.astro b/packages/astro/test/fixtures/reroute/src/pages/auth/base.astro new file mode 100644 index 000000000000..be31dfb14141 --- /dev/null +++ b/packages/astro/test/fixtures/reroute/src/pages/auth/base.astro @@ -0,0 +1,10 @@ +--- +--- + + + Base + + +

Base

+ + diff --git a/packages/astro/test/fixtures/reroute/src/pages/auth/dashboard.astro b/packages/astro/test/fixtures/reroute/src/pages/auth/dashboard.astro new file mode 100644 index 000000000000..bfa006aa01a7 --- /dev/null +++ b/packages/astro/test/fixtures/reroute/src/pages/auth/dashboard.astro @@ -0,0 +1,10 @@ +--- +--- + + + Dashboard + + +

Dashboard

+ + diff --git a/packages/astro/test/fixtures/reroute/src/pages/auth/settings.astro b/packages/astro/test/fixtures/reroute/src/pages/auth/settings.astro new file mode 100644 index 000000000000..9eee5fe95149 --- /dev/null +++ b/packages/astro/test/fixtures/reroute/src/pages/auth/settings.astro @@ -0,0 +1,10 @@ +--- +--- + + + Settings + + +

Settings

+ + diff --git a/packages/astro/test/fixtures/reroute/src/pages/index.astro b/packages/astro/test/fixtures/reroute/src/pages/index.astro index 727a45a65758..91a6fd0fb0fc 100644 --- a/packages/astro/test/fixtures/reroute/src/pages/index.astro +++ b/packages/astro/test/fixtures/reroute/src/pages/index.astro @@ -1,4 +1,5 @@ --- +const auth = Astro.locals.auth; --- @@ -6,5 +7,6 @@

Index

+ {auth ?

Called auth

: ""} diff --git a/packages/astro/test/reroute.test.js b/packages/astro/test/reroute.test.js index ba0ea41bc31a..3a9273258e81 100644 --- a/packages/astro/test/reroute.test.js +++ b/packages/astro/test/reroute.test.js @@ -165,3 +165,36 @@ describe('SSR reroute', () => { assert.equal($('h1').text(), 'Index'); }); }); + +describe('Middleware', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + let devServer; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/reroute/', + }); + devServer = await fixture.startDevServer(); + }); + + after(async () => { + await devServer.stop(); + }); + + it('should render a locals populated in the third middleware function, because we use next("/")', async () => { + const html = await fixture.fetch('/auth/base').then((res) => res.text()); + const $ = cheerioLoad(html); + + assert.equal($('h1').text(), 'Index'); + assert.equal($('p').text(), 'Called auth'); + }); + + it('should NOT render locals populated in the third middleware function, because we use ctx.reroute("/")', async () => { + const html = await fixture.fetch('/auth/dashboard').then((res) => res.text()); + const $ = cheerioLoad(html); + + assert.equal($('h1').text(), 'Index'); + assert.equal($('p').text(), ''); + }); +}); From d7f0a476d5ecaf2c4cb1b43160de887407ff9ba2 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Wed, 24 Apr 2024 13:11:23 +0100 Subject: [PATCH 05/18] feat: rerouting --- .changeset/pink-ligers-share.md | 49 +++++++++++++ packages/astro/src/@types/astro.ts | 68 +++++++++++++++++-- packages/astro/src/core/app/pipeline.ts | 1 - .../src/core/middleware/callMiddleware.ts | 16 ++++- packages/astro/src/core/render-context.ts | 8 ++- .../src/vite-plugin-astro-server/pipeline.ts | 1 - 6 files changed, 132 insertions(+), 11 deletions(-) create mode 100644 .changeset/pink-ligers-share.md diff --git a/.changeset/pink-ligers-share.md b/.changeset/pink-ligers-share.md new file mode 100644 index 000000000000..84d5eb922039 --- /dev/null +++ b/.changeset/pink-ligers-share.md @@ -0,0 +1,49 @@ +--- +"astro": minor +--- + +Add experimental rerouting in Astro, via `reroute()` function and `next()` function. + +The feature is available via experimental flag: + +```js +export default defineConfig({ + experimental: { + rerouting: true + } +}) +``` + +When enabled, you can use `reroute()` to **render** another page without changing the URL of the browser in Astro pages and endpoints. + +```astro +--- +// src/pages/dashboard.astro +if (!Astro.props.allowed) { + return Astro.reroute("/") +} +--- +``` + +```js +// src/pages/api.js +export function GET(ctx) { + if (!ctx.locals.allowed) { + return ctx.reroute("/") + } +} +``` + +The middleware `next()` function now accepts the same payload of the `reroute()` function. For example, with `next("/")`, you can call the next middleware function with a new `Request`. + +```js +// src/middleware.js +export function onRequest(ctx, next) { + if (!ctx.cookies.get("allowed")) { + return next("/") // new signature + } + return next(); +} +``` + +> **NOTE**: please [read the RFC](https://github.com/withastro/roadmap/blob/feat/reroute/proposals/0047-rerouting.md) to understand the current expectations of the new APIs. diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 67b89ba48f2e..c060b99ea626 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -251,7 +251,16 @@ export interface AstroGlobal< */ redirect: AstroSharedContext['redirect']; /** - * TODO add documentation + * It reroutes to another page. As opposed to redirects, the URL won't change, and Astro will render the HTML emitted + * by the rerouted URL passed as argument. + * + * ## Example + * + * ```js + * if (pageIsNotEnabled) { + * return Astro.reroute('/fallback-page') + * } + * ``` */ reroute: AstroSharedContext['reroute']; /** @@ -1645,7 +1654,7 @@ export interface AstroUserConfig { domains?: Record; }; - /** ⚠️ WARNING: SUBJECT TO CHANGE */ + /** ! WARNING: SUBJECT TO CHANGE */ db?: Config.Database; /** @@ -1932,10 +1941,38 @@ export interface AstroUserConfig { * @name experimental.rerouting * @type {boolean} * @default `false` - * @version 4.6.0 + * @version 4.8.0 * @description * - * TODO + * Enables the use of rerouting features in Astro pages, Endpoints and Astro middleware: + * + * ```astro + * --- + * // src/pages/dashboard.astro + * if (!Astro.props.allowed) { + * return Astro.reroute("/") + * } + * --- + * ``` + * + * ```js + * // src/pages/api.js + * export function GET(ctx) { + * if (!ctx.locals.allowed) { + * return ctx.reroute("/") + * } + * } + * ``` + * + * ```js + * // src/middleware.js + * export function onRequest(ctx, next) { + * if (!ctx.cookies.get("allowed")) { + * return next("/") // new signature + * } + * return next(); + * } + * ``` */ rerouting: boolean; }; @@ -2508,7 +2545,16 @@ interface AstroSharedContext< redirect(path: string, status?: ValidRedirectStatus): Response; /** - * TODO: add documentation + * It reroutes to another page. As opposed to redirects, the URL won't change, and Astro will render the HTML emitted + * by the rerouted URL passed as argument. + * + * ## Example + * + * ```js + * if (pageIsNotEnabled) { + * return Astro.reroute('/fallback-page') + * } + * ``` */ reroute(reroutePayload: ReroutePayload): Promise; @@ -2627,7 +2673,17 @@ export interface APIContext< redirect: AstroSharedContext['redirect']; /** - * TODO: docs + * It reroutes to another page. As opposed to redirects, the URL won't change, and Astro will render the HTML emitted + * by the rerouted URL passed as argument. + * + * ## Example + * + * ```ts + * // src/pages/secret.ts + * export function GET(ctx) { + * return ctx.reroute(new URL("../"), ctx.url); + * } + * ``` */ reroute: AstroSharedContext['reroute']; diff --git a/packages/astro/src/core/app/pipeline.ts b/packages/astro/src/core/app/pipeline.ts index 8b00a1c4fe7d..97784dc962ef 100644 --- a/packages/astro/src/core/app/pipeline.ts +++ b/packages/astro/src/core/app/pipeline.ts @@ -98,7 +98,6 @@ export class AppPipeline extends Pipeline { const componentInstance = await this.getComponentByRoute(foundRoute); return [foundRoute, componentInstance]; } else { - // TODO: handle error properly throw new Error('Route not found'); } } diff --git a/packages/astro/src/core/middleware/callMiddleware.ts b/packages/astro/src/core/middleware/callMiddleware.ts index 755291be39cb..baf3c0b3ef5a 100644 --- a/packages/astro/src/core/middleware/callMiddleware.ts +++ b/packages/astro/src/core/middleware/callMiddleware.ts @@ -5,6 +5,7 @@ import type { ReroutePayload, } from '../../@types/astro.js'; import { AstroError, AstroErrorData } from '../errors/index.js'; +import type { Logger } from '../logger/core.js'; /** * Utility function that is in charge of calling the middleware. @@ -46,14 +47,25 @@ export async function callMiddleware( responseFunction: ( apiContext: APIContext, reroutePayload?: ReroutePayload - ) => Promise | Response + ) => Promise | Response, + // TODO: remove these two arguments once rerouting goes out of experimental + enableRerouting: boolean, + logger: Logger ): Promise { let nextCalled = false; let responseFunctionPromise: Promise | Response | undefined = undefined; const next: MiddlewareNext = async (payload) => { nextCalled = true; + if (enableRerouting) { + responseFunctionPromise = responseFunction(apiContext, payload); + } else { + logger.warn( + 'router', + 'You tried to use the routing feature without enabling it via experimental flag. This is not allowed.' + ); + responseFunctionPromise = responseFunction(apiContext); + } // We need to pass the APIContext pass to `callMiddleware` because it can be mutated across middleware functions - responseFunctionPromise = responseFunction(apiContext, payload); return responseFunctionPromise; }; diff --git a/packages/astro/src/core/render-context.ts b/packages/astro/src/core/render-context.ts index efada3e215a7..12796f3c205e 100644 --- a/packages/astro/src/core/render-context.ts +++ b/packages/astro/src/core/render-context.ts @@ -174,7 +174,13 @@ export class RenderContext { } }; - const response = await callMiddleware(middleware, apiContext, lastNext); + const response = await callMiddleware( + middleware, + apiContext, + lastNext, + this.pipeline.manifest.reroutingEnabled, + this.pipeline.logger + ); if (response.headers.get(ROUTE_TYPE_HEADER)) { response.headers.delete(ROUTE_TYPE_HEADER); } diff --git a/packages/astro/src/vite-plugin-astro-server/pipeline.ts b/packages/astro/src/vite-plugin-astro-server/pipeline.ts index 6176786fb289..66bf9394d803 100644 --- a/packages/astro/src/vite-plugin-astro-server/pipeline.ts +++ b/packages/astro/src/vite-plugin-astro-server/pipeline.ts @@ -220,7 +220,6 @@ export class DevPipeline extends Pipeline { const componentInstance = await this.getComponentByRoute(foundRoute); return [foundRoute, componentInstance]; } else { - // TODO: handle error properly throw new Error('Route not found'); } } From 6ad885019df3101d4e01ba0c7be65ce2d9688b0e Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Mon, 6 May 2024 10:16:52 +0100 Subject: [PATCH 06/18] chore: rename to `rewrite` --- packages/astro/src/@types/astro.ts | 20 ++++++++--------- packages/astro/src/core/app/pipeline.ts | 4 ++-- packages/astro/src/core/app/types.ts | 2 +- packages/astro/src/core/base-pipeline.ts | 6 ++--- packages/astro/src/core/build/generate.ts | 4 ++-- packages/astro/src/core/build/pipeline.ts | 4 ++-- .../src/core/build/plugins/plugin-manifest.ts | 2 +- packages/astro/src/core/config/schema.ts | 4 ++-- .../src/core/middleware/callMiddleware.ts | 4 ++-- packages/astro/src/core/middleware/index.ts | 6 ++--- .../astro/src/core/middleware/sequence.ts | 6 ++--- packages/astro/src/core/render-context.ts | 22 +++++++++---------- .../src/vite-plugin-astro-server/pipeline.ts | 4 ++-- .../src/vite-plugin-astro-server/plugin.ts | 2 +- .../test/fixtures/reroute/astro.config.mjs | 2 +- .../test/fixtures/reroute/src/middleware.js | 2 +- .../reroute/src/pages/blog/hello/index.astro | 2 +- .../reroute/src/pages/blog/salut/index.astro | 2 +- .../reroute/src/pages/dynamic/[id].astro | 2 +- .../fixtures/reroute/src/pages/reroute.astro | 2 +- .../reroute/src/pages/spread/[...id].astro | 2 +- .../test/{reroute.test.js => rewrite.test.js} | 0 22 files changed, 52 insertions(+), 52 deletions(-) rename packages/astro/test/{reroute.test.js => rewrite.test.js} (100%) diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index c060b99ea626..b4c7e362026d 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -262,7 +262,7 @@ export interface AstroGlobal< * } * ``` */ - reroute: AstroSharedContext['reroute']; + rewrite: AstroSharedContext['rewrite']; /** * The element allows a component to reference itself recursively. * @@ -1938,19 +1938,19 @@ export interface AstroUserConfig { /** * @docs - * @name experimental.rerouting + * @name experimental.rewriting * @type {boolean} * @default `false` * @version 4.8.0 * @description * - * Enables the use of rerouting features in Astro pages, Endpoints and Astro middleware: + * Enables the use of rewriting features in Astro pages, Endpoints and Astro middleware: * * ```astro * --- * // src/pages/dashboard.astro * if (!Astro.props.allowed) { - * return Astro.reroute("/") + * return Astro.rewrite("/") * } * --- * ``` @@ -1959,7 +1959,7 @@ export interface AstroUserConfig { * // src/pages/api.js * export function GET(ctx) { * if (!ctx.locals.allowed) { - * return ctx.reroute("/") + * return ctx.rewrite("/") * } * } * ``` @@ -1974,7 +1974,7 @@ export interface AstroUserConfig { * } * ``` */ - rerouting: boolean; + rewriting: boolean; }; } @@ -2556,7 +2556,7 @@ interface AstroSharedContext< * } * ``` */ - reroute(reroutePayload: ReroutePayload): Promise; + rewrite(rewritePayload: RewritePayload): Promise; /** * Object accessed via Astro middleware @@ -2685,7 +2685,7 @@ export interface APIContext< * } * ``` */ - reroute: AstroSharedContext['reroute']; + rewrite: AstroSharedContext['rewrite']; /** * An object that middlewares can use to store extra information related to the request. @@ -2881,9 +2881,9 @@ export interface AstroIntegration { }; } -export type ReroutePayload = string | URL | Request; +export type RewritePayload = string | URL | Request; -export type MiddlewareNext = (reroutePayload?: ReroutePayload) => Promise; +export type MiddlewareNext = (reroutePayload?: RewritePayload) => Promise; export type MiddlewareHandler = ( context: APIContext, next: MiddlewareNext diff --git a/packages/astro/src/core/app/pipeline.ts b/packages/astro/src/core/app/pipeline.ts index 97784dc962ef..cceecab49acd 100644 --- a/packages/astro/src/core/app/pipeline.ts +++ b/packages/astro/src/core/app/pipeline.ts @@ -4,7 +4,7 @@ import type { SSRElement, SSRResult, ComponentInstance, - ReroutePayload, + RewritePayload, } from '../../@types/astro.js'; import { Pipeline } from '../base-pipeline.js'; import { DEFAULT_404_COMPONENT } from '../constants.js'; @@ -71,7 +71,7 @@ export class AppPipeline extends Pipeline { return module.page(); } - async tryReroute(payload: ReroutePayload): Promise<[RouteData, ComponentInstance]> { + async tryRewrite(payload: RewritePayload): Promise<[RouteData, ComponentInstance]> { let foundRoute; for (const route of this.#manifestData!.routes) { diff --git a/packages/astro/src/core/app/types.ts b/packages/astro/src/core/app/types.ts index 6b327fd6fefa..30134252ef9d 100644 --- a/packages/astro/src/core/app/types.ts +++ b/packages/astro/src/core/app/types.ts @@ -66,7 +66,7 @@ export type SSRManifest = { middleware: MiddlewareHandler; checkOrigin: boolean; // TODO: remove once the experimental flag is removed - reroutingEnabled: boolean; + rewritingEnabled: boolean; }; export type SSRManifestI18n = { diff --git a/packages/astro/src/core/base-pipeline.ts b/packages/astro/src/core/base-pipeline.ts index 4f6c82553995..11cff7c809f5 100644 --- a/packages/astro/src/core/base-pipeline.ts +++ b/packages/astro/src/core/base-pipeline.ts @@ -1,7 +1,7 @@ import type { ComponentInstance, MiddlewareHandler, - ReroutePayload, + RewritePayload, RouteData, RuntimeMode, SSRLoadedRenderer, @@ -69,9 +69,9 @@ export abstract class Pipeline { * * - if not `RouteData` is found * - * @param {ReroutePayload} reroutePayload + * @param {RewritePayload} rewritePayload */ - abstract tryReroute(reroutePayload: ReroutePayload): Promise<[RouteData, ComponentInstance]>; + abstract tryRewrite(rewritePayload: RewritePayload): Promise<[RouteData, ComponentInstance]>; /** * Tells the pipeline how to retrieve a component give a `RouteData` diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index 5d5401e5c9c0..77effe3a652e 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -283,7 +283,7 @@ async function getPathsForRoute( const label = staticPaths.length === 1 ? 'page' : 'pages'; logger.debug( 'build', - `├── ${bold(green('✔'))} ${route.component} → ${magenta(`[${staticPaths.length} ${label}]`)}` + `├── ${bold(green('√'))} ${route.component} → ${magenta(`[${staticPaths.length} ${label}]`)}` ); paths = staticPaths @@ -556,7 +556,7 @@ function createBuildManifest( i18n: i18nManifest, buildFormat: settings.config.build.format, middleware, - reroutingEnabled: settings.config.experimental.rerouting, + rewritingEnabled: settings.config.experimental.rewriting, checkOrigin: settings.config.experimental.security?.csrfProtection?.origin ?? false, }; } diff --git a/packages/astro/src/core/build/pipeline.ts b/packages/astro/src/core/build/pipeline.ts index 5acc14682ea5..e545e2ab3346 100644 --- a/packages/astro/src/core/build/pipeline.ts +++ b/packages/astro/src/core/build/pipeline.ts @@ -1,6 +1,6 @@ import type { ComponentInstance, - ReroutePayload, + RewritePayload, RouteData, SSRLoadedRenderer, SSRResult, @@ -273,7 +273,7 @@ export class BuildPipeline extends Pipeline { } } - async tryReroute(payload: ReroutePayload): Promise<[RouteData, ComponentInstance]> { + async tryRewrite(payload: RewritePayload): Promise<[RouteData, ComponentInstance]> { let foundRoute: RouteData | undefined; // options.manifest is the actual type that contains the information for (const route of this.options.manifest.routes) { diff --git a/packages/astro/src/core/build/plugins/plugin-manifest.ts b/packages/astro/src/core/build/plugins/plugin-manifest.ts index 8e5d77ca4ba4..5bb6ddab038a 100644 --- a/packages/astro/src/core/build/plugins/plugin-manifest.ts +++ b/packages/astro/src/core/build/plugins/plugin-manifest.ts @@ -277,6 +277,6 @@ function buildManifest( i18n: i18nManifest, buildFormat: settings.config.build.format, checkOrigin: settings.config.experimental.security?.csrfProtection?.origin ?? false, - reroutingEnabled: settings.config.experimental.rerouting, + rewritingEnabled: settings.config.experimental.rewriting, }; } diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts index 879589b4b87d..0fd4c58e66fd 100644 --- a/packages/astro/src/core/config/schema.ts +++ b/packages/astro/src/core/config/schema.ts @@ -87,7 +87,7 @@ const ASTRO_CONFIG_DEFAULTS = { globalRoutePriority: false, i18nDomains: false, security: {}, - rerouting: false, + rewriting: false, }, } satisfies AstroUserConfig & { server: { open: boolean } }; @@ -526,7 +526,7 @@ export const AstroConfigSchema = z.object({ .optional() .default(ASTRO_CONFIG_DEFAULTS.experimental.security), i18nDomains: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.i18nDomains), - rerouting: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.rerouting), + rewriting: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.rewriting), }) .strict( `Invalid or outdated experimental feature.\nCheck for incorrect spelling or outdated Astro version.\nSee https://docs.astro.build/en/reference/configuration-reference/#experimental-flags for a list of all current experiments.` diff --git a/packages/astro/src/core/middleware/callMiddleware.ts b/packages/astro/src/core/middleware/callMiddleware.ts index baf3c0b3ef5a..a6e8012e84c0 100644 --- a/packages/astro/src/core/middleware/callMiddleware.ts +++ b/packages/astro/src/core/middleware/callMiddleware.ts @@ -2,7 +2,7 @@ import type { APIContext, MiddlewareHandler, MiddlewareNext, - ReroutePayload, + RewritePayload, } from '../../@types/astro.js'; import { AstroError, AstroErrorData } from '../errors/index.js'; import type { Logger } from '../logger/core.js'; @@ -46,7 +46,7 @@ export async function callMiddleware( apiContext: APIContext, responseFunction: ( apiContext: APIContext, - reroutePayload?: ReroutePayload + reroutePayload?: RewritePayload ) => Promise | Response, // TODO: remove these two arguments once rerouting goes out of experimental enableRerouting: boolean, diff --git a/packages/astro/src/core/middleware/index.ts b/packages/astro/src/core/middleware/index.ts index cabbbc9cb9b3..17c206d6e9f5 100644 --- a/packages/astro/src/core/middleware/index.ts +++ b/packages/astro/src/core/middleware/index.ts @@ -1,4 +1,4 @@ -import type { APIContext, MiddlewareHandler, Params, ReroutePayload } from '../../@types/astro.js'; +import type { APIContext, MiddlewareHandler, Params, RewritePayload } from '../../@types/astro.js'; import { computeCurrentLocale, computePreferredLocale, @@ -47,7 +47,7 @@ function createContext({ const route = url.pathname; // TODO verify that this function works in an edge middleware environment - const reroute = (_reroutePayload: ReroutePayload) => { + const reroute = (_reroutePayload: RewritePayload) => { // return dummy response return Promise.resolve(new Response(null)); }; @@ -59,7 +59,7 @@ function createContext({ site: undefined, generator: `Astro v${ASTRO_VERSION}`, props: {}, - reroute, + rewrite: reroute, redirect(path, status) { return new Response(null, { status: status || 302, diff --git a/packages/astro/src/core/middleware/sequence.ts b/packages/astro/src/core/middleware/sequence.ts index 3782bc30befa..ef27d03c2cb4 100644 --- a/packages/astro/src/core/middleware/sequence.ts +++ b/packages/astro/src/core/middleware/sequence.ts @@ -1,4 +1,4 @@ -import type { APIContext, MiddlewareHandler, ReroutePayload } from '../../@types/astro.js'; +import type { APIContext, MiddlewareHandler, RewritePayload } from '../../@types/astro.js'; import { defineMiddleware } from './index.js'; import { AstroCookies } from '../cookies/cookies.js'; @@ -20,7 +20,7 @@ export function sequence(...handlers: MiddlewareHandler[]): MiddlewareHandler { /** * This variable is used to carry the rerouting payload across middleware functions. */ - let carriedPayload: ReroutePayload | undefined = undefined; + let carriedPayload: RewritePayload | undefined = undefined; return applyHandle(0, context); function applyHandle(i: number, handleContext: APIContext) { @@ -28,7 +28,7 @@ export function sequence(...handlers: MiddlewareHandler[]): MiddlewareHandler { // @ts-expect-error // SAFETY: Usually `next` always returns something in user land, but in `sequence` we are actually // doing a loop over all the `next` functions, and eventually we call the last `next` that returns the `Response`. - const result = handle(handleContext, async (payload: ReroutePayload) => { + const result = handle(handleContext, async (payload: RewritePayload) => { if (i < length - 1) { if (payload) { let newRequest; diff --git a/packages/astro/src/core/render-context.ts b/packages/astro/src/core/render-context.ts index 12796f3c205e..6ba23fb72796 100644 --- a/packages/astro/src/core/render-context.ts +++ b/packages/astro/src/core/render-context.ts @@ -5,7 +5,7 @@ import type { ComponentInstance, MiddlewareHandler, MiddlewareNext, - ReroutePayload, + RewritePayload, RouteData, SSRResult, } from '../@types/astro.js'; @@ -111,11 +111,11 @@ export class RenderContext { statusText: 'Loop Detected', }); } - const lastNext = async (ctx: APIContext, payload?: ReroutePayload) => { + const lastNext = async (ctx: APIContext, payload?: RewritePayload) => { if (payload) { - if (this.pipeline.manifest.reroutingEnabled) { + if (this.pipeline.manifest.rewritingEnabled) { try { - const [routeData, component] = await pipeline.tryReroute(payload); + const [routeData, component] = await pipeline.tryRewrite(payload); this.routeData = routeData; componentInstance = component; } catch (e) { @@ -178,7 +178,7 @@ export class RenderContext { middleware, apiContext, lastNext, - this.pipeline.manifest.reroutingEnabled, + this.pipeline.manifest.rewritingEnabled, this.pipeline.logger ); if (response.headers.get(ROUTE_TYPE_HEADER)) { @@ -198,10 +198,10 @@ export class RenderContext { const redirect = (path: string, status = 302) => new Response(null, { status, headers: { Location: path } }); - const reroute = async (reroutePayload: ReroutePayload) => { + const rewrite = async (reroutePayload: RewritePayload) => { pipeline.logger.debug('router', 'Called rerouting to:', reroutePayload); try { - const [routeData, component] = await pipeline.tryReroute(reroutePayload); + const [routeData, component] = await pipeline.tryRewrite(reroutePayload); this.routeData = routeData; if (reroutePayload instanceof Request) { this.request = reroutePayload; @@ -257,7 +257,7 @@ export class RenderContext { }, props, redirect, - reroute, + rewrite, request: this.request, site: pipeline.site, url, @@ -385,10 +385,10 @@ export class RenderContext { return new Response(null, { status, headers: { Location: path } }); }; - const reroute = async (reroutePayload: ReroutePayload) => { + const rewrite = async (reroutePayload: RewritePayload) => { try { pipeline.logger.debug('router', 'Calling rerouting: ', reroutePayload); - const [routeData, component] = await pipeline.tryReroute(reroutePayload); + const [routeData, component] = await pipeline.tryRewrite(reroutePayload); this.routeData = routeData; if (reroutePayload instanceof Request) { this.request = reroutePayload; @@ -431,7 +431,7 @@ export class RenderContext { }, locals, redirect, - reroute, + rewrite, request: this.request, response, site: pipeline.site, diff --git a/packages/astro/src/vite-plugin-astro-server/pipeline.ts b/packages/astro/src/vite-plugin-astro-server/pipeline.ts index 66bf9394d803..f7ee8f83ab5e 100644 --- a/packages/astro/src/vite-plugin-astro-server/pipeline.ts +++ b/packages/astro/src/vite-plugin-astro-server/pipeline.ts @@ -4,7 +4,7 @@ import type { ComponentInstance, DevToolbarMetadata, ManifestData, - ReroutePayload, + RewritePayload, RouteData, SSRElement, SSRLoadedRenderer, @@ -190,7 +190,7 @@ export class DevPipeline extends Pipeline { } } - async tryReroute(payload: ReroutePayload): Promise<[RouteData, ComponentInstance]> { + async tryRewrite(payload: RewritePayload): Promise<[RouteData, ComponentInstance]> { let foundRoute; if (!this.manifestData) { throw new Error('Missing manifest data'); diff --git a/packages/astro/src/vite-plugin-astro-server/plugin.ts b/packages/astro/src/vite-plugin-astro-server/plugin.ts index 10b9ff463768..3c6f06ee9a05 100644 --- a/packages/astro/src/vite-plugin-astro-server/plugin.ts +++ b/packages/astro/src/vite-plugin-astro-server/plugin.ts @@ -145,7 +145,7 @@ export function createDevelopmentManifest(settings: AstroSettings): SSRManifest inlinedScripts: new Map(), i18n: i18nManifest, checkOrigin: settings.config.experimental.security?.csrfProtection?.origin ?? false, - reroutingEnabled: settings.config.experimental.rerouting, + rewritingEnabled: settings.config.experimental.rewriting, middleware(_, next) { return next(); }, diff --git a/packages/astro/test/fixtures/reroute/astro.config.mjs b/packages/astro/test/fixtures/reroute/astro.config.mjs index 6d20ec89a31d..af13ef19b477 100644 --- a/packages/astro/test/fixtures/reroute/astro.config.mjs +++ b/packages/astro/test/fixtures/reroute/astro.config.mjs @@ -3,7 +3,7 @@ import { defineConfig } from 'astro/config'; // https://astro.build/config export default defineConfig({ experimental: { - rerouting: true + rewriting: true }, site: "https://example.com" }); diff --git a/packages/astro/test/fixtures/reroute/src/middleware.js b/packages/astro/test/fixtures/reroute/src/middleware.js index 0cf03c1d7713..4d7c2a7956c8 100644 --- a/packages/astro/test/fixtures/reroute/src/middleware.js +++ b/packages/astro/test/fixtures/reroute/src/middleware.js @@ -13,7 +13,7 @@ export const second = async (context, next) => { if (context.url.pathname.includes('/auth')) { if (context.url.pathname.includes('/auth/dashboard')) { contextReroute = true; - return await context.reroute('/'); + return await context.rewrite('/'); } if (context.url.pathname.includes('/auth/base')) { return await next('/'); diff --git a/packages/astro/test/fixtures/reroute/src/pages/blog/hello/index.astro b/packages/astro/test/fixtures/reroute/src/pages/blog/hello/index.astro index 07a8544aecb1..8c38e518a7b7 100644 --- a/packages/astro/test/fixtures/reroute/src/pages/blog/hello/index.astro +++ b/packages/astro/test/fixtures/reroute/src/pages/blog/hello/index.astro @@ -1,5 +1,5 @@ --- -return Astro.reroute(new URL("../../", Astro.url)) +return Astro.rewrite(new URL("../../", Astro.url)) --- diff --git a/packages/astro/test/fixtures/reroute/src/pages/blog/salut/index.astro b/packages/astro/test/fixtures/reroute/src/pages/blog/salut/index.astro index 373653afd7e0..89d35ce2564d 100644 --- a/packages/astro/test/fixtures/reroute/src/pages/blog/salut/index.astro +++ b/packages/astro/test/fixtures/reroute/src/pages/blog/salut/index.astro @@ -1,5 +1,5 @@ --- -return Astro.reroute(new Request(new URL("../../", Astro.url))) +return Astro.rewrite(new Request(new URL("../../", Astro.url))) --- diff --git a/packages/astro/test/fixtures/reroute/src/pages/dynamic/[id].astro b/packages/astro/test/fixtures/reroute/src/pages/dynamic/[id].astro index fc1b64fbba1a..8d849de160bf 100644 --- a/packages/astro/test/fixtures/reroute/src/pages/dynamic/[id].astro +++ b/packages/astro/test/fixtures/reroute/src/pages/dynamic/[id].astro @@ -7,7 +7,7 @@ export function getStaticPaths() { } -return Astro.reroute("/") +return Astro.rewrite("/") --- diff --git a/packages/astro/test/fixtures/reroute/src/pages/reroute.astro b/packages/astro/test/fixtures/reroute/src/pages/reroute.astro index 8396946f144b..dbc7a6ae628a 100644 --- a/packages/astro/test/fixtures/reroute/src/pages/reroute.astro +++ b/packages/astro/test/fixtures/reroute/src/pages/reroute.astro @@ -1,5 +1,5 @@ --- -return Astro.reroute("/") +return Astro.rewrite("/") --- diff --git a/packages/astro/test/fixtures/reroute/src/pages/spread/[...id].astro b/packages/astro/test/fixtures/reroute/src/pages/spread/[...id].astro index 1169380cab62..0bab88d0f7b1 100644 --- a/packages/astro/test/fixtures/reroute/src/pages/spread/[...id].astro +++ b/packages/astro/test/fixtures/reroute/src/pages/spread/[...id].astro @@ -5,7 +5,7 @@ export function getStaticPaths() { ]; } -return Astro.reroute("/") +return Astro.rewrite("/") --- diff --git a/packages/astro/test/reroute.test.js b/packages/astro/test/rewrite.test.js similarity index 100% rename from packages/astro/test/reroute.test.js rename to packages/astro/test/rewrite.test.js From 973ddf65cda8fb21dd9860d3e8efd979e909d865 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Mon, 6 May 2024 10:22:54 +0100 Subject: [PATCH 07/18] chore: better error message --- packages/astro/src/core/render-context.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/astro/src/core/render-context.ts b/packages/astro/src/core/render-context.ts index 6ba23fb72796..055830b31383 100644 --- a/packages/astro/src/core/render-context.ts +++ b/packages/astro/src/core/render-context.ts @@ -104,11 +104,11 @@ export class RenderContext { const apiContext = this.createAPIContext(props); this.counter++; - if (this.counter == 4) { + if (this.counter === 4) { return new Response('Loop Detected', { // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/508 status: 508, - statusText: 'Loop Detected', + statusText: 'Astro detected a loop where you tried to call the rewriting logic more than four times.', }); } const lastNext = async (ctx: APIContext, payload?: RewritePayload) => { From f34c8add7aa235e48a53c7d5945a8c8487676484 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Mon, 6 May 2024 16:13:18 +0100 Subject: [PATCH 08/18] chore: update the chageset --- .changeset/pink-ligers-share.md | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/.changeset/pink-ligers-share.md b/.changeset/pink-ligers-share.md index 84d5eb922039..32372cd45b19 100644 --- a/.changeset/pink-ligers-share.md +++ b/.changeset/pink-ligers-share.md @@ -2,25 +2,26 @@ "astro": minor --- -Add experimental rerouting in Astro, via `reroute()` function and `next()` function. - +Add experimental rewriting in Astro, via `rewrite()` function and `next()` function. + The feature is available via experimental flag: ```js export default defineConfig({ experimental: { - rerouting: true + rewriting: true } }) ``` -When enabled, you can use `reroute()` to **render** another page without changing the URL of the browser in Astro pages and endpoints. +When enabled, you can use `rewrite()` to **render +** another page without changing the URL of the browser in Astro pages and endpoints. ```astro --- // src/pages/dashboard.astro if (!Astro.props.allowed) { - return Astro.reroute("/") + return Astro.rewrite("/") } --- ``` @@ -28,22 +29,23 @@ if (!Astro.props.allowed) { ```js // src/pages/api.js export function GET(ctx) { - if (!ctx.locals.allowed) { - return ctx.reroute("/") - } + if (!ctx.locals.allowed) { + return ctx.rewrite("/") + } } ``` -The middleware `next()` function now accepts the same payload of the `reroute()` function. For example, with `next("/")`, you can call the next middleware function with a new `Request`. +The middleware `next()` function now accepts the same payload of the `rewrite()` function. For example, with `next("/")`, you can call the next middleware function with a new `Request`. ```js // src/middleware.js export function onRequest(ctx, next) { - if (!ctx.cookies.get("allowed")) { - return next("/") // new signature - } - return next(); + if (!ctx.cookies.get("allowed")) { + return next("/") // new signature + } + return next(); } ``` -> **NOTE**: please [read the RFC](https://github.com/withastro/roadmap/blob/feat/reroute/proposals/0047-rerouting.md) to understand the current expectations of the new APIs. +> **NOTE +**: please [read the RFC](https://github.com/withastro/roadmap/blob/feat/reroute/proposals/0047-rerouting.md) to understand the current expectations of the new APIs. From 18804bf3bf1737cf867ca668b514bc213da3f2de Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Tue, 7 May 2024 13:03:58 +0100 Subject: [PATCH 09/18] Apply suggestions from code review Co-authored-by: Sarah Rainsberger --- packages/astro/src/@types/astro.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index b4c7e362026d..8e20dd06e4d5 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -1944,9 +1944,19 @@ export interface AstroUserConfig { * @version 4.8.0 * @description * - * Enables the use of rewriting features in Astro pages, Endpoints and Astro middleware: + * Enables a routing feature for rewriting requests in Astro pages, Endpoints and Astro middleware, giving you programmatic control over your routes. * - * ```astro + * ```js + * { + * experimental: { + * rewriting: true, + * }, + * } + * ``` + * + * Use `Astro.rewrite` in your `.astro` files to reroute to a different page: + * + * ```astro "rewrite" * --- * // src/pages/dashboard.astro * if (!Astro.props.allowed) { @@ -1973,6 +1983,8 @@ export interface AstroUserConfig { * return next(); * } * ``` + * + * For a complete overview, and to give feedback on this experimental API, see the [Rerouting RFC](https://github.com/withastro/roadmap/blob/feat/reroute/proposals/0047-rerouting.md). */ rewriting: boolean; }; From 9eb5dbc37898f15846c20abefecde4e4fc86820f Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Tue, 7 May 2024 13:16:38 +0100 Subject: [PATCH 10/18] chore: update docs based on feedback --- packages/astro/src/@types/astro.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 8e20dd06e4d5..0289fa1b209d 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -1965,6 +1965,8 @@ export interface AstroUserConfig { * --- * ``` * + * Use `context.rewrite` in your endpoint files to reroute to a different page: + * * ```js * // src/pages/api.js * export function GET(ctx) { @@ -1974,6 +1976,8 @@ export interface AstroUserConfig { * } * ``` * + * Use `next("/")` in your middleware file to reroute to a different page, and still call the next middleware function: + * * ```js * // src/middleware.js * export function onRequest(ctx, next) { From c0149cd32b7a35bd8beda677da295426b97e83bc Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Tue, 7 May 2024 13:21:01 +0100 Subject: [PATCH 11/18] lock file --- pnpm-lock.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index be362ddc2641..8a32dab848f9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3114,6 +3114,12 @@ importers: specifier: workspace:* version: link:../../.. + packages/astro/test/fixtures/middleware-virtual: + dependencies: + astro: + specifier: workspace:* + version: link:../../.. + packages/astro/test/fixtures/minification-html: dependencies: astro: @@ -3312,6 +3318,12 @@ importers: specifier: workspace:* version: link:../../.. + packages/astro/test/fixtures/reroute: + dependencies: + astro: + specifier: workspace:* + version: link:../../.. + packages/astro/test/fixtures/root-srcdir-css: dependencies: astro: From d3f22caac7216c0679139bce950624a19d92844b Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Tue, 7 May 2024 16:14:17 +0100 Subject: [PATCH 12/18] Apply suggestions from code review Co-authored-by: Sarah Rainsberger Co-authored-by: Matthew Phillips Co-authored-by: Ben Holmes --- .changeset/pink-ligers-share.md | 6 +++--- packages/astro/src/@types/astro.ts | 18 +++++++++--------- packages/astro/src/core/app/pipeline.ts | 10 ++++------ .../src/core/middleware/callMiddleware.ts | 4 ++-- packages/astro/src/core/render-context.ts | 8 ++++---- .../src/vite-plugin-astro-server/pipeline.ts | 2 +- 6 files changed, 23 insertions(+), 25 deletions(-) diff --git a/.changeset/pink-ligers-share.md b/.changeset/pink-ligers-share.md index 32372cd45b19..8a4410d0390f 100644 --- a/.changeset/pink-ligers-share.md +++ b/.changeset/pink-ligers-share.md @@ -2,9 +2,9 @@ "astro": minor --- -Add experimental rewriting in Astro, via `rewrite()` function and `next()` function. +Adds experimental rewriting in Astro, via `rewrite()` function and `next()` function. -The feature is available via experimental flag: +The feature is available via an experimental flag in `astro.config.mjs`: ```js export default defineConfig({ @@ -35,7 +35,7 @@ export function GET(ctx) { } ``` -The middleware `next()` function now accepts the same payload of the `rewrite()` function. For example, with `next("/")`, you can call the next middleware function with a new `Request`. +The middleware `next()` function now accepts a parameter with the same type as the `rewrite()` function. For example, with `next("/")`, you can call the next middleware function with a new `Request`. ```js // src/middleware.js diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 0289fa1b209d..f9d2f4fe261e 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -251,14 +251,14 @@ export interface AstroGlobal< */ redirect: AstroSharedContext['redirect']; /** - * It reroutes to another page. As opposed to redirects, the URL won't change, and Astro will render the HTML emitted - * by the rerouted URL passed as argument. + * It rewrites to another page. As opposed to redirects, the URL won't change, and Astro will render the HTML emitted + * by the rewritten URL passed as argument. * * ## Example * * ```js * if (pageIsNotEnabled) { - * return Astro.reroute('/fallback-page') + * return Astro.rewrite('/fallback-page') * } * ``` */ @@ -1944,7 +1944,7 @@ export interface AstroUserConfig { * @version 4.8.0 * @description * - * Enables a routing feature for rewriting requests in Astro pages, Endpoints and Astro middleware, giving you programmatic control over your routes. + * Enables a routing feature for rewriting requests in Astro pages, endpoints and Astro middleware, giving you programmatic control over your routes. * * ```js * { @@ -1976,7 +1976,7 @@ export interface AstroUserConfig { * } * ``` * - * Use `next("/")` in your middleware file to reroute to a different page, and still call the next middleware function: + * Use `next("/")` in your middleware file to reroute to a different page, and then call the next middleware function: * * ```js * // src/middleware.js @@ -2561,14 +2561,14 @@ interface AstroSharedContext< redirect(path: string, status?: ValidRedirectStatus): Response; /** - * It reroutes to another page. As opposed to redirects, the URL won't change, and Astro will render the HTML emitted + * It rewrites to another page. As opposed to redirects, the URL won't change, and Astro will render the HTML emitted * by the rerouted URL passed as argument. * * ## Example * * ```js * if (pageIsNotEnabled) { - * return Astro.reroute('/fallback-page') + * return Astro.rewrite('/fallback-page') * } * ``` */ @@ -2697,7 +2697,7 @@ export interface APIContext< * ```ts * // src/pages/secret.ts * export function GET(ctx) { - * return ctx.reroute(new URL("../"), ctx.url); + * return ctx.rewrite(new URL("../"), ctx.url); * } * ``` */ @@ -2899,7 +2899,7 @@ export interface AstroIntegration { export type RewritePayload = string | URL | Request; -export type MiddlewareNext = (reroutePayload?: RewritePayload) => Promise; +export type MiddlewareNext = (rewritePayload?: RewritePayload) => Promise; export type MiddlewareHandler = ( context: APIContext, next: MiddlewareNext diff --git a/packages/astro/src/core/app/pipeline.ts b/packages/astro/src/core/app/pipeline.ts index cceecab49acd..423f77b53c25 100644 --- a/packages/astro/src/core/app/pipeline.ts +++ b/packages/astro/src/core/app/pipeline.ts @@ -97,9 +97,8 @@ export class AppPipeline extends Pipeline { if (foundRoute) { const componentInstance = await this.getComponentByRoute(foundRoute); return [foundRoute, componentInstance]; - } else { - throw new Error('Route not found'); } + throw new Error('Route not found'); } async getModuleForRoute(route: RouteData): Promise { @@ -123,10 +122,9 @@ export class AppPipeline extends Pipeline { return await importComponentInstance(); } else if (this.manifest.pageModule) { return this.manifest.pageModule; - } else { - 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." - ); + 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." + ); } } } diff --git a/packages/astro/src/core/middleware/callMiddleware.ts b/packages/astro/src/core/middleware/callMiddleware.ts index a6e8012e84c0..b92e0f3cb19b 100644 --- a/packages/astro/src/core/middleware/callMiddleware.ts +++ b/packages/astro/src/core/middleware/callMiddleware.ts @@ -46,7 +46,7 @@ export async function callMiddleware( apiContext: APIContext, responseFunction: ( apiContext: APIContext, - reroutePayload?: RewritePayload + rewritePayload?: RewritePayload ) => Promise | Response, // TODO: remove these two arguments once rerouting goes out of experimental enableRerouting: boolean, @@ -61,7 +61,7 @@ export async function callMiddleware( } else { logger.warn( 'router', - 'You tried to use the routing feature without enabling it via experimental flag. This is not allowed.' + 'The rewrite API is experimental. To use this feature, add the `rewriting` flag to the `experimental` object in your Astro config.' ); responseFunctionPromise = responseFunction(apiContext); } diff --git a/packages/astro/src/core/render-context.ts b/packages/astro/src/core/render-context.ts index 055830b31383..2db6935162f4 100644 --- a/packages/astro/src/core/render-context.ts +++ b/packages/astro/src/core/render-context.ts @@ -129,7 +129,7 @@ export class RenderContext { } else { this.pipeline.logger.warn( 'router', - 'You tried to use the routing feature without enabling it via experimental flag. This is not allowed.' + 'The rewrite API is experimental. To use this feature, add the `rewriting` flag to the `experimental` object in your Astro config.' ); } } @@ -199,7 +199,7 @@ export class RenderContext { new Response(null, { status, headers: { Location: path } }); const rewrite = async (reroutePayload: RewritePayload) => { - pipeline.logger.debug('router', 'Called rerouting to:', reroutePayload); + pipeline.logger.debug('router', 'Called rewriting to:', reroutePayload); try { const [routeData, component] = await pipeline.tryRewrite(reroutePayload); this.routeData = routeData; @@ -217,7 +217,7 @@ export class RenderContext { this.isRerouting = true; return await this.render(component); } catch (e) { - pipeline.logger.debug('router', 'Routing failed.', e); + pipeline.logger.debug('router', 'Rewrite failed.', e); return new Response('Not found', { status: 404, statusText: 'Not found', @@ -387,7 +387,7 @@ export class RenderContext { const rewrite = async (reroutePayload: RewritePayload) => { try { - pipeline.logger.debug('router', 'Calling rerouting: ', reroutePayload); + pipeline.logger.debug('router', 'Calling rewrite: ', reroutePayload); const [routeData, component] = await pipeline.tryRewrite(reroutePayload); this.routeData = routeData; if (reroutePayload instanceof Request) { diff --git a/packages/astro/src/vite-plugin-astro-server/pipeline.ts b/packages/astro/src/vite-plugin-astro-server/pipeline.ts index f7ee8f83ab5e..84820bfb1246 100644 --- a/packages/astro/src/vite-plugin-astro-server/pipeline.ts +++ b/packages/astro/src/vite-plugin-astro-server/pipeline.ts @@ -193,7 +193,7 @@ export class DevPipeline extends Pipeline { async tryRewrite(payload: RewritePayload): Promise<[RouteData, ComponentInstance]> { let foundRoute; if (!this.manifestData) { - throw new Error('Missing manifest data'); + throw new Error('Missing manifest data. This is an internal error, please file an issue.'); } for (const route of this.manifestData.routes) { From a63885fba6e4acb535297a043bb58e0b11415fb3 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Tue, 7 May 2024 16:33:53 +0100 Subject: [PATCH 13/18] feedback --- .changeset/pink-ligers-share.md | 6 ++---- packages/astro/src/@types/astro.ts | 8 ++++---- packages/astro/src/core/app/pipeline.ts | 14 ++++++-------- packages/astro/src/core/build/pipeline.ts | 12 ++++++------ packages/astro/src/core/errors/errors-data.ts | 12 ++++++++++++ packages/astro/src/core/render-context.ts | 3 ++- .../astro/src/vite-plugin-astro-server/pipeline.ts | 13 ++++++------- 7 files changed, 38 insertions(+), 30 deletions(-) diff --git a/.changeset/pink-ligers-share.md b/.changeset/pink-ligers-share.md index 8a4410d0390f..29bb1c9d2fed 100644 --- a/.changeset/pink-ligers-share.md +++ b/.changeset/pink-ligers-share.md @@ -14,8 +14,7 @@ export default defineConfig({ }) ``` -When enabled, you can use `rewrite()` to **render -** another page without changing the URL of the browser in Astro pages and endpoints. +When enabled, you can use `rewrite()` to **render** another page without changing the URL of the browser in Astro pages and endpoints. ```astro --- @@ -47,5 +46,4 @@ export function onRequest(ctx, next) { } ``` -> **NOTE -**: please [read the RFC](https://github.com/withastro/roadmap/blob/feat/reroute/proposals/0047-rerouting.md) to understand the current expectations of the new APIs. +> **NOTE**: please [read the RFC](https://github.com/withastro/roadmap/blob/feat/reroute/proposals/0047-rerouting.md) to understand the current expectations of the new APIs. diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index f9d2f4fe261e..ba9cfe3a7d09 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -1944,7 +1944,7 @@ export interface AstroUserConfig { * @version 4.8.0 * @description * - * Enables a routing feature for rewriting requests in Astro pages, endpoints and Astro middleware, giving you programmatic control over your routes. + * Enables a routing feature for rewriting requests in Astro pages, endpoints and Astro middleware, giving you programmatic control over your routes. * * ```js * { @@ -1966,7 +1966,7 @@ export interface AstroUserConfig { * ``` * * Use `context.rewrite` in your endpoint files to reroute to a different page: - * + * * ```js * // src/pages/api.js * export function GET(ctx) { @@ -1977,7 +1977,7 @@ export interface AstroUserConfig { * ``` * * Use `next("/")` in your middleware file to reroute to a different page, and then call the next middleware function: - * + * * ```js * // src/middleware.js * export function onRequest(ctx, next) { @@ -1987,7 +1987,7 @@ export interface AstroUserConfig { * return next(); * } * ``` - * + * * For a complete overview, and to give feedback on this experimental API, see the [Rerouting RFC](https://github.com/withastro/roadmap/blob/feat/reroute/proposals/0047-rerouting.md). */ rewriting: boolean; diff --git a/packages/astro/src/core/app/pipeline.ts b/packages/astro/src/core/app/pipeline.ts index 423f77b53c25..5697e541c58a 100644 --- a/packages/astro/src/core/app/pipeline.ts +++ b/packages/astro/src/core/app/pipeline.ts @@ -86,11 +86,9 @@ export class AppPipeline extends Pipeline { foundRoute = route; break; } - } else { - if (route.pattern.test(decodeURI(payload))) { - foundRoute = route; - break; - } + } else if (route.pattern.test(decodeURI(payload))) { + foundRoute = route; + break; } } @@ -122,9 +120,9 @@ export class AppPipeline extends Pipeline { 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." - ); + 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." + ); } } } diff --git a/packages/astro/src/core/build/pipeline.ts b/packages/astro/src/core/build/pipeline.ts index e545e2ab3346..532759f1e426 100644 --- a/packages/astro/src/core/build/pipeline.ts +++ b/packages/astro/src/core/build/pipeline.ts @@ -33,6 +33,8 @@ import type { PageBuildData, SinglePageBuiltModule, StaticBuildOptions } from '. import { i18nHasFallback } from './util.js'; import { RedirectSinglePageBuiltModule } from '../redirects/index.js'; import { getOutDirWithinCwd } from './common.js'; +import { RouteNotFound } from '../errors/errors-data.js'; +import { AstroError } from '../errors/index.js'; /** * The build pipeline is responsible to gather the files emitted by the SSR build and generate the pages by executing these files. @@ -288,18 +290,16 @@ export class BuildPipeline extends Pipeline { foundRoute = route; break; } - } else { - if (route.pattern.test(decodeURI(payload))) { - foundRoute = route; - break; - } + } else if (route.pattern.test(decodeURI(payload))) { + foundRoute = route; + break; } } if (foundRoute) { const componentInstance = await this.getComponentByRoute(foundRoute); return [foundRoute, componentInstance]; } else { - throw new Error('Route not found'); + throw new AstroError(RouteNotFound); } } diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts index 180160064ab1..7ebc3a3831a1 100644 --- a/packages/astro/src/core/errors/errors-data.ts +++ b/packages/astro/src/core/errors/errors-data.ts @@ -1483,6 +1483,18 @@ export const UnsupportedConfigTransformError = { hint: 'See the devalue library for all supported types: https://github.com/rich-harris/devalue', } satisfies ErrorData; +/** + * @docs + * @description + * + * Astro couldn't find a route matching the one provided by the user + */ +export const RouteNotFound = { + name: 'RouteNotFound', + title: 'Route not found.', + message: `Astro could find a route that matches the one you requested.`, +} satisfies ErrorData; + // Generic catch-all - Only use this in extreme cases, like if there was a cosmic ray bit flip. export const UnknownError = { name: 'UnknownError', title: 'Unknown Error.' } satisfies ErrorData; diff --git a/packages/astro/src/core/render-context.ts b/packages/astro/src/core/render-context.ts index 2db6935162f4..03cd1d7cc38a 100644 --- a/packages/astro/src/core/render-context.ts +++ b/packages/astro/src/core/render-context.ts @@ -108,7 +108,8 @@ export class RenderContext { 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.', + statusText: + 'Astro detected a loop where you tried to call the rewriting logic more than four times.', }); } const lastNext = async (ctx: APIContext, payload?: RewritePayload) => { diff --git a/packages/astro/src/vite-plugin-astro-server/pipeline.ts b/packages/astro/src/vite-plugin-astro-server/pipeline.ts index 84820bfb1246..aa14ee12cadc 100644 --- a/packages/astro/src/vite-plugin-astro-server/pipeline.ts +++ b/packages/astro/src/vite-plugin-astro-server/pipeline.ts @@ -14,7 +14,7 @@ import { getInfoOutput } from '../cli/info/index.js'; import type { HeadElements } from '../core/base-pipeline.js'; import { ASTRO_VERSION, DEFAULT_404_COMPONENT } from '../core/constants.js'; import { enhanceViteSSRError } from '../core/errors/dev/index.js'; -import { AggregateError, CSSError, MarkdownError } from '../core/errors/index.js'; +import { AggregateError, AstroError, CSSError, MarkdownError } from '../core/errors/index.js'; import type { Logger } from '../core/logger/core.js'; import type { ModuleLoader } from '../core/module-loader/index.js'; import { loadRenderer, Pipeline } from '../core/render/index.js'; @@ -25,6 +25,7 @@ import { getComponentMetadata } from './metadata.js'; import { createResolve } from './resolve.js'; import { default404Page } from './response.js'; import { getScriptsForURL } from './scripts.js'; +import { RouteNotFound } from '../core/errors/errors-data.js'; export class DevPipeline extends Pipeline { // renderers are loaded on every request, @@ -208,11 +209,9 @@ export class DevPipeline extends Pipeline { foundRoute = route; break; } - } else { - if (route.pattern.test(decodeURI(payload))) { - foundRoute = route; - break; - } + } else if (route.pattern.test(decodeURI(payload))) { + foundRoute = route; + break; } } @@ -220,7 +219,7 @@ export class DevPipeline extends Pipeline { const componentInstance = await this.getComponentByRoute(foundRoute); return [foundRoute, componentInstance]; } else { - throw new Error('Route not found'); + throw new AstroError(RouteNotFound); } } From d254dcb252912bb217ffd67dc5fe88e40264e92f Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Tue, 7 May 2024 16:34:38 +0100 Subject: [PATCH 14/18] rename --- packages/astro/src/core/render-context.ts | 12 ++++++------ .../astro/src/vite-plugin-astro-server/pipeline.ts | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/astro/src/core/render-context.ts b/packages/astro/src/core/render-context.ts index 03cd1d7cc38a..279745ac19e4 100644 --- a/packages/astro/src/core/render-context.ts +++ b/packages/astro/src/core/render-context.ts @@ -50,9 +50,9 @@ export class RenderContext { ) {} /** - * A flag that tells the render content if the rerouting was triggered + * A flag that tells the render content if the rewriting was triggered */ - isRerouting = false; + isRewriting = false; /** * A safety net in case of loops */ @@ -125,7 +125,7 @@ export class RenderContext { statusText: 'Not found', }); } finally { - this.isRerouting = true; + this.isRewriting = true; } } else { this.pipeline.logger.warn( @@ -163,7 +163,7 @@ export class RenderContext { if ( this.routeData.route === '/404' || this.routeData.route === '/500' || - this.isRerouting + this.isRewriting ) { response.headers.set(REROUTE_DIRECTIVE_HEADER, 'no'); } @@ -215,7 +215,7 @@ export class RenderContext { this.url = new URL(this.request.url); this.cookies = new AstroCookies(this.request); this.params = getParams(routeData, url.toString()); - this.isRerouting = true; + this.isRewriting = true; return await this.render(component); } catch (e) { pipeline.logger.debug('router', 'Rewrite failed.', e); @@ -402,7 +402,7 @@ export class RenderContext { this.url = new URL(this.request.url); this.cookies = new AstroCookies(this.request); this.params = getParams(routeData, url.toString()); - this.isRerouting = true; + this.isRewriting = true; return await this.render(component); } catch (e) { pipeline.logger.debug('router', 'Rerouting failed, returning a 404.', e); diff --git a/packages/astro/src/vite-plugin-astro-server/pipeline.ts b/packages/astro/src/vite-plugin-astro-server/pipeline.ts index aa14ee12cadc..685d13f570c4 100644 --- a/packages/astro/src/vite-plugin-astro-server/pipeline.ts +++ b/packages/astro/src/vite-plugin-astro-server/pipeline.ts @@ -74,7 +74,7 @@ export class DevPipeline extends Pipeline { mode, settings, } = this; - const filePath = new URL(`./${routeData.component}`, root); + const filePath = new URL(`${routeData.component}`, root); // Add hoisted script tags, skip if direct rendering with `directRenderScript` const { scripts } = settings.config.experimental.directRenderScript ? { scripts: new Set() } @@ -146,7 +146,7 @@ export class DevPipeline extends Pipeline { config: { root }, loader, } = this; - const filePath = new URL(`./${routeData.component}`, root); + const filePath = new URL(`${routeData.component}`, root); return getComponentMetadata(filePath, loader); } @@ -186,7 +186,7 @@ export class DevPipeline extends Pipeline { if (component) { return component; } else { - const filePath = new URL(`./${routeData.component}`, this.config.root); + const filePath = new URL(`${routeData.component}`, this.config.root); return await this.preload(routeData, filePath); } } From 78b13f85da870b280c273220ae722e4bd78aa30f Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Tue, 7 May 2024 16:48:00 +0100 Subject: [PATCH 15/18] add tests for 404 --- .../reroute/src/pages/blog/oops.astro | 11 ++++ packages/astro/test/rewrite.test.js | 53 +++++++++++++------ 2 files changed, 49 insertions(+), 15 deletions(-) create mode 100644 packages/astro/test/fixtures/reroute/src/pages/blog/oops.astro diff --git a/packages/astro/test/fixtures/reroute/src/pages/blog/oops.astro b/packages/astro/test/fixtures/reroute/src/pages/blog/oops.astro new file mode 100644 index 000000000000..df1f1f76a331 --- /dev/null +++ b/packages/astro/test/fixtures/reroute/src/pages/blog/oops.astro @@ -0,0 +1,11 @@ +--- +return Astro.rewrite("/404") +--- + + + Blog hello + + +

Blog hello

+ + diff --git a/packages/astro/test/rewrite.test.js b/packages/astro/test/rewrite.test.js index 3a9273258e81..39ff084a4c80 100644 --- a/packages/astro/test/rewrite.test.js +++ b/packages/astro/test/rewrite.test.js @@ -20,40 +20,47 @@ describe('Dev reroute', () => { await devServer.stop(); }); - it('the render the index page when navigating /reroute ', async () => { + it('should render the index page when navigating /reroute ', async () => { const html = await fixture.fetch('/reroute').then((res) => res.text()); const $ = cheerioLoad(html); assert.equal($('h1').text(), 'Index'); }); - it('the render the index page when navigating /blog/hello ', async () => { + it('should render the index page when navigating /blog/hello ', async () => { const html = await fixture.fetch('/blog/hello').then((res) => res.text()); const $ = cheerioLoad(html); assert.equal($('h1').text(), 'Index'); }); - it('the render the index page when navigating /blog/salut ', async () => { + it('should render the index page when navigating /blog/salut ', async () => { const html = await fixture.fetch('/blog/salut').then((res) => res.text()); const $ = cheerioLoad(html); assert.equal($('h1').text(), 'Index'); }); - it('the render the index page when navigating dynamic route /dynamic/[id] ', async () => { + it('should render the index page when navigating dynamic route /dynamic/[id] ', async () => { const html = await fixture.fetch('/dynamic/hello').then((res) => res.text()); const $ = cheerioLoad(html); assert.equal($('h1').text(), 'Index'); }); - it('the render the index page when navigating spread route /spread/[...spread] ', async () => { + it('should render the index page when navigating spread route /spread/[...spread] ', async () => { const html = await fixture.fetch('/spread/hello').then((res) => res.text()); const $ = cheerioLoad(html); assert.equal($('h1').text(), 'Index'); }); + + it('should render the 404 built-in page', async () => { + const html = await fixture.fetch('/blog/oops').then((res) => res.text()); + const $ = cheerioLoad(html); + + assert.equal($('h1').text(), '404: Not found'); + }); }); describe('Build reroute', () => { @@ -67,21 +74,21 @@ describe('Build reroute', () => { await fixture.build(); }); - it('the render the index page when navigating /reroute ', async () => { + it('should render the index page when navigating /reroute ', async () => { const html = await fixture.readFile('/reroute/index.html'); const $ = cheerioLoad(html); assert.equal($('h1').text(), 'Index'); }); - it('the render the index page when navigating /blog/hello ', async () => { + it('should render the index page when navigating /blog/hello ', async () => { const html = await fixture.readFile('/blog/hello/index.html'); const $ = cheerioLoad(html); assert.equal($('h1').text(), 'Index'); }); - it('the render the index page when navigating /blog/salut ', async () => { + it('should render the index page when navigating /blog/salut ', async () => { const html = await fixture.readFile('/blog/salut/index.html'); const $ = cheerioLoad(html); @@ -89,19 +96,28 @@ describe('Build reroute', () => { assert.equal($('h1').text(), 'Index'); }); - it('the render the index page when navigating dynamic route /dynamic/[id] ', async () => { + it('should render the index page when navigating dynamic route /dynamic/[id] ', async () => { const html = await fixture.readFile('/dynamic/hello/index.html'); const $ = cheerioLoad(html); assert.equal($('h1').text(), 'Index'); }); - it('the render the index page when navigating spread route /spread/[...spread] ', async () => { + it('should render the index page when navigating spread route /spread/[...spread] ', async () => { const html = await fixture.readFile('/spread/hello/index.html'); const $ = cheerioLoad(html); assert.equal($('h1').text(), 'Index'); }); + + it('should render the 404 built-in page', async () => { + try { + const html = await fixture.readFile('/spread/oops/index.html'); + assert.fail('Not found'); + } catch { + assert.ok; + } + }); }); describe('SSR reroute', () => { @@ -119,7 +135,7 @@ describe('SSR reroute', () => { app = await fixture.loadTestAdapterApp(); }); - it('the render the index page when navigating /reroute ', async () => { + it('should render the index page when navigating /reroute ', async () => { const request = new Request('http://example.com/reroute'); const response = await app.render(request); const html = await response.text(); @@ -128,7 +144,7 @@ describe('SSR reroute', () => { assert.equal($('h1').text(), 'Index'); }); - it('the render the index page when navigating /blog/hello ', async () => { + it('should render the index page when navigating /blog/hello ', async () => { const request = new Request('http://example.com/blog/hello'); const response = await app.render(request); const html = await response.text(); @@ -137,7 +153,7 @@ describe('SSR reroute', () => { assert.equal($('h1').text(), 'Index'); }); - it('the render the index page when navigating /blog/salut ', async () => { + it('should render the index page when navigating /blog/salut ', async () => { const request = new Request('http://example.com/blog/salut'); const response = await app.render(request); const html = await response.text(); @@ -147,7 +163,7 @@ describe('SSR reroute', () => { assert.equal($('h1').text(), 'Index'); }); - it('the render the index page when navigating dynamic route /dynamic/[id] ', async () => { + it('should render the index page when navigating dynamic route /dynamic/[id] ', async () => { const request = new Request('http://example.com/dynamic/hello'); const response = await app.render(request); const html = await response.text(); @@ -156,7 +172,7 @@ describe('SSR reroute', () => { assert.equal($('h1').text(), 'Index'); }); - it('the render the index page when navigating spread route /spread/[...spread] ', async () => { + it('should render the index page when navigating spread route /spread/[...spread] ', async () => { const request = new Request('http://example.com/spread/hello'); const response = await app.render(request); const html = await response.text(); @@ -164,6 +180,13 @@ describe('SSR reroute', () => { assert.equal($('h1').text(), 'Index'); }); + + it('should render the 404 built-in page', async () => { + const request = new Request('http://example.com/blog/oops'); + const response = await app.render(request); + const html = await response.text(); + assert.equal(html, 'Not found'); + }); }); describe('Middleware', () => { From f733520b2b355d003c7f0b0ff051b950761f198b Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Tue, 7 May 2024 16:51:00 +0100 Subject: [PATCH 16/18] revert change --- packages/astro/src/core/build/generate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index 77effe3a652e..355d551eaa6c 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -283,7 +283,7 @@ async function getPathsForRoute( const label = staticPaths.length === 1 ? 'page' : 'pages'; logger.debug( 'build', - `├── ${bold(green('√'))} ${route.component} → ${magenta(`[${staticPaths.length} ${label}]`)}` + `├── ${bold(green('✔'))} ${route.component} → ${magenta(`[${staticPaths.length} ${label}]`)}` ); paths = staticPaths From bab2d4173e7f9e182592383ff5932bcd5b7d19d9 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Tue, 7 May 2024 16:52:36 +0100 Subject: [PATCH 17/18] fix regression --- packages/astro/src/core/app/pipeline.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/astro/src/core/app/pipeline.ts b/packages/astro/src/core/app/pipeline.ts index 5697e541c58a..77d2f80b24f2 100644 --- a/packages/astro/src/core/app/pipeline.ts +++ b/packages/astro/src/core/app/pipeline.ts @@ -120,10 +120,10 @@ export class AppPipeline extends Pipeline { 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." - ); } + 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." + ); } } } From ff5602a33adaccbd83d1297bc4f10f3d7dbfd103 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Tue, 7 May 2024 17:54:28 +0100 Subject: [PATCH 18/18] Update .changeset/pink-ligers-share.md Co-authored-by: Sarah Rainsberger --- .changeset/pink-ligers-share.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/pink-ligers-share.md b/.changeset/pink-ligers-share.md index 29bb1c9d2fed..e7923350fb5b 100644 --- a/.changeset/pink-ligers-share.md +++ b/.changeset/pink-ligers-share.md @@ -2,7 +2,7 @@ "astro": minor --- -Adds experimental rewriting in Astro, via `rewrite()` function and `next()` function. +Adds experimental rewriting in Astro with a new `rewrite()` function and the middleware `next()` function. The feature is available via an experimental flag in `astro.config.mjs`: