Skip to content

Commit

Permalink
refactor: add pipeline concept (#8020)
Browse files Browse the repository at this point in the history
  • Loading branch information
ematipico authored Aug 10, 2023
1 parent 924bef9 commit a39ff7e
Show file tree
Hide file tree
Showing 15 changed files with 279 additions and 78 deletions.
13 changes: 13 additions & 0 deletions packages/astro/src/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,16 @@
Code that executes within the top-level Node context. Contains the main Astro logic for the `build` and `dev` commands, and also manages the Vite server and SSR.

[See CONTRIBUTING.md](../../../../CONTRIBUTING.md) for a code overview.

## Pipeline

The pipeline is an internal concept that describes how Astro pages are eventually created and rendered to the user.

Each pipeline has different requirements, criteria and quirks. Although, each pipeline must use the same underline functions, because
the core of the pipeline is the same.

The core of the pipeline is rendering a generic route (page, endpoint or redirect) and returning a `Response`.
When rendering a route, a pipeline must pass a `RenderContext` and `ComponentInstance`. The way these two information are
computed doesn't concern the core of a pipeline. In fact, these types will be computed in different manner based on the type of pipeline.

Each consumer will decide how to handle a `Response`.
70 changes: 21 additions & 49 deletions packages/astro/src/core/app/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import mime from 'mime';
import type {
EndpointHandler,
ManifestData,
MiddlewareEndpointHandler,
RouteData,
SSRElement,
SSRManifest,
} from '../../@types/astro';
import type { SinglePageBuiltModule } from '../build/types';
import { attachToResponse, getSetCookiesFromResponse } from '../cookies/index.js';
import { getSetCookiesFromResponse } from '../cookies/index.js';
import { consoleLogDestination } from '../logger/console.js';
import { error, type LogOptions } from '../logger/core.js';
import {
Expand All @@ -16,12 +16,10 @@ import {
removeTrailingForwardSlash,
} from '../path.js';
import { RedirectSinglePageBuiltModule } from '../redirects/index.js';
import { isResponse } from '../render/core.js';
import {
createEnvironment,
createRenderContext,
tryRenderRoute,
type Environment,
type RenderContext,
} from '../render/index.js';
import { RouteCache } from '../render/route-cache.js';
Expand All @@ -32,6 +30,7 @@ import {
} from '../render/ssr-element.js';
import { matchRoute } from '../routing/match.js';
import type { RouteInfo } from './types';
import { EndpointNotFoundError, SSRRoutePipeline } from './ssrPipeline.js';
export { deserializeManifest } from './common.js';

const clientLocalsSymbol = Symbol.for('astro.locals');
Expand All @@ -53,16 +52,15 @@ export class App {
/**
* The current environment of the application
*/
#env: Environment;
#manifest: SSRManifest;
#manifestData: ManifestData;
#routeDataToRouteInfo: Map<RouteData, RouteInfo>;
#encoder = new TextEncoder();
#logging: LogOptions = {
dest: consoleLogDestination,
level: 'info',
};
#baseWithoutTrailingSlash: string;
#pipeline: SSRRoutePipeline;

constructor(manifest: SSRManifest, streaming = true) {
this.#manifest = manifest;
Expand All @@ -71,7 +69,7 @@ export class App {
};
this.#routeDataToRouteInfo = new Map(manifest.routes.map((route) => [route.routeData, route]));
this.#baseWithoutTrailingSlash = removeTrailingForwardSlash(this.#manifest.base);
this.#env = this.#createEnvironment(streaming);
this.#pipeline = new SSRRoutePipeline(this.#createEnvironment(streaming));
}

set setManifest(newManifest: SSRManifest) {
Expand Down Expand Up @@ -163,19 +161,21 @@ export class App {
);
let response;
try {
response = await tryRenderRoute(
routeData.type,
renderContext,
this.#env,
pageModule,
mod.onRequest
);
// NOTE: ideally we could set the middleware function just once, but we don't have the infrastructure to that yet
if (mod.onRequest) {
this.#pipeline.setMiddlewareFunction(mod.onRequest as MiddlewareEndpointHandler);
}
response = await this.#pipeline.renderRoute(renderContext, pageModule);
} catch (err: any) {
error(this.#logging, 'ssr', err.stack || err.message || String(err));
return this.#renderError(request, { status: 500 });
if (err instanceof EndpointNotFoundError) {
return this.#renderError(request, { status: 404, response: err.originalResponse });
} else {
error(this.#logging, 'ssr', err.stack || err.message || String(err));
return this.#renderError(request, { status: 500 });
}
}

if (isResponse(response, routeData.type)) {
if (SSRRoutePipeline.isResponse(response, routeData.type)) {
if (STATUS_CODES.has(response.status)) {
return this.#renderError(request, {
response,
Expand All @@ -184,35 +184,8 @@ export class App {
}
Reflect.set(response, responseSentSymbol, true);
return response;
} else {
if (response.type === 'response') {
if (response.response.headers.get('X-Astro-Response') === 'Not-Found') {
return this.#renderError(request, {
response: response.response,
status: 404,
});
}
return response.response;
} else {
const headers = new Headers();
const mimeType = mime.getType(url.pathname);
if (mimeType) {
headers.set('Content-Type', `${mimeType};charset=utf-8`);
} else {
headers.set('Content-Type', 'text/plain;charset=utf-8');
}
const bytes =
response.encoding !== 'binary' ? this.#encoder.encode(response.body) : response.body;
headers.set('Content-Length', bytes.byteLength.toString());

const newResponse = new Response(bytes, {
status: 200,
headers,
});
attachToResponse(newResponse, response.cookies);
return newResponse;
}
}
return response;
}

setCookieHeaders(response: Response) {
Expand All @@ -238,7 +211,7 @@ export class App {
pathname,
route: routeData,
status,
env: this.#env,
env: this.#pipeline.env,
mod: handler as any,
});
} else {
Expand Down Expand Up @@ -272,7 +245,7 @@ export class App {
route: routeData,
status,
mod,
env: this.#env,
env: this.#pipeline.env,
});
}
}
Expand Down Expand Up @@ -301,9 +274,8 @@ export class App {
);
const page = (await mod.page()) as any;
const response = (await tryRenderRoute(
'page', // this is hardcoded to ensure proper behavior for missing endpoints
newRenderContext,
this.#env,
this.#pipeline.env,
page
)) as Response;
return this.#mergeResponses(response, originalResponse);
Expand Down
54 changes: 54 additions & 0 deletions packages/astro/src/core/app/ssrPipeline.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type { Environment } from '../render';
import type { EndpointCallResult } from '../endpoint/index.js';
import mime from 'mime';
import { attachCookiesToResponse } from '../cookies/index.js';
import { Pipeline } from '../pipeline.js';

/**
* Thrown when an endpoint contains a response with the header "X-Astro-Response" === 'Not-Found'
*/
export class EndpointNotFoundError extends Error {
originalResponse: Response;
constructor(originalResponse: Response) {
super();
this.originalResponse = originalResponse;
}
}

export class SSRRoutePipeline extends Pipeline {
encoder = new TextEncoder();

constructor(env: Environment) {
super(env);
this.setEndpointHandler(this.#ssrEndpointHandler);
}

// This function is responsible for handling the result coming from an endpoint.
async #ssrEndpointHandler(request: Request, response: EndpointCallResult): Promise<Response> {
if (response.type === 'response') {
if (response.response.headers.get('X-Astro-Response') === 'Not-Found') {
throw new EndpointNotFoundError(response.response);
}
return response.response;
} else {
const url = new URL(request.url);
const headers = new Headers();
const mimeType = mime.getType(url.pathname);
if (mimeType) {
headers.set('Content-Type', `${mimeType};charset=utf-8`);
} else {
headers.set('Content-Type', 'text/plain;charset=utf-8');
}
const bytes =
response.encoding !== 'binary' ? this.encoder.encode(response.body) : response.body;
headers.set('Content-Length', bytes.byteLength.toString());

const newResponse = new Response(bytes, {
status: 200,
headers,
});
attachCookiesToResponse(newResponse, response.cookies);
return newResponse;
}
}
}
2 changes: 1 addition & 1 deletion packages/astro/src/core/build/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -547,7 +547,7 @@ async function generatePath(

let response;
try {
response = await tryRenderRoute(pageData.route.type, renderContext, env, mod, onRequest);
response = await tryRenderRoute(renderContext, env, mod, onRequest);
} catch (err) {
if (!AstroError.is(err) && !(err as SSRError).id && typeof err === 'object') {
(err as SSRError).id = pageData.component;
Expand Down
1 change: 0 additions & 1 deletion packages/astro/src/core/build/plugins/plugin-analyzer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { Node as ESTreeNode } from 'estree-walker';
import type { ModuleInfo, PluginContext } from 'rollup';
import type { Plugin as VitePlugin } from 'vite';
import type { PluginMetadata as AstroPluginMetadata } from '../../../vite-plugin-astro/types';
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/core/cookies/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { AstroCookies } from './cookies.js';
export { attachToResponse, getSetCookiesFromResponse } from './response.js';
export { attachCookiesToResponse, getSetCookiesFromResponse } from './response.js';
2 changes: 1 addition & 1 deletion packages/astro/src/core/cookies/response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { AstroCookies } from './cookies';

const astroCookiesSymbol = Symbol.for('astro.cookies');

export function attachToResponse(response: Response, cookies: AstroCookies) {
export function attachCookiesToResponse(response: Response, cookies: AstroCookies) {
Reflect.set(response, astroCookiesSymbol, cookies);
}

Expand Down
4 changes: 2 additions & 2 deletions packages/astro/src/core/endpoint/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type {
import type { Environment, RenderContext } from '../render/index';
import { renderEndpoint } from '../../runtime/server/index.js';
import { ASTRO_VERSION } from '../constants.js';
import { AstroCookies, attachToResponse } from '../cookies/index.js';
import { AstroCookies, attachCookiesToResponse } from '../cookies/index.js';
import { AstroError, AstroErrorData } from '../errors/index.js';
import { warn } from '../logger/core.js';
import { callMiddleware } from '../middleware/callMiddleware.js';
Expand Down Expand Up @@ -125,7 +125,7 @@ export async function callEndpoint<MiddlewareResult = Response | EndpointOutput>
}

if (response instanceof Response) {
attachToResponse(response, context.cookies);
attachCookiesToResponse(response, context.cookies);
return {
type: 'response',
response,
Expand Down
Loading

0 comments on commit a39ff7e

Please sign in to comment.