From 465f1680cd8435dda13506bc00526760d6d5a168 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Wed, 24 Apr 2024 13:11:23 +0100 Subject: [PATCH] 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 0000000000000..84d5eb9220396 --- /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 2a981a1cbd3b4..9a14151d8ffc0 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -246,7 +246,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']; /** @@ -1640,7 +1649,7 @@ export interface AstroUserConfig { domains?: Record; }; - /** ⚠️ WARNING: SUBJECT TO CHANGE */ + /** ! WARNING: SUBJECT TO CHANGE */ db?: Config.Database; /** @@ -1927,10 +1936,38 @@ export interface AstroUserConfig { * @name experimental.rerouting * @type {boolean} * @default `false` - * @version 4.6.0 + * @version 4.7.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; }; @@ -2495,7 +2532,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; @@ -2614,7 +2660,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 8b00a1c4fe7d6..97784dc962ef9 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 755291be39cbe..baf3c0b3ef5a7 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 efada3e215a74..12796f3c205e9 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 7667bb377b125..4a85c20571e9f 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'); } }