Skip to content

Commit

Permalink
per-request pipeline
Browse files Browse the repository at this point in the history
  • Loading branch information
lilnasy committed Jan 31, 2024
1 parent 7763d76 commit 1ddd87d
Show file tree
Hide file tree
Showing 9 changed files with 83 additions and 216 deletions.
29 changes: 13 additions & 16 deletions packages/astro/src/core/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,13 +331,13 @@ export class App {
this.#manifest.trailingSlash,
this.#manifest.buildFormat
);
if (i18nMiddleware) {
this.#environment.setMiddlewareFunction(sequence(i18nMiddleware, this.#manifest.middleware));
this.#environment.onBeforeRenderRoute(i18nPipelineHook);
} else {
this.#environment.setMiddlewareFunction(this.#manifest.middleware);
}
response = await this.#environment.renderRoute(renderContext, pageModule);
const pipeline = this.#environment.createPipeline({
pathname,
renderContext,
hookBefore: i18nPipelineHook,
middleware: sequence(i18nMiddleware, this.#manifest.middleware)
})
response = await pipeline.renderRoute(pageModule);
} catch (err: any) {
this.#logger.error(null, err.stack || err.message || String(err));
return this.#renderError(request, { status: 500 });
Expand Down Expand Up @@ -494,15 +494,12 @@ export class App {
mod,
status
);
const page = (await mod.page()) as any;
if (skipMiddleware === false) {
this.#environment.setMiddlewareFunction(this.#manifest.middleware);
}
if (skipMiddleware) {
// make sure middleware set by other requests is cleared out
this.#environment.unsetMiddlewareFunction();
}
const response = await this.#environment.renderRoute(newRenderContext, page);
const pipeline = this.#environment.createPipeline({
pathname: this.#getPathnameFromRequest(request),
renderContext: newRenderContext,
middleware: skipMiddleware ? (_, next) => next() : undefined
});
const response = await pipeline.renderRoute(await mod.page());
return this.#mergeResponses(response, originalResponse);
} catch {
// Middleware may be the cause of the error, so we try rendering 404/500.astro without it.
Expand Down
27 changes: 13 additions & 14 deletions packages/astro/src/core/build/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,19 +280,6 @@ async function generatePage(
// may be used in the future for handling rel=modulepreload, rel=icon, rel=manifest etc.
const linkIds: [] = [];
const scripts = pageInfo?.hoistedScript ?? null;
// prepare the middleware
const i18nMiddleware = createI18nMiddleware(
manifest.i18n,
manifest.base,
manifest.trailingSlash,
manifest.buildFormat
);
if (config.i18n && i18nMiddleware) {
environment.setMiddlewareFunction(sequence(i18nMiddleware, onRequest));
environment.onBeforeRenderRoute(i18nPipelineHook);
} else {
environment.setMiddlewareFunction(onRequest);
}
if (!pageModulePromise) {
throw new Error(
`Unable to find the module for ${pageData.component}. This is unexpected and likely a bug in Astro, please report.`
Expand Down Expand Up @@ -570,12 +557,24 @@ async function generatePath(
routing: i18n?.routing,
defaultLocale: i18n?.defaultLocale,
});
const i18nMiddleware = createI18nMiddleware(
manifest.i18n,
manifest.base,
manifest.trailingSlash,
manifest.buildFormat
)
const pipeline = environment.createPipeline({
pathname,
renderContext,
hookBefore: i18nPipelineHook,
middleware: sequence(i18nMiddleware, manifest.middleware)
})

let body: string | Uint8Array;

let response: Response;
try {
response = await environment.renderRoute(renderContext, mod);
response = await pipeline.renderRoute(mod);
} catch (err) {
if (!AstroError.is(err) && !(err as SSRError).id && typeof err === 'object') {
(err as SSRError).id = route.component;
Expand Down
71 changes: 21 additions & 50 deletions packages/astro/src/core/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@ import { callMiddleware } from './middleware/callMiddleware.js';
import { renderPage } from './render/core.js';
import { type Environment, type RenderContext } from './render/index.js';

type PipelineHooks = {
before: PipelineHookFunction[];
};

export type PipelineHookFunction = (ctx: RenderContext, mod: ComponentInstance | undefined) => void;

/**
Expand All @@ -16,47 +12,38 @@ export type PipelineHookFunction = (ctx: RenderContext, mod: ComponentInstance |
* Check the {@link ./README.md|README} for more information about the pipeline.
*/
export class Pipeline {
env: Environment;
#onRequest?: MiddlewareHandler;
#hooks: PipelineHooks = {
before: [],
};

/**
* When creating a pipeline, an environment is mandatory.
* The environment won't change for the whole lifetime of the pipeline.
*/
constructor(env: Environment) {
this.env = env;
}

setEnvironment() {}
constructor(
readonly environment: Environment,
readonly locals: App.Locals,
readonly request: Request,
readonly pathname: string,
readonly renderContext: RenderContext,
readonly hookBefore: PipelineHookFunction = () => {},
private middleware = environment.middleware
) {}

/**
* A middleware function that will be called before each request.
*/
setMiddlewareFunction(onRequest: MiddlewareHandler) {
this.#onRequest = onRequest;
setMiddlewareFunction(middleware: MiddlewareHandler) {
this.middleware = middleware;
}

/**
* Removes the current middleware function. Subsequent requests won't trigger any middleware.
*/
unsetMiddlewareFunction() {
this.#onRequest = undefined;
this.middleware = (_, next) => next();
}

/**
* The main function of the pipeline. Use this function to render any route known to Astro;
*/
async renderRoute(
renderContext: RenderContext,
componentInstance: ComponentInstance | undefined
): Promise<Response> {
for (const hook of this.#hooks.before) {
hook(renderContext, componentInstance);
}
return await this.#tryRenderRoute(renderContext, this.env, componentInstance, this.#onRequest);
this.hookBefore(this.renderContext, componentInstance);
return await this.#tryRenderRoute(componentInstance, this.middleware);
}

/**
Expand All @@ -70,21 +57,13 @@ export class Pipeline {
* It throws an error if the page can't be rendered.
*/
async #tryRenderRoute(
renderContext: Readonly<RenderContext>,
env: Environment,
mod: Readonly<ComponentInstance> | undefined,
onRequest?: MiddlewareHandler
): Promise<Response> {
const apiContext = createAPIContext({
request: renderContext.request,
params: renderContext.params,
props: renderContext.props,
site: env.site,
adapterName: env.adapterName,
locales: renderContext.locales,
routingStrategy: renderContext.routing,
defaultLocale: renderContext.defaultLocale,
});
const { renderContext, environment } = this;
const { defaultLocale, locales, params, props, request, routing: routingStrategy } = renderContext;
const { adapterName, site } = environment;
const apiContext = createAPIContext({ adapterName, defaultLocale, locales, params, props, request, routingStrategy, site });

switch (renderContext.route.type) {
case 'page':
Expand All @@ -95,32 +74,24 @@ export class Pipeline {
return renderPage({
mod,
renderContext,
env,
env: environment,
cookies: apiContext.cookies,
});
});
} else {
return await renderPage({
mod,
renderContext,
env,
env: environment,
cookies: apiContext.cookies,
});
}
}
case 'endpoint': {
return await callEndpoint(mod as any as EndpointHandler, env, renderContext, onRequest);
return await callEndpoint(mod as any as EndpointHandler, environment, renderContext, onRequest);
}
default:
throw new Error(`Couldn't find route of type [${renderContext.route.type}]`);
}
}

/**
* Store a function that will be called before starting the rendering phase.
* @param fn
*/
onBeforeRenderRoute(fn: PipelineHookFunction) {
this.#hooks.before.push(fn);
}
}
110 changes: 7 additions & 103 deletions packages/astro/src/core/render/environment.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,15 @@
import type { ComponentInstance, EndpointHandler, MiddlewareHandler, RuntimeMode, SSRLoadedRenderer, SSRManifest } from '../../@types/astro.js';
import { callEndpoint, createAPIContext } from '../endpoint/index.js';
import type { MiddlewareHandler, RuntimeMode, SSRLoadedRenderer, SSRManifest } from '../../@types/astro.js';
import type { Logger } from '../logger/core.js';
import { callMiddleware } from '../middleware/callMiddleware.js';
import type { RenderContext } from './context.js';
import { renderPage } from './core.js';
import type { RouteCache } from './route-cache.js';

type PipelineHooks = {
before: PipelineHookFunction[];
};

export type PipelineHookFunction = (ctx: RenderContext, mod: ComponentInstance | undefined) => void;
import { Pipeline, type PipelineHookFunction } from '../pipeline.js';
import type { RenderContext } from './context.js';

/**
* The environment represents the static parts of rendering that do not change between requests.
* These are mostly known when the server first starts up and do not change.
* Thus, an environment is created once at process start and then used by every pipeline.
*/
export class Environment {
export abstract class Environment {
constructor(
readonly logger: Logger,
readonly manifest: SSRManifest,
Expand All @@ -40,102 +32,14 @@ export class Environment {
readonly clientDirectives = manifest.clientDirectives,
readonly compressHTML = manifest.compressHTML,
readonly i18n = manifest.i18n,
private middleware = manifest.middleware,
readonly middleware = manifest.middleware,
/**
* Used for `Astro.site`.
*/
readonly site = manifest.site,
) {}

#hooks: PipelineHooks = {
before: [],
};

/**
* A middleware function that will be called before each request.
*/
setMiddlewareFunction(onRequest: MiddlewareHandler) {
this.middleware = onRequest;
}

/**
* Removes the current middleware function. Subsequent requests won't trigger any middleware.
*/
unsetMiddlewareFunction() {
this.middleware = (_, next) => next();
}

/**
* Returns the current environment
*/
getEnvironment(): Readonly<Environment> {
return this;
}

/**
* The main function of the pipeline. Use this function to render any route known to Astro;
*/
async renderRoute(
renderContext: RenderContext,
componentInstance: ComponentInstance | undefined
): Promise<Response> {
for (const hook of this.#hooks.before) {
hook(renderContext, componentInstance);
}
return await this.#tryRenderRoute(renderContext, componentInstance);
}

/**
* It attempts to render a route. A route can be a:
* - page
* - redirect
* - endpoint
*
* ## Errors
*
* It throws an error if the page can't be rendered.
*/
async #tryRenderRoute(
renderContext: Readonly<RenderContext>,
mod: Readonly<ComponentInstance> | undefined,
): Promise<Response> {
const apiContext = createAPIContext({
request: renderContext.request,
params: renderContext.params,
props: renderContext.props,
site: this.site,
adapterName: this.adapterName,
locales: renderContext.locales,
routingStrategy: renderContext.routing,
defaultLocale: renderContext.defaultLocale,
});

switch (renderContext.route.type) {
case 'page':
case 'fallback':
case 'redirect': {
return await callMiddleware(this.middleware, apiContext, () => {
return renderPage({
mod,
renderContext,
env: this,
cookies: apiContext.cookies,
});
});
}
case 'endpoint': {
return await callEndpoint(mod as any as EndpointHandler, this, renderContext, this.middleware);
}
default:
throw new Error(`Couldn't find route of type [${renderContext.route.type}]`);
}
}

/**
* Store a function that will be called before starting the rendering phase.
* @param fn
*/
onBeforeRenderRoute(fn: PipelineHookFunction) {
this.#hooks.before.push(fn);
createPipeline({ renderContext, pathname, hookBefore, middleware }: { pathname: string; renderContext: RenderContext; hookBefore?: PipelineHookFunction; middleware?: MiddlewareHandler }) {
return new Pipeline(this, renderContext.locals ?? {}, renderContext.request, pathname, renderContext, hookBefore, middleware);
}
}
1 change: 1 addition & 0 deletions packages/astro/src/vite-plugin-astro-server/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export async function handleRequest({
incomingResponse,
manifest,
}: HandleRequest) {
console.log(new Error)
const { config, loader } = environment;
const origin = `${loader.isHttps() ? 'https' : 'http'}://${incomingRequest.headers.host}`;
const buildingToSSR = isServerLikeOutput(config);
Expand Down
Loading

0 comments on commit 1ddd87d

Please sign in to comment.