Compare commits
1 commit
main
...
poc/pipeli
Author | SHA1 | Date | |
---|---|---|---|
|
b452149595 |
10 changed files with 212 additions and 68 deletions
|
@ -1869,7 +1869,7 @@ export interface SSRLoadedRenderer extends AstroRenderer {
|
|||
|
||||
export type HookParameters<
|
||||
Hook extends keyof AstroIntegration['hooks'],
|
||||
Fn = AstroIntegration['hooks'][Hook]
|
||||
Fn = AstroIntegration['hooks'][Hook],
|
||||
> = Fn extends (...args: any) => any ? Parameters<Fn>[0] : never;
|
||||
|
||||
export interface AstroIntegration {
|
||||
|
|
|
@ -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 {
|
||||
|
@ -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';
|
||||
|
@ -32,6 +30,7 @@ import {
|
|||
} from '../render/ssr-element.js';
|
||||
import { matchRoute } from '../routing/match.js';
|
||||
import type { RouteInfo } from './types';
|
||||
import { SSRRoutePipeline } from '../pipeline';
|
||||
export { deserializeManifest } from './common.js';
|
||||
|
||||
const clientLocalsSymbol = Symbol.for('astro.locals');
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -164,19 +162,16 @@ export class App {
|
|||
);
|
||||
let response;
|
||||
try {
|
||||
response = await tryRenderRoute(
|
||||
routeData.type,
|
||||
renderContext,
|
||||
this.#env,
|
||||
pageModule,
|
||||
mod.onRequest
|
||||
);
|
||||
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 (isResponse(response, routeData.type)) {
|
||||
if (SSRRoutePipeline.isResponse(response, routeData.type)) {
|
||||
if (STATUS_CODES.has(response.status)) {
|
||||
return this.#renderError(request, {
|
||||
response,
|
||||
|
@ -185,35 +180,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) {
|
||||
|
@ -239,7 +207,7 @@ export class App {
|
|||
pathname,
|
||||
route: routeData,
|
||||
status,
|
||||
env: this.#env,
|
||||
env: this.#pipeline.env,
|
||||
mod: handler as any,
|
||||
});
|
||||
} else {
|
||||
|
@ -273,7 +241,7 @@ export class App {
|
|||
route: routeData,
|
||||
status,
|
||||
mod,
|
||||
env: this.#env,
|
||||
env: this.#pipeline.env,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -302,9 +270,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);
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
export { AstroCookies } from './cookies.js';
|
||||
export { attachToResponse, getSetCookiesFromResponse } from './response.js';
|
||||
export { attachCookiesToResponse, getSetCookiesFromResponse } from './response.js';
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ 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';
|
||||
|
@ -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,
|
||||
|
|
176
packages/astro/src/core/pipeline.ts
Normal file
176
packages/astro/src/core/pipeline.ts
Normal file
|
@ -0,0 +1,176 @@
|
|||
import type { Environment } from './render/environment';
|
||||
import {createRenderContext, type RenderContext, tryRenderRoute} from './render';
|
||||
import type { EndpointCallResult } from './endpoint';
|
||||
import type { ComponentInstance, MiddlewareEndpointHandler, RouteType } from '../@types/astro';
|
||||
import { attachCookiesToResponse } from './cookies';
|
||||
import { TextEncoder } from 'util';
|
||||
import mime from 'mime';
|
||||
import type {TransformResult} from "@astrojs/compiler";
|
||||
import {createBasicEnvironment} from "../../test/units/test-utils";
|
||||
|
||||
/**
|
||||
* Questions:
|
||||
* 1. Can we call `getStaticPaths` really early?? Ideally when we load the component. -> idea is to make type the result of
|
||||
* `getStaticPaths`, so we can make serializable and stub it via JS (no need of compiler or make a module).
|
||||
* 2. When rendering a route, what are the info that belong to that route that are not shared with other routes? I guess:
|
||||
* - the Request
|
||||
* - a component instance?
|
||||
* - styles?
|
||||
* - scripts?
|
||||
* - links?
|
||||
* 3. In `RenderContext` we have a route which is a `RouteData`. What's used for? and why it can be optional?
|
||||
*/
|
||||
|
||||
/**
|
||||
* IDEAS:
|
||||
* - what if `handleRequest` dev, instead of directly rendering the page, returns only the info needed to render a route?
|
||||
* It would return only the `RenderContext`, because that's what needed for a route to render.
|
||||
*/
|
||||
|
||||
type EndpointHandler = (
|
||||
originalRequest: Request,
|
||||
result: EndpointCallResult
|
||||
) => Promise<Response> | Response;
|
||||
|
||||
export class Pipeline {
|
||||
env: Environment;
|
||||
onRequest?: MiddlewareEndpointHandler;
|
||||
endpointHandler?: EndpointHandler;
|
||||
|
||||
constructor(env: Environment) {
|
||||
this.env = env;
|
||||
}
|
||||
|
||||
setEndpointHandler(handler: EndpointHandler) {
|
||||
this.endpointHandler = handler;
|
||||
}
|
||||
|
||||
setMiddlewareFunction(onRequest: MiddlewareEndpointHandler) {
|
||||
this.onRequest = onRequest;
|
||||
}
|
||||
|
||||
async renderRoute(
|
||||
renderContext: RenderContext,
|
||||
componentInstance: ComponentInstance
|
||||
): Promise<Response> {
|
||||
const result = await tryRenderRoute(renderContext, this.env, componentInstance, this.onRequest);
|
||||
if (Pipeline.isEndpointResult(result, renderContext.route.type)) {
|
||||
if (!this.endpointHandler) {
|
||||
throw new Error('You must set the endpoint handler');
|
||||
}
|
||||
return this.endpointHandler(renderContext.request, result);
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
static isEndpointResult(result: any, routeType: RouteType): result is EndpointCallResult {
|
||||
return !(result instanceof Response) && routeType === 'endpoint';
|
||||
}
|
||||
|
||||
static isResponse(result: any, routeType: RouteType): result is Response {
|
||||
return result instanceof Response && (routeType === 'page' || routeType === 'redirect');
|
||||
}
|
||||
}
|
||||
|
||||
class DevRoutePipeline extends Pipeline {
|
||||
clearRouteCache() {
|
||||
this.env.routeCache.clearAll();
|
||||
}
|
||||
}
|
||||
|
||||
class BuildRoutePipeline extends Pipeline {
|
||||
|
||||
}
|
||||
|
||||
class TestRoutePipeline extends Pipeline {
|
||||
// NOTE: we can also store JSX renderers is we need?
|
||||
constructor() {
|
||||
super(createBasicEnvironment());
|
||||
}
|
||||
|
||||
async renderAstroPage(contents: string) {
|
||||
const compilationResult = await this.#compile(contents);
|
||||
const renderContext = await this.#computeTestContext(compilationResult);
|
||||
const componentInstance = await this.#computeComponentInstance(compilationResult);
|
||||
const response = await super.renderRoute(renderContext, componentInstance);
|
||||
return response;
|
||||
}
|
||||
|
||||
// TODO: compute `RenderContext` from compilation result, probably
|
||||
async #computeTestContext(result: Readonly<TransformResult>): Promise<RenderContext> {
|
||||
}
|
||||
|
||||
// TODO: compute `ComponentInstance` from compilation result, probably
|
||||
async #computeComponentInstance(result: Readonly<TransformResult>): Promise<ComponentInstance> {
|
||||
|
||||
}
|
||||
|
||||
async #compile(contents: string): Promise<TransformResult> {
|
||||
const compiler = await import("@astrojs/compiler");
|
||||
const result = await compiler.transform(contents);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Example of testing
|
||||
|
||||
|
||||
async function middleware_should_work() {
|
||||
const testPipeline = new TestRoutePipeline();
|
||||
const page = `
|
||||
---
|
||||
const title = Astro.locals.title;
|
||||
---
|
||||
<title>{title}</title>
|
||||
`;
|
||||
testPipeline.setMiddlewareFunction((context, next) => {
|
||||
context.locals = {
|
||||
title: "Test"
|
||||
}
|
||||
return next();
|
||||
})
|
||||
const result = await testPipeline.renderAstroPage(page);
|
||||
const text = await result.text();
|
||||
// assertion text contains "Test"
|
||||
}
|
||||
|
||||
|
||||
export class SSRRoutePipeline extends Pipeline {
|
||||
encoder = new TextEncoder();
|
||||
|
||||
constructor(env: Environment) {
|
||||
super(env);
|
||||
this.setEndpointHandler(this.ssrEndpointHandler);
|
||||
}
|
||||
|
||||
async ssrEndpointHandler(request: Request, response: EndpointCallResult): Promise<Response> {
|
||||
if (response.type === 'response') {
|
||||
if (response.response.headers.get('X-Astro-Response') === 'Not-Found') {
|
||||
// TODO: throw proper astro error to catch in the app/index.ts, and render a 404 instead
|
||||
throw new Error('');
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -22,7 +22,7 @@ export interface RenderContext {
|
|||
links?: Set<SSRElement>;
|
||||
styles?: Set<SSRElement>;
|
||||
componentMetadata?: SSRResult['componentMetadata'];
|
||||
route?: RouteData;
|
||||
route: RouteData;
|
||||
status?: number;
|
||||
params: Params;
|
||||
props: Props;
|
||||
|
@ -32,6 +32,7 @@ export interface RenderContext {
|
|||
export type CreateRenderContextArgs = Partial<
|
||||
Omit<RenderContext, 'params' | 'props' | 'locals'>
|
||||
> & {
|
||||
route: RouteData;
|
||||
request: RenderContext['request'];
|
||||
mod: ComponentInstance;
|
||||
env: Environment;
|
||||
|
|
|
@ -7,7 +7,7 @@ import type {
|
|||
RouteType,
|
||||
} from '../../@types/astro';
|
||||
import { renderPage as runtimeRenderPage } from '../../runtime/server/index.js';
|
||||
import { attachToResponse } from '../cookies/index.js';
|
||||
import { attachCookiesToResponse } from '../cookies/index.js';
|
||||
import { callEndpoint, createAPIContext, type EndpointCallResult } from '../endpoint/index.js';
|
||||
import { callMiddleware } from '../middleware/callMiddleware.js';
|
||||
import { redirectRouteGenerate, redirectRouteStatus, routeIsRedirect } from '../redirects/index.js';
|
||||
|
@ -76,7 +76,7 @@ async function renderPage({ mod, renderContext, env, cookies }: RenderPage) {
|
|||
// If there is an Astro.cookies instance, attach it to the response so that
|
||||
// adapters can grab the Set-Cookie headers.
|
||||
if (result.cookies) {
|
||||
attachToResponse(response, result.cookies);
|
||||
attachCookiesToResponse(response, result.cookies);
|
||||
}
|
||||
|
||||
return response;
|
||||
|
@ -93,7 +93,6 @@ async function renderPage({ mod, renderContext, env, cookies }: RenderPage) {
|
|||
* It throws an error if the page can't be rendered.
|
||||
*/
|
||||
export async function tryRenderRoute<MiddlewareReturnType = Response>(
|
||||
routeType: RouteType,
|
||||
renderContext: Readonly<RenderContext>,
|
||||
env: Readonly<Environment>,
|
||||
mod: Readonly<ComponentInstance>,
|
||||
|
@ -107,7 +106,7 @@ export async function tryRenderRoute<MiddlewareReturnType = Response>(
|
|||
adapterName: env.adapterName,
|
||||
});
|
||||
|
||||
switch (routeType) {
|
||||
switch (renderContext.route.type) {
|
||||
case 'page':
|
||||
case 'redirect': {
|
||||
if (onRequest) {
|
||||
|
@ -143,7 +142,7 @@ export async function tryRenderRoute<MiddlewareReturnType = Response>(
|
|||
return result;
|
||||
}
|
||||
default:
|
||||
throw new Error(`Couldn't find route of type [${routeType}]`);
|
||||
throw new Error(`Couldn't find route of type [${renderContext.route.type}]`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ export interface SSROptions {
|
|||
/** Request */
|
||||
request: Request;
|
||||
/** optional, in case we need to render something outside of a dev server */
|
||||
route?: RouteData;
|
||||
route: RouteData;
|
||||
/**
|
||||
* Optional middlewares
|
||||
*/
|
||||
|
|
|
@ -5,10 +5,11 @@ import type {
|
|||
ManifestData,
|
||||
MiddlewareResponseHandler,
|
||||
RouteData,
|
||||
RouteType,
|
||||
SSRElement,
|
||||
SSRManifest,
|
||||
} from '../@types/astro';
|
||||
import { attachToResponse } from '../core/cookies/index.js';
|
||||
import { attachCookiesToResponse } from '../core/cookies/index.js';
|
||||
import { AstroErrorData, isAstroError } from '../core/errors/index.js';
|
||||
import { warn } from '../core/logger/core.js';
|
||||
import { loadMiddleware } from '../core/middleware/loadMiddleware.js';
|
||||
|
@ -49,18 +50,18 @@ export interface MatchedRoute {
|
|||
mod: ComponentInstance;
|
||||
}
|
||||
|
||||
function getCustom404Route(manifest: ManifestData): RouteData | undefined {
|
||||
function getCustom404Route(manifestData: ManifestData): RouteData | undefined {
|
||||
const route404 = /^\/404\/?$/;
|
||||
return manifest.routes.find((r) => route404.test(r.route));
|
||||
return manifestData.routes.find((r) => route404.test(r.route));
|
||||
}
|
||||
|
||||
export async function matchRoute(
|
||||
pathname: string,
|
||||
env: DevelopmentEnvironment,
|
||||
manifest: ManifestData
|
||||
manifestData: ManifestData
|
||||
): Promise<MatchedRoute | undefined> {
|
||||
const { logging, settings, routeCache } = env;
|
||||
const matches = matchAllRoutes(pathname, manifest);
|
||||
const matches = matchAllRoutes(pathname, manifestData);
|
||||
const preloadedMatches = await getSortedPreloadedMatches({ env, matches, settings });
|
||||
|
||||
for await (const { preloadedComponent, route: maybeRoute, filePath } of preloadedMatches) {
|
||||
|
@ -96,7 +97,7 @@ export async function matchRoute(
|
|||
// build formats, and is necessary based on how the manifest tracks build targets.
|
||||
const altPathname = pathname.replace(/(index)?\.html$/, '');
|
||||
if (altPathname !== pathname) {
|
||||
return await matchRoute(altPathname, env, manifest);
|
||||
return await matchRoute(altPathname, env, manifestData);
|
||||
}
|
||||
|
||||
if (matches.length) {
|
||||
|
@ -112,7 +113,7 @@ export async function matchRoute(
|
|||
}
|
||||
|
||||
log404(logging, pathname);
|
||||
const custom404 = getCustom404Route(manifest);
|
||||
const custom404 = getCustom404Route(manifestData);
|
||||
|
||||
if (custom404) {
|
||||
const filePath = new URL(`./${custom404.component}`, settings.config.root);
|
||||
|
@ -216,7 +217,7 @@ export async function handleRoute({
|
|||
});
|
||||
const onRequest = options.middleware?.onRequest as MiddlewareResponseHandler | undefined;
|
||||
|
||||
const result = await tryRenderRoute(route.type, renderContext, env, mod, onRequest);
|
||||
const result = await tryRenderRoute(renderContext, env, mod, onRequest);
|
||||
if (isEndpointResult(result, route.type)) {
|
||||
if (result.type === 'response') {
|
||||
if (result.response.headers.get('X-Astro-Response') === 'Not-Found') {
|
||||
|
@ -255,7 +256,7 @@ export async function handleRoute({
|
|||
},
|
||||
}
|
||||
);
|
||||
attachToResponse(response, result.cookies);
|
||||
attachCookiesToResponse(response, result.cookies);
|
||||
await writeWebResponse(incomingResponse, response);
|
||||
}
|
||||
} else {
|
||||
|
|
Loading…
Reference in a new issue