diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index a695ab2b7d2e..ec06f2450483 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -245,6 +245,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. * @@ -1918,6 +1922,18 @@ export interface AstroUserConfig { origin?: boolean; }; }; + + /** + * @docs + * @name experimental.rerouting + * @type {boolean} + * @default `false` + * @version 4.6.0 + * @description + * + * TODO + */ + rerouting: boolean; }; } @@ -2479,6 +2495,11 @@ interface AstroSharedContext< */ redirect(path: string, status?: ValidRedirectStatus): Response; + /** + * TODO: add documentation + */ + reroute(reroutePayload: ReroutePayload): Promise; + /** * Object accessed via Astro middleware */ @@ -2784,7 +2805,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 381c9e6426c8..402c68c2ab53 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 a89aa10f071c..39e9ebc03a42 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, isServerLikeOutput } 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'; @@ -21,6 +28,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. @@ -225,4 +234,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 c1e87bf458d3..e2f29920477f 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 d9b91a9ce2c6..348a8db04118 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 eb05df6f5db6..877b0f072151 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, }; @@ -249,17 +322,43 @@ export class RenderContext { slotValues: Record | null ): AstroGlobal { 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, }); } 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', + }); + } + }; + const slots = new Slots(result, slotValues, pipeline.logger) as unknown as AstroGlobal['slots']; // `Astro.self` is added by the compiler @@ -283,7 +382,8 @@ export class RenderContext { props, locals, redirect, - request, + reroute, + request: this.request, response, slots, site: pipeline.site, 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 0b7859846ee6..4217ebd139e9 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, resolveIdToUrl, viteID } from '../core/util.js'; import { isServerLikeOutput } from '../prerender/utils.js'; import { PAGE_SCRIPT_ID } from '../vite-plugin-scripts/index.js'; @@ -30,6 +32,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, @@ -44,13 +53,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 { @@ -81,7 +95,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, 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', () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d6c25470b4ab..9879c6585033 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3102,6 +3102,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: @@ -3300,6 +3306,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: