0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2025-04-07 23:41:43 -05:00

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
This commit is contained in:
Emanuele Stoppa 2024-04-22 10:49:59 +01:00
parent 2f4d627815
commit 251ab8bb12
29 changed files with 465 additions and 86 deletions

View file

@ -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 <Astro.self /> element allows a component to reference itself recursively.
*
@ -1917,6 +1921,18 @@ export interface AstroUserConfig {
origin?: boolean;
};
};
/**
* @docs
* @name experimental.rerouting
* @type {boolean}
* @default `false`
* @version 4.6.0
* @description
*
* TODO
*/
rerouting: boolean;
};
}
@ -2478,6 +2494,11 @@ interface AstroSharedContext<
*/
redirect(path: string, status?: ValidRedirectStatus): Response;
/**
* TODO: add documentation
*/
reroute(reroutePayload: ReroutePayload): Promise<Response>;
/**
* Object accessed via Astro middleware
*/
@ -2784,7 +2805,9 @@ export interface AstroIntegration {
};
}
export type MiddlewareNext = () => Promise<Response>;
export type ReroutePayload = string | URL | Request;
export type MiddlewareNext = (reroutePayload?: ReroutePayload) => Promise<Response>;
export type MiddlewareHandler = (
context: APIContext,
next: MiddlewareNext

View file

@ -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<ComponentInstance> {
throw new Error('unimplemented');
}
tryReroute(_reroutePayload: ReroutePayload): Promise<[RouteData, ComponentInstance]> {
throw new Error('unimplemented');
}
}

View file

@ -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 = {

View file

@ -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> | HeadElements;
abstract componentMetadata(routeData: RouteData): Promise<SSRResult['componentMetadata']> | 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<ComponentInstance>;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface

View file

@ -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,
};
}

View file

@ -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.
@ -226,4 +235,12 @@ export class BuildPipeline extends Pipeline {
return pages;
}
getComponentByRoute(_routeData: RouteData): Promise<ComponentInstance> {
throw new Error('unimplemented');
}
tryReroute(_reroutePayload: ReroutePayload): Promise<[RouteData, ComponentInstance]> {
throw new Error('unimplemented');
}
}

View file

@ -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,
};
}

View file

@ -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.`

View file

@ -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> | Response
responseFunction: (reroutePayload?: ReroutePayload) => Promise<Response> | Response
): Promise<Response> {
let nextCalled = false;
let responseFunctionPromise: Promise<Response> | Response | undefined = undefined;
const next: MiddlewareNext = async () => {
const next: MiddlewareNext = async (payload) => {
nextCalled = true;
responseFunctionPromise = responseFunction();
responseFunctionPromise = responseFunction(payload);
return responseFunctionPromise;
};

View file

@ -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,

View file

@ -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;

View file

@ -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<RenderContext, 'pathname' | 'pipeline' | 'request' | 'routeData'> &
Partial<Pick<RenderContext, 'locals' | 'middleware' | 'status'>>) {
Partial<Pick<RenderContext, 'locals' | 'middleware' | 'status'>>): RenderContext {
return new RenderContext(
pipeline,
locals,
@ -80,11 +91,11 @@ export class RenderContext {
* - fallback
*/
async render(componentInstance: ComponentInstance | undefined): Promise<Response> {
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<AstroGlobal, 'props' | 'self' | 'slots'> {
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,

View file

@ -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({

View file

@ -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<SSRLoadedRenderer>();
manifestData: ManifestData | undefined;
componentInterner: WeakMap<RouteData, ComponentInstance> = 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<DevPipeline, 'loader' | 'logger' | 'manifest' | 'settings'>) {
return new DevPipeline(loader, logger, manifest, settings);
static create(
manifestData: ManifestData,
{
loader,
logger,
manifest,
settings,
}: Pick<DevPipeline, 'loader' | 'logger' | 'manifest' | 'settings'>
) {
const pipeline = new DevPipeline(loader, logger, manifest, settings);
pipeline.manifestData = manifestData;
return pipeline;
}
async headElements(routeData: RouteData): Promise<HeadElements> {
@ -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<RouteData, ComponentInstance>();
}
async getComponentByRoute(routeData: RouteData): Promise<ComponentInstance> {
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;
}
}

View file

@ -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();
},

View file

@ -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;

View file

@ -0,0 +1,3 @@
import { defineConfig } from "astro/config";
export default defineConfig({})

View file

@ -0,0 +1,8 @@
{
"name": "@test/middleware-virtual",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*"
}
}

View file

@ -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();
});

View file

@ -0,0 +1,13 @@
---
const data = Astro.locals;
---
<html>
<head>
<title>Index</title>
</head>
<body>
<span>Index</span>
</body>
</html>

View file

@ -0,0 +1,8 @@
import { defineConfig } from 'astro/config';
// https://astro.build/config
export default defineConfig({
experimental: {
rerouting: true
}
});

View file

@ -0,0 +1,8 @@
{
"name": "@test/reroute",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*"
}
}

View file

@ -0,0 +1,11 @@
---
return Astro.reroute(new URL("../../", Astro.url))
---
<html>
<head>
<title>Blog hello</title>
</head>
<body>
<h1>Blog hello</h1>
</body>
</html>

View file

@ -0,0 +1,11 @@
---
return Astro.reroute(new Request(new URL("../../", Astro.url)))
---
<html>
<head>
<title>Blog hello</title>
</head>
<body>
<h1>Blog hello</h1>
</body>
</html>

View file

@ -0,0 +1,10 @@
---
---
<html>
<head>
<title>Index</title>
</head>
<body>
<h1>Index</h1>
</body>
</html>

View file

@ -0,0 +1,11 @@
---
return Astro.reroute("/")
---
<html>
<head>
<title>Reroute</title>
</head>
<body>
<h1>Reroute</h1>
</body>
</html>

View file

@ -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');
});
});

View file

@ -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),

View file

@ -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', () => {