0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2025-02-03 22:29:08 -05:00

feat: implement the next(payload) feature for rerouting

This commit is contained in:
Emanuele Stoppa 2024-04-23 14:57:23 +01:00
parent 242ba26103
commit dbb47d62eb
12 changed files with 137 additions and 10 deletions

View file

@ -2613,6 +2613,11 @@ export interface APIContext<
*/
redirect: AstroSharedContext['redirect'];
/**
* TODO: docs
*/
reroute: AstroSharedContext['reroute'];
/**
* An object that middlewares can use to store extra information related to the request.
*

View file

@ -43,13 +43,17 @@ import { AstroError, AstroErrorData } from '../errors/index.js';
export async function callMiddleware(
onRequest: MiddlewareHandler,
apiContext: APIContext,
responseFunction: (reroutePayload?: ReroutePayload) => Promise<Response> | Response
responseFunction: (
apiContext: APIContext,
reroutePayload?: ReroutePayload
) => Promise<Response> | Response
): Promise<Response> {
let nextCalled = false;
let responseFunctionPromise: Promise<Response> | Response | undefined = undefined;
const next: MiddlewareNext = async (payload) => {
nextCalled = true;
responseFunctionPromise = responseFunction(payload);
// We need to pass the APIContext pass to `callMiddleware` because it can be mutated across middleware functions
responseFunctionPromise = responseFunction(apiContext, payload);
return responseFunctionPromise;
};

View file

@ -30,6 +30,8 @@ export type CreateContext = {
* A list of locales that are supported by the user
*/
userDefinedLocales?: string[];
site?: URL;
};
/**

View file

@ -1,5 +1,6 @@
import type { APIContext, MiddlewareHandler, ReroutePayload } from '../../@types/astro.js';
import { defineMiddleware } from './index.js';
import { AstroCookies } from '../cookies/cookies.js';
// From SvelteKit: https://github.com/sveltejs/kit/blob/master/packages/kit/src/exports/hooks/sequence.js
/**
@ -10,12 +11,16 @@ export function sequence(...handlers: MiddlewareHandler[]): MiddlewareHandler {
const filtered = handlers.filter((h) => !!h);
const length = filtered.length;
if (!length) {
return defineMiddleware((context, next) => {
return defineMiddleware((_context, next) => {
return next();
});
}
return defineMiddleware((context, next) => {
/**
* This variable is used to carry the rerouting payload across middleware functions.
*/
let carriedPayload: ReroutePayload | undefined = undefined;
return applyHandle(0, context);
function applyHandle(i: number, handleContext: APIContext) {
@ -25,9 +30,27 @@ export function sequence(...handlers: MiddlewareHandler[]): MiddlewareHandler {
// doing a loop over all the `next` functions, and eventually we call the last `next` that returns the `Response`.
const result = handle(handleContext, async (payload: ReroutePayload) => {
if (i < length - 1) {
if (payload) {
let newRequest;
if (payload instanceof Request) {
newRequest = payload;
} else if (payload instanceof URL) {
newRequest = new Request(payload, handleContext.request);
} else {
handleContext.request;
newRequest = new Request(
new URL(payload, handleContext.url.origin),
handleContext.request
);
}
carriedPayload = payload;
handleContext.request = newRequest;
handleContext.url = new URL(newRequest.url);
handleContext.cookies = new AstroCookies(newRequest);
}
return applyHandle(i + 1, handleContext);
} else {
return next(payload);
return next(payload ?? carriedPayload);
}
});
return result;

View file

@ -111,7 +111,7 @@ export class RenderContext {
statusText: 'Loop Detected',
});
}
const lastNext: MiddlewareNext = async (payload) => {
const lastNext = async (ctx: APIContext, payload?: ReroutePayload) => {
if (payload) {
if (this.pipeline.manifest.reroutingEnabled) {
try {
@ -135,7 +135,7 @@ export class RenderContext {
}
switch (this.routeData.type) {
case 'endpoint':
return renderEndpoint(componentInstance as any, apiContext, serverLike, logger);
return renderEndpoint(componentInstance as any, ctx, serverLike, logger);
case 'redirect':
return renderRedirect(this);
case 'page': {
@ -174,9 +174,7 @@ export class RenderContext {
}
};
const response = this.isRerouting
? await lastNext()
: await callMiddleware(middleware, apiContext, lastNext);
const response = await callMiddleware(middleware, apiContext, lastNext);
if (response.headers.get(ROUTE_TYPE_HEADER)) {
response.headers.delete(ROUTE_TYPE_HEADER);
}

View file

@ -4,5 +4,6 @@ import { defineConfig } from 'astro/config';
export default defineConfig({
experimental: {
rerouting: true
}
},
site: "https://example.com"
});

View file

@ -0,0 +1,29 @@
import { sequence } from 'astro:middleware';
export const first = async (context, next) => {
if (context.url.pathname.includes('/auth')) {
}
return next();
};
export const second = async (context, next) => {
if (context.url.pathname.includes('/auth')) {
if (context.url.pathname.includes('/auth/dashboard')) {
return await context.reroute('/');
}
if (context.url.pathname.includes('/auth/base')) {
return await next('/');
}
}
return next();
};
export const third = async (context, next) => {
if (context.url.pathname.startsWith('/')) {
context.locals.auth = 'Third function called';
}
return next();
};
export const onRequest = sequence(first, second, third);

View file

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

View file

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

View file

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

View file

@ -1,4 +1,5 @@
---
const auth = Astro.locals.auth;
---
<html>
<head>
@ -6,5 +7,6 @@
</head>
<body>
<h1>Index</h1>
{auth ? <p>Called auth</p>: ""}
</body>
</html>

View file

@ -165,3 +165,36 @@ describe('SSR reroute', () => {
assert.equal($('h1').text(), 'Index');
});
});
describe('Middleware', () => {
/** @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('should render a locals populated in the third middleware function, because we use next("/")', async () => {
const html = await fixture.fetch('/auth/base').then((res) => res.text());
const $ = cheerioLoad(html);
assert.equal($('h1').text(), 'Index');
assert.equal($('p').text(), 'Called auth');
});
it('should NOT render locals populated in the third middleware function, because we use ctx.reroute("/")', async () => {
const html = await fixture.fetch('/auth/dashboard').then((res) => res.text());
const $ = cheerioLoad(html);
assert.equal($('h1').text(), 'Index');
assert.equal($('p').text(), '');
});
});