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: