Skip to content

Commit

Permalink
feat: implement reroute in dev (#10818)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
ematipico committed May 6, 2024
1 parent fd508a0 commit 8b3ae01
Show file tree
Hide file tree
Showing 29 changed files with 465 additions and 86 deletions.
25 changes: 24 additions & 1 deletion packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,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.
*
Expand Down Expand Up @@ -1922,6 +1926,18 @@ export interface AstroUserConfig {
origin?: boolean;
};
};

/**
* @docs
* @name experimental.rerouting
* @type {boolean}
* @default `false`
* @version 4.6.0
* @description
*
* TODO
*/
rerouting: boolean;
};
}

Expand Down Expand Up @@ -2491,6 +2507,11 @@ interface AstroSharedContext<
*/
redirect(path: string, status?: ValidRedirectStatus): Response;

/**
* TODO: add documentation
*/
reroute(reroutePayload: ReroutePayload): Promise<Response>;

/**
* Object accessed via Astro middleware
*/
Expand Down Expand Up @@ -2799,7 +2820,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
Expand Down
15 changes: 14 additions & 1 deletion packages/astro/src/core/app/pipeline.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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');
}
}
2 changes: 2 additions & 0 deletions packages/astro/src/core/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
19 changes: 19 additions & 0 deletions packages/astro/src/core/base-pipeline.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type {
ComponentInstance,
MiddlewareHandler,
ReroutePayload,
RouteData,
RuntimeMode,
SSRLoadedRenderer,
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/core/build/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}
19 changes: 18 additions & 1 deletion packages/astro/src/core/build/pipeline.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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.
Expand Down Expand Up @@ -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');
}
}
1 change: 1 addition & 0 deletions packages/astro/src/core/build/plugins/plugin-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}
2 changes: 2 additions & 0 deletions packages/astro/src/core/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ const ASTRO_CONFIG_DEFAULTS = {
globalRoutePriority: false,
i18nDomains: false,
security: {},
rerouting: false,
},
} satisfies AstroUserConfig & { server: { open: boolean } };

Expand Down Expand Up @@ -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.`
Expand Down
13 changes: 9 additions & 4 deletions packages/astro/src/core/middleware/callMiddleware.ts
Original file line number Diff line number Diff line change
@@ -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';

/**
Expand Down Expand Up @@ -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;
};

Expand Down
14 changes: 9 additions & 5 deletions packages/astro/src/core/middleware/index.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Expand Down Expand Up @@ -49,13 +46,20 @@ 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,
params,
site: undefined,
generator: `Astro v${ASTRO_VERSION}`,
props: {},
reroute,
redirect(path, status) {
return new Response(null, {
status: status || 302,
Expand Down
9 changes: 4 additions & 5 deletions packages/astro/src/core/middleware/sequence.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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) => {
Expand All @@ -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;
Expand Down
Loading

0 comments on commit 8b3ae01

Please sign in to comment.