Skip to content

Commit 35c8652

Browse files
committed
feat: support middlewares in SSG for endpoints and pages
1 parent 2fa0d18 commit 35c8652

File tree

15 files changed

+240
-92
lines changed

15 files changed

+240
-92
lines changed

packages/astro/src/@types/astro.ts

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1391,11 +1391,6 @@ interface AstroSharedContext<Props extends Record<string, any> = Record<string,
13911391
* TODO documentation
13921392
*/
13931393
locals: Locals;
1394-
1395-
/**
1396-
* TODO documentation
1397-
*/
1398-
setLocal: (key: string, value: any) => void;
13991394
}
14001395

14011396
export interface APIContext<Props extends Record<string, any> = Record<string, any>>
@@ -1562,14 +1557,16 @@ export interface AstroIntegration {
15621557
};
15631558
}
15641559

1565-
export type MiddlewareResolve = (context: APIContext) => Promise<Response>;
1566-
export type MiddlewareHandler = (
1560+
export type MiddlewareResolve<R> = (context: APIContext) => Promise<R>;
1561+
export type MiddlewareHandler<R> = (
15671562
context: Readonly<APIContext>,
1568-
response: MiddlewareResolve
1569-
) => Promise<Response>;
1563+
response: MiddlewareResolve<R>
1564+
) => Promise<R>;
15701565

1571-
export type AstroMiddlewareInstance = {
1572-
onRequest?: MiddlewareHandler;
1566+
// NOTE: when updating this file with other functions,
1567+
// remember to update `plugin-page.ts` too, to add that function as a no-op function.
1568+
export type AstroMiddlewareInstance<R> = {
1569+
onRequest?: MiddlewareHandler<R>;
15731570
};
15741571

15751572
export interface AstroPluginOptions {

packages/astro/src/core/app/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,8 @@ export class App {
231231
status,
232232
});
233233

234-
const result = await callEndpoint(handler, this.#env, ctx, this.#logging);
234+
// TODO PLT-104 add adapter middleware here
235+
const result = await callEndpoint(handler, this.#env, ctx, this.#logging, undefined);
235236

236237
if (result.type === 'response') {
237238
if (result.response.headers.get('X-Astro-Response') === 'Not-Found') {

packages/astro/src/core/build/generate.ts

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@ import type { OutputAsset, OutputChunk } from 'rollup';
66
import { fileURLToPath } from 'url';
77
import type {
88
AstroConfig,
9+
AstroMiddlewareInstance,
910
AstroSettings,
1011
ComponentInstance,
1112
EndpointHandler,
1213
ImageTransform,
14+
MiddlewareHandler,
1315
RouteType,
1416
SSRError,
1517
SSRLoadedRenderer,
18+
EndpointOutput,
1619
} from '../../@types/astro';
1720
import {
1821
generateImage as generateImageInternal,
@@ -27,10 +30,20 @@ import {
2730
} from '../../core/path.js';
2831
import { runHookBuildGenerated } from '../../integrations/index.js';
2932
import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
30-
import { call as callEndpoint, throwIfRedirectNotAllowed } from '../endpoint/index.js';
31-
import { AstroError } from '../errors/index.js';
33+
import {
34+
call as callEndpoint,
35+
createAPIContext,
36+
throwIfRedirectNotAllowed,
37+
} from '../endpoint/index.js';
38+
import { AstroError, AstroErrorData } from '../errors/index.js';
3239
import { debug, info } from '../logger/core.js';
33-
import { createEnvironment, createRenderContext, renderPage } from '../render/index.js';
40+
import {
41+
createEnvironment,
42+
createRenderContext,
43+
getParamsAndProps,
44+
GetParamsAndPropsError,
45+
renderPage,
46+
} from '../render/index.js';
3447
import { callGetStaticPaths } from '../render/route-cache.js';
3548
import { createLinkStylesheetElementSet, createModuleScriptsSet } from '../render/ssr-element.js';
3649
import { createRequest } from '../request.js';
@@ -45,6 +58,7 @@ import {
4558
} from './internal.js';
4659
import type { PageBuildData, SingleFileBuiltModule, StaticBuildOptions } from './types';
4760
import { getTimeStat } from './util.js';
61+
import { callMiddleware } from '../middleware/index.js';
4862

4963
function shouldSkipDraft(pageModule: ComponentInstance, settings: AstroSettings): boolean {
5064
return (
@@ -168,6 +182,7 @@ async function generatePage(
168182
const scripts = pageInfo?.hoistedScript ?? null;
169183

170184
const pageModule = ssrEntry.pageMap?.get(pageData.component);
185+
const middleware = ssrEntry.middleware;
171186

172187
if (!pageModule) {
173188
throw new Error(
@@ -197,7 +212,7 @@ async function generatePage(
197212

198213
for (let i = 0; i < paths.length; i++) {
199214
const path = paths[i];
200-
await generatePath(path, opts, generationOptions);
215+
await generatePath(path, opts, generationOptions, middleware);
201216
const timeEnd = performance.now();
202217
const timeChange = getTimeStat(timeStart, timeEnd);
203218
const timeIncrease = `(+${timeChange})`;
@@ -339,7 +354,8 @@ function getUrlForPath(
339354
async function generatePath(
340355
pathname: string,
341356
opts: StaticBuildOptions,
342-
gopts: GeneratePathOptions
357+
gopts: GeneratePathOptions,
358+
middleware: AstroMiddlewareInstance<unknown>
343359
) {
344360
const { settings, logging, origin, routeCache } = opts;
345361
const { mod, internals, linkIds, scripts: hoistedScripts, pageData, renderers } = gopts;
@@ -412,6 +428,7 @@ async function generatePath(
412428
ssr,
413429
streaming: true,
414430
});
431+
415432
const ctx = createRenderContext({
416433
origin,
417434
pathname,
@@ -426,7 +443,14 @@ async function generatePath(
426443
let encoding: BufferEncoding | undefined;
427444
if (pageData.route.type === 'endpoint') {
428445
const endpointHandler = mod as unknown as EndpointHandler;
429-
const result = await callEndpoint(endpointHandler, env, ctx, logging);
446+
447+
const result = await callEndpoint(
448+
endpointHandler,
449+
env,
450+
ctx,
451+
logging,
452+
middleware as AstroMiddlewareInstance<Response | EndpointOutput>
453+
);
430454

431455
if (result.type === 'response') {
432456
throwIfRedirectNotAllowed(result.response, opts.settings.config);
@@ -441,7 +465,39 @@ async function generatePath(
441465
} else {
442466
let response: Response;
443467
try {
444-
response = await renderPage(mod, ctx, env);
468+
const paramsAndPropsResp = await getParamsAndProps({
469+
mod: mod as any,
470+
route: ctx.route,
471+
routeCache: env.routeCache,
472+
pathname: ctx.pathname,
473+
logging: env.logging,
474+
ssr: env.ssr,
475+
});
476+
477+
if (paramsAndPropsResp === GetParamsAndPropsError.NoMatchingStaticPath) {
478+
throw new AstroError({
479+
...AstroErrorData.NoMatchingStaticPathFound,
480+
message: AstroErrorData.NoMatchingStaticPathFound.message(ctx.pathname),
481+
hint: ctx.route?.component
482+
? AstroErrorData.NoMatchingStaticPathFound.hint([ctx.route?.component])
483+
: '',
484+
});
485+
}
486+
const [params, props] = paramsAndPropsResp;
487+
488+
const context = createAPIContext({
489+
request: ctx.request,
490+
params,
491+
props,
492+
site: env.site,
493+
adapterName: env.adapterName,
494+
});
495+
// If the user doesn't configure a middleware, the rollup plugin emits a no-op function,
496+
// so it's safe to use `callMiddleware` regardless
497+
let onRequest = middleware.onRequest as MiddlewareHandler<Response>;
498+
response = await callMiddleware<Response>(onRequest, context, () => {
499+
return renderPage(mod, ctx, env, context);
500+
});
445501
} catch (err) {
446502
if (!AstroError.is(err) && !(err as SSRError).id && typeof err === 'object') {
447503
(err as SSRError).id = pageData.component;

packages/astro/src/core/build/internal.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ export interface BuildInternals {
7878
// The SSR entry chunk. Kept in internals to share between ssr/client build steps
7979
ssrEntryChunk?: OutputChunk;
8080
componentMetadata: SSRResult['componentMetadata'];
81+
82+
middleware?: ViteID;
8183
}
8284

8385
/**

packages/astro/src/core/build/plugins/plugin-pages.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import type { Plugin as VitePlugin } from 'vite';
22
import type { AstroBuildPlugin } from '../plugin';
33
import type { StaticBuildOptions } from '../types';
4-
54
import { pagesVirtualModuleId, resolvedPagesVirtualModuleId } from '../../app/index.js';
65
import { addRollupInput } from '../add-rollup-input.js';
76
import { eachPageData, hasPrerenderedPages, type BuildInternals } from '../internal.js';
7+
import { MIDDLEWARE_PATH_SEGMENT_NAME } from '../../constants.js';
88

99
export function vitePluginPages(opts: StaticBuildOptions, internals: BuildInternals): VitePlugin {
1010
return {
@@ -22,8 +22,10 @@ export function vitePluginPages(opts: StaticBuildOptions, internals: BuildIntern
2222
}
2323
},
2424

25-
load(id) {
25+
async load(id) {
2626
if (id === resolvedPagesVirtualModuleId) {
27+
const middlewareId = await this.resolve(`./src/${MIDDLEWARE_PATH_SEGMENT_NAME}`);
28+
2729
let importMap = '';
2830
let imports = [];
2931
let i = 0;
@@ -47,8 +49,12 @@ export function vitePluginPages(opts: StaticBuildOptions, internals: BuildIntern
4749

4850
const def = `${imports.join('\n')}
4951
52+
${middlewareId ? `import * as _middleware from "${middlewareId.id}";` : ''}
53+
5054
export const pageMap = new Map([${importMap}]);
51-
export const renderers = [${rendererItems}];`;
55+
export const renderers = [${rendererItems}];
56+
export const middleware = ${middlewareId ? '_middleware' : '{ onRequest() {} }'};
57+
`;
5258

5359
return def;
5460
}

packages/astro/src/core/build/static-build.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
} from '../../core/build/internal.js';
1313
import { emptyDir, removeEmptyDirs } from '../../core/fs/index.js';
1414
import { appendForwardSlash, prependForwardSlash } from '../../core/path.js';
15-
import { isModeServerWithNoAdapter } from '../../core/util.js';
15+
import { isModeServerWithNoAdapter, viteID } from '../../core/util.js';
1616
import { runHookBuildSetup } from '../../integrations/index.js';
1717
import { PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
1818
import { resolvedPagesVirtualModuleId } from '../app/index.js';
@@ -25,6 +25,7 @@ import { createPluginContainer, type AstroBuildPluginContainer } from './plugin.
2525
import { registerAllPlugins } from './plugins/index.js';
2626
import type { PageBuildData, StaticBuildOptions } from './types';
2727
import { getTimeStat } from './util.js';
28+
import { MIDDLEWARE_PATH_SEGMENT_NAME } from '../constants.js';
2829

2930
export async function viteBuild(opts: StaticBuildOptions) {
3031
const { allPages, settings } = opts;
@@ -57,6 +58,11 @@ export async function viteBuild(opts: StaticBuildOptions) {
5758
facadeIdToPageDataMap.set(fileURLToPath(astroModuleURL), pageData);
5859
}
5960

61+
// track middleware
62+
63+
const middlewareURL = new URL('./src/' + MIDDLEWARE_PATH_SEGMENT_NAME, settings.config.root);
64+
internals.middleware = viteID(middlewareURL);
65+
6066
// Empty out the dist folder, if needed. Vite has a config for doing this
6167
// but because we are running 2 vite builds in parallel, that would cause a race
6268
// condition, so we are doing it ourselves
@@ -114,7 +120,7 @@ export async function staticBuild(opts: StaticBuildOptions, internals: BuildInte
114120
case 'static': {
115121
settings.timer.start('Static generate');
116122
await generatePages(opts, internals);
117-
await cleanServerOutput(opts);
123+
// await cleanServerOutput(opts);
118124
settings.timer.end('Static generate');
119125
return;
120126
}

packages/astro/src/core/build/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { default as vite, InlineConfig } from 'vite';
22
import type {
33
AstroConfig,
4+
AstroMiddlewareInstance,
45
AstroSettings,
56
BuildConfig,
67
ComponentInstance,
@@ -44,6 +45,7 @@ export interface StaticBuildOptions {
4445

4546
export interface SingleFileBuiltModule {
4647
pageMap: Map<ComponentPath, ComponentInstance>;
48+
middleware: AstroMiddlewareInstance<unknown>;
4749
renderers: SSRLoadedRenderer[];
4850
}
4951

packages/astro/src/core/endpoint/dev/index.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import type { EndpointHandler } from '../../../@types/astro';
1+
import type {
2+
AstroMiddlewareInstance,
3+
EndpointHandler,
4+
EndpointOutput,
5+
} from '../../../@types/astro';
26
import type { LogOptions } from '../../logger/core';
37
import type { SSROptions } from '../../render/dev';
48
import { createRenderContext } from '../../render/index.js';
@@ -8,6 +12,7 @@ export async function call(options: SSROptions, logging: LogOptions) {
812
const {
913
env,
1014
preload: [, mod],
15+
middleware,
1116
} = options;
1217
const endpointHandler = mod as unknown as EndpointHandler;
1318

@@ -18,5 +23,11 @@ export async function call(options: SSROptions, logging: LogOptions) {
1823
route: options.route,
1924
});
2025

21-
return await callEndpoint(endpointHandler, env, ctx, logging);
26+
return await callEndpoint(
27+
endpointHandler,
28+
env,
29+
ctx,
30+
logging,
31+
middleware as AstroMiddlewareInstance<Response | EndpointOutput>
32+
);
2233
}

packages/astro/src/core/endpoint/index.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
import type { APIContext, AstroConfig, EndpointHandler, Params } from '../../@types/astro';
1+
import type {
2+
APIContext,
3+
AstroConfig,
4+
AstroMiddlewareInstance,
5+
EndpointHandler,
6+
EndpointOutput,
7+
MiddlewareHandler,
8+
Params,
9+
} from '../../@types/astro';
210
import type { Environment, RenderContext } from '../render/index';
311

412
import { renderEndpoint } from '../../runtime/server/index.js';
@@ -7,6 +15,7 @@ import { AstroCookies, attachToResponse } from '../cookies/index.js';
715
import { AstroError, AstroErrorData } from '../errors/index.js';
816
import { warn, type LogOptions } from '../logger/core.js';
917
import { getParamsAndProps, GetParamsAndPropsError } from '../render/core.js';
18+
import { callMiddleware } from '../middleware/index.js';
1019

1120
const clientAddressSymbol = Symbol.for('astro.clientAddress');
1221
const clientLocalsSymbol = Symbol.for('astro.locals');
@@ -75,7 +84,8 @@ export async function call(
7584
mod: EndpointHandler,
7685
env: Environment,
7786
ctx: RenderContext,
78-
logging: LogOptions
87+
logging: LogOptions,
88+
middleware: AstroMiddlewareInstance<Response | EndpointOutput> | undefined
7989
): Promise<EndpointCallResult> {
8090
const paramsAndPropsResp = await getParamsAndProps({
8191
mod: mod as any,
@@ -105,7 +115,15 @@ export async function call(
105115
adapterName: env.adapterName,
106116
});
107117

108-
const response = await renderEndpoint(mod, context, env.ssr);
118+
let response;
119+
if (middleware && middleware.onRequest) {
120+
const onRequest = middleware.onRequest as MiddlewareHandler<Response | EndpointOutput>;
121+
response = await callMiddleware<Response | EndpointOutput>(onRequest, context, () => {
122+
return renderEndpoint(mod, context, env.ssr);
123+
});
124+
} else {
125+
response = await renderEndpoint(mod, context, env.ssr);
126+
}
109127

110128
if (response instanceof Response) {
111129
attachToResponse(response, context.cookies);

0 commit comments

Comments
 (0)