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:
parent
a4e7d6650d
commit
ba505e39d7
30 changed files with 477 additions and 86 deletions
|
@ -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.
|
||||
*
|
||||
|
@ -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<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
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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<ComponentInstance> {
|
||||
throw new Error('unimplemented');
|
||||
}
|
||||
|
||||
tryReroute(_reroutePayload: ReroutePayload): Promise<[RouteData, ComponentInstance]> {
|
||||
throw new Error('unimplemented');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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.`
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
@ -249,17 +322,43 @@ export class RenderContext {
|
|||
slotValues: Record<string, any> | 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,
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
|
|
3
packages/astro/test/fixtures/middleware-virtual/astro.config.mjs
vendored
Normal file
3
packages/astro/test/fixtures/middleware-virtual/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
import { defineConfig } from "astro/config";
|
||||
|
||||
export default defineConfig({})
|
8
packages/astro/test/fixtures/middleware-virtual/package.json
vendored
Normal file
8
packages/astro/test/fixtures/middleware-virtual/package.json
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "@test/middleware-virtual",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*"
|
||||
}
|
||||
}
|
6
packages/astro/test/fixtures/middleware-virtual/src/middleware.js
vendored
Normal file
6
packages/astro/test/fixtures/middleware-virtual/src/middleware.js
vendored
Normal 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();
|
||||
});
|
13
packages/astro/test/fixtures/middleware-virtual/src/pages/index.astro
vendored
Normal file
13
packages/astro/test/fixtures/middleware-virtual/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
const data = Astro.locals;
|
||||
---
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>Index</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<span>Index</span>
|
||||
</body>
|
||||
</html>
|
8
packages/astro/test/fixtures/reroute/astro.config.mjs
vendored
Normal file
8
packages/astro/test/fixtures/reroute/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { defineConfig } from 'astro/config';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
experimental: {
|
||||
rerouting: true
|
||||
}
|
||||
});
|
8
packages/astro/test/fixtures/reroute/package.json
vendored
Normal file
8
packages/astro/test/fixtures/reroute/package.json
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "@test/reroute",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*"
|
||||
}
|
||||
}
|
11
packages/astro/test/fixtures/reroute/src/pages/blog/hello/index.astro
vendored
Normal file
11
packages/astro/test/fixtures/reroute/src/pages/blog/hello/index.astro
vendored
Normal 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>
|
11
packages/astro/test/fixtures/reroute/src/pages/blog/salut/index.astro
vendored
Normal file
11
packages/astro/test/fixtures/reroute/src/pages/blog/salut/index.astro
vendored
Normal 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>
|
10
packages/astro/test/fixtures/reroute/src/pages/index.astro
vendored
Normal file
10
packages/astro/test/fixtures/reroute/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
<title>Index</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Index</h1>
|
||||
</body>
|
||||
</html>
|
11
packages/astro/test/fixtures/reroute/src/pages/reroute.astro
vendored
Normal file
11
packages/astro/test/fixtures/reroute/src/pages/reroute.astro
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
return Astro.reroute("/")
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
<title>Reroute</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Reroute</h1>
|
||||
</body>
|
||||
</html>
|
42
packages/astro/test/reroute.test.js
Normal file
42
packages/astro/test/reroute.test.js
Normal 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');
|
||||
});
|
||||
});
|
|
@ -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),
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
12
pnpm-lock.yaml
generated
12
pnpm-lock.yaml
generated
|
@ -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:
|
||||
|
|
Loading…
Add table
Reference in a new issue