refactor: add pipeline concept (#8020)
This commit is contained in:
parent
924bef998e
commit
a39ff7ed6b
15 changed files with 279 additions and 78 deletions
|
@ -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.
|
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.
|
[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`.
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import mime from 'mime';
|
|
||||||
import type {
|
import type {
|
||||||
EndpointHandler,
|
EndpointHandler,
|
||||||
ManifestData,
|
ManifestData,
|
||||||
|
MiddlewareEndpointHandler,
|
||||||
RouteData,
|
RouteData,
|
||||||
SSRElement,
|
SSRElement,
|
||||||
SSRManifest,
|
SSRManifest,
|
||||||
} from '../../@types/astro';
|
} from '../../@types/astro';
|
||||||
import type { SinglePageBuiltModule } from '../build/types';
|
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 { consoleLogDestination } from '../logger/console.js';
|
||||||
import { error, type LogOptions } from '../logger/core.js';
|
import { error, type LogOptions } from '../logger/core.js';
|
||||||
import {
|
import {
|
||||||
|
@ -16,12 +16,10 @@ import {
|
||||||
removeTrailingForwardSlash,
|
removeTrailingForwardSlash,
|
||||||
} from '../path.js';
|
} from '../path.js';
|
||||||
import { RedirectSinglePageBuiltModule } from '../redirects/index.js';
|
import { RedirectSinglePageBuiltModule } from '../redirects/index.js';
|
||||||
import { isResponse } from '../render/core.js';
|
|
||||||
import {
|
import {
|
||||||
createEnvironment,
|
createEnvironment,
|
||||||
createRenderContext,
|
createRenderContext,
|
||||||
tryRenderRoute,
|
tryRenderRoute,
|
||||||
type Environment,
|
|
||||||
type RenderContext,
|
type RenderContext,
|
||||||
} from '../render/index.js';
|
} from '../render/index.js';
|
||||||
import { RouteCache } from '../render/route-cache.js';
|
import { RouteCache } from '../render/route-cache.js';
|
||||||
|
@ -32,6 +30,7 @@ import {
|
||||||
} from '../render/ssr-element.js';
|
} from '../render/ssr-element.js';
|
||||||
import { matchRoute } from '../routing/match.js';
|
import { matchRoute } from '../routing/match.js';
|
||||||
import type { RouteInfo } from './types';
|
import type { RouteInfo } from './types';
|
||||||
|
import { EndpointNotFoundError, SSRRoutePipeline } from './ssrPipeline.js';
|
||||||
export { deserializeManifest } from './common.js';
|
export { deserializeManifest } from './common.js';
|
||||||
|
|
||||||
const clientLocalsSymbol = Symbol.for('astro.locals');
|
const clientLocalsSymbol = Symbol.for('astro.locals');
|
||||||
|
@ -53,16 +52,15 @@ export class App {
|
||||||
/**
|
/**
|
||||||
* The current environment of the application
|
* The current environment of the application
|
||||||
*/
|
*/
|
||||||
#env: Environment;
|
|
||||||
#manifest: SSRManifest;
|
#manifest: SSRManifest;
|
||||||
#manifestData: ManifestData;
|
#manifestData: ManifestData;
|
||||||
#routeDataToRouteInfo: Map<RouteData, RouteInfo>;
|
#routeDataToRouteInfo: Map<RouteData, RouteInfo>;
|
||||||
#encoder = new TextEncoder();
|
|
||||||
#logging: LogOptions = {
|
#logging: LogOptions = {
|
||||||
dest: consoleLogDestination,
|
dest: consoleLogDestination,
|
||||||
level: 'info',
|
level: 'info',
|
||||||
};
|
};
|
||||||
#baseWithoutTrailingSlash: string;
|
#baseWithoutTrailingSlash: string;
|
||||||
|
#pipeline: SSRRoutePipeline;
|
||||||
|
|
||||||
constructor(manifest: SSRManifest, streaming = true) {
|
constructor(manifest: SSRManifest, streaming = true) {
|
||||||
this.#manifest = manifest;
|
this.#manifest = manifest;
|
||||||
|
@ -71,7 +69,7 @@ export class App {
|
||||||
};
|
};
|
||||||
this.#routeDataToRouteInfo = new Map(manifest.routes.map((route) => [route.routeData, route]));
|
this.#routeDataToRouteInfo = new Map(manifest.routes.map((route) => [route.routeData, route]));
|
||||||
this.#baseWithoutTrailingSlash = removeTrailingForwardSlash(this.#manifest.base);
|
this.#baseWithoutTrailingSlash = removeTrailingForwardSlash(this.#manifest.base);
|
||||||
this.#env = this.#createEnvironment(streaming);
|
this.#pipeline = new SSRRoutePipeline(this.#createEnvironment(streaming));
|
||||||
}
|
}
|
||||||
|
|
||||||
set setManifest(newManifest: SSRManifest) {
|
set setManifest(newManifest: SSRManifest) {
|
||||||
|
@ -163,19 +161,21 @@ export class App {
|
||||||
);
|
);
|
||||||
let response;
|
let response;
|
||||||
try {
|
try {
|
||||||
response = await tryRenderRoute(
|
// NOTE: ideally we could set the middleware function just once, but we don't have the infrastructure to that yet
|
||||||
routeData.type,
|
if (mod.onRequest) {
|
||||||
renderContext,
|
this.#pipeline.setMiddlewareFunction(mod.onRequest as MiddlewareEndpointHandler);
|
||||||
this.#env,
|
}
|
||||||
pageModule,
|
response = await this.#pipeline.renderRoute(renderContext, pageModule);
|
||||||
mod.onRequest
|
|
||||||
);
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
error(this.#logging, 'ssr', err.stack || err.message || String(err));
|
if (err instanceof EndpointNotFoundError) {
|
||||||
return this.#renderError(request, { status: 500 });
|
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)) {
|
if (STATUS_CODES.has(response.status)) {
|
||||||
return this.#renderError(request, {
|
return this.#renderError(request, {
|
||||||
response,
|
response,
|
||||||
|
@ -184,35 +184,8 @@ export class App {
|
||||||
}
|
}
|
||||||
Reflect.set(response, responseSentSymbol, true);
|
Reflect.set(response, responseSentSymbol, true);
|
||||||
return response;
|
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) {
|
setCookieHeaders(response: Response) {
|
||||||
|
@ -238,7 +211,7 @@ export class App {
|
||||||
pathname,
|
pathname,
|
||||||
route: routeData,
|
route: routeData,
|
||||||
status,
|
status,
|
||||||
env: this.#env,
|
env: this.#pipeline.env,
|
||||||
mod: handler as any,
|
mod: handler as any,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -272,7 +245,7 @@ export class App {
|
||||||
route: routeData,
|
route: routeData,
|
||||||
status,
|
status,
|
||||||
mod,
|
mod,
|
||||||
env: this.#env,
|
env: this.#pipeline.env,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -301,9 +274,8 @@ export class App {
|
||||||
);
|
);
|
||||||
const page = (await mod.page()) as any;
|
const page = (await mod.page()) as any;
|
||||||
const response = (await tryRenderRoute(
|
const response = (await tryRenderRoute(
|
||||||
'page', // this is hardcoded to ensure proper behavior for missing endpoints
|
|
||||||
newRenderContext,
|
newRenderContext,
|
||||||
this.#env,
|
this.#pipeline.env,
|
||||||
page
|
page
|
||||||
)) as Response;
|
)) as Response;
|
||||||
return this.#mergeResponses(response, originalResponse);
|
return this.#mergeResponses(response, originalResponse);
|
||||||
|
|
54
packages/astro/src/core/app/ssrPipeline.ts
Normal file
54
packages/astro/src/core/app/ssrPipeline.ts
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -547,7 +547,7 @@ async function generatePath(
|
||||||
|
|
||||||
let response;
|
let response;
|
||||||
try {
|
try {
|
||||||
response = await tryRenderRoute(pageData.route.type, renderContext, env, mod, onRequest);
|
response = await tryRenderRoute(renderContext, env, mod, onRequest);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!AstroError.is(err) && !(err as SSRError).id && typeof err === 'object') {
|
if (!AstroError.is(err) && !(err as SSRError).id && typeof err === 'object') {
|
||||||
(err as SSRError).id = pageData.component;
|
(err as SSRError).id = pageData.component;
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import type { Node as ESTreeNode } from 'estree-walker';
|
|
||||||
import type { ModuleInfo, PluginContext } from 'rollup';
|
import type { ModuleInfo, PluginContext } from 'rollup';
|
||||||
import type { Plugin as VitePlugin } from 'vite';
|
import type { Plugin as VitePlugin } from 'vite';
|
||||||
import type { PluginMetadata as AstroPluginMetadata } from '../../../vite-plugin-astro/types';
|
import type { PluginMetadata as AstroPluginMetadata } from '../../../vite-plugin-astro/types';
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
export { AstroCookies } from './cookies.js';
|
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');
|
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);
|
Reflect.set(response, astroCookiesSymbol, cookies);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ import type {
|
||||||
import type { Environment, RenderContext } from '../render/index';
|
import type { Environment, RenderContext } from '../render/index';
|
||||||
import { renderEndpoint } from '../../runtime/server/index.js';
|
import { renderEndpoint } from '../../runtime/server/index.js';
|
||||||
import { ASTRO_VERSION } from '../constants.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 { AstroError, AstroErrorData } from '../errors/index.js';
|
||||||
import { warn } from '../logger/core.js';
|
import { warn } from '../logger/core.js';
|
||||||
import { callMiddleware } from '../middleware/callMiddleware.js';
|
import { callMiddleware } from '../middleware/callMiddleware.js';
|
||||||
|
@ -125,7 +125,7 @@ export async function callEndpoint<MiddlewareResult = Response | EndpointOutput>
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response instanceof Response) {
|
if (response instanceof Response) {
|
||||||
attachToResponse(response, context.cookies);
|
attachCookiesToResponse(response, context.cookies);
|
||||||
return {
|
return {
|
||||||
type: 'response',
|
type: 'response',
|
||||||
response,
|
response,
|
||||||
|
|
156
packages/astro/src/core/pipeline.ts
Normal file
156
packages/astro/src/core/pipeline.ts
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
import { type RenderContext, type Environment } from './render/index.js';
|
||||||
|
import { type EndpointCallResult, callEndpoint, createAPIContext } from './endpoint/index.js';
|
||||||
|
import type {
|
||||||
|
MiddlewareHandler,
|
||||||
|
MiddlewareResponseHandler,
|
||||||
|
ComponentInstance,
|
||||||
|
MiddlewareEndpointHandler,
|
||||||
|
RouteType,
|
||||||
|
EndpointHandler,
|
||||||
|
} from '../@types/astro';
|
||||||
|
import { callMiddleware } from './middleware/callMiddleware.js';
|
||||||
|
import { renderPage } from './render/core.js';
|
||||||
|
|
||||||
|
type EndpointResultHandler = (
|
||||||
|
originalRequest: Request,
|
||||||
|
result: EndpointCallResult
|
||||||
|
) => Promise<Response> | Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the basic class of a pipeline.
|
||||||
|
*
|
||||||
|
* Check the {@link ./README.md|README} for more information about the pipeline.
|
||||||
|
*/
|
||||||
|
export class Pipeline {
|
||||||
|
env: Environment;
|
||||||
|
onRequest?: MiddlewareEndpointHandler;
|
||||||
|
/**
|
||||||
|
* The handler accepts the *original* `Request` and result returned by the endpoint.
|
||||||
|
* It must return a `Response`.
|
||||||
|
*/
|
||||||
|
endpointHandler?: EndpointResultHandler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When rendering a route, an "endpoint" will a type that needs to be handled and transformed into a `Response`.
|
||||||
|
*
|
||||||
|
* Each consumer might have different needs; use this function to set up the handler.
|
||||||
|
*/
|
||||||
|
setEndpointHandler(handler: EndpointResultHandler) {
|
||||||
|
this.endpointHandler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A middleware function that will be called before each request.
|
||||||
|
*/
|
||||||
|
setMiddlewareFunction(onRequest: MiddlewareEndpointHandler) {
|
||||||
|
this.onRequest = onRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The main function of the pipeline. Use this function to render any route known to Astro;
|
||||||
|
*/
|
||||||
|
async renderRoute(
|
||||||
|
renderContext: RenderContext,
|
||||||
|
componentInstance: ComponentInstance
|
||||||
|
): Promise<Response> {
|
||||||
|
const result = await this.#tryRenderRoute(
|
||||||
|
renderContext,
|
||||||
|
this.env,
|
||||||
|
componentInstance,
|
||||||
|
this.onRequest
|
||||||
|
);
|
||||||
|
if (Pipeline.isEndpointResult(result, renderContext.route.type)) {
|
||||||
|
if (!this.endpointHandler) {
|
||||||
|
throw new Error(
|
||||||
|
'You created a pipeline that does not know how to handle the result coming from an endpoint.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.endpointHandler(renderContext.request, result);
|
||||||
|
} else {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<MiddlewareReturnType = Response>(
|
||||||
|
renderContext: Readonly<RenderContext>,
|
||||||
|
env: Readonly<Environment>,
|
||||||
|
mod: Readonly<ComponentInstance>,
|
||||||
|
onRequest?: MiddlewareHandler<MiddlewareReturnType>
|
||||||
|
): Promise<Response | EndpointCallResult> {
|
||||||
|
const apiContext = createAPIContext({
|
||||||
|
request: renderContext.request,
|
||||||
|
params: renderContext.params,
|
||||||
|
props: renderContext.props,
|
||||||
|
site: env.site,
|
||||||
|
adapterName: env.adapterName,
|
||||||
|
});
|
||||||
|
|
||||||
|
switch (renderContext.route.type) {
|
||||||
|
case 'page':
|
||||||
|
case 'redirect': {
|
||||||
|
if (onRequest) {
|
||||||
|
return await callMiddleware<Response>(
|
||||||
|
env.logging,
|
||||||
|
onRequest as MiddlewareResponseHandler,
|
||||||
|
apiContext,
|
||||||
|
() => {
|
||||||
|
return renderPage({
|
||||||
|
mod,
|
||||||
|
renderContext,
|
||||||
|
env,
|
||||||
|
cookies: apiContext.cookies,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return await renderPage({
|
||||||
|
mod,
|
||||||
|
renderContext,
|
||||||
|
env,
|
||||||
|
cookies: apiContext.cookies,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'endpoint': {
|
||||||
|
const result = await callEndpoint(
|
||||||
|
mod as any as EndpointHandler,
|
||||||
|
env,
|
||||||
|
renderContext,
|
||||||
|
onRequest
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error(`Couldn't find route of type [${renderContext.route.type}]`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this function
|
||||||
|
*/
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,7 +22,7 @@ export interface RenderContext {
|
||||||
links?: Set<SSRElement>;
|
links?: Set<SSRElement>;
|
||||||
styles?: Set<SSRElement>;
|
styles?: Set<SSRElement>;
|
||||||
componentMetadata?: SSRResult['componentMetadata'];
|
componentMetadata?: SSRResult['componentMetadata'];
|
||||||
route?: RouteData;
|
route: RouteData;
|
||||||
status?: number;
|
status?: number;
|
||||||
params: Params;
|
params: Params;
|
||||||
props: Props;
|
props: Props;
|
||||||
|
@ -32,6 +32,7 @@ export interface RenderContext {
|
||||||
export type CreateRenderContextArgs = Partial<
|
export type CreateRenderContextArgs = Partial<
|
||||||
Omit<RenderContext, 'params' | 'props' | 'locals'>
|
Omit<RenderContext, 'params' | 'props' | 'locals'>
|
||||||
> & {
|
> & {
|
||||||
|
route: RouteData;
|
||||||
request: RenderContext['request'];
|
request: RenderContext['request'];
|
||||||
mod: ComponentInstance;
|
mod: ComponentInstance;
|
||||||
env: Environment;
|
env: Environment;
|
||||||
|
|
|
@ -7,7 +7,7 @@ import type {
|
||||||
RouteType,
|
RouteType,
|
||||||
} from '../../@types/astro';
|
} from '../../@types/astro';
|
||||||
import { renderPage as runtimeRenderPage } from '../../runtime/server/index.js';
|
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 { callEndpoint, createAPIContext, type EndpointCallResult } from '../endpoint/index.js';
|
||||||
import { callMiddleware } from '../middleware/callMiddleware.js';
|
import { callMiddleware } from '../middleware/callMiddleware.js';
|
||||||
import { redirectRouteGenerate, redirectRouteStatus, routeIsRedirect } from '../redirects/index.js';
|
import { redirectRouteGenerate, redirectRouteStatus, routeIsRedirect } from '../redirects/index.js';
|
||||||
|
@ -22,7 +22,7 @@ export type RenderPage = {
|
||||||
cookies: AstroCookies;
|
cookies: AstroCookies;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function renderPage({ mod, renderContext, env, cookies }: RenderPage) {
|
export async function renderPage({ mod, renderContext, env, cookies }: RenderPage) {
|
||||||
if (routeIsRedirect(renderContext.route)) {
|
if (routeIsRedirect(renderContext.route)) {
|
||||||
return new Response(null, {
|
return new Response(null, {
|
||||||
status: redirectRouteStatus(renderContext.route, renderContext.request.method),
|
status: redirectRouteStatus(renderContext.route, renderContext.request.method),
|
||||||
|
@ -70,7 +70,7 @@ async function renderPage({ mod, renderContext, env, cookies }: RenderPage) {
|
||||||
// If there is an Astro.cookies instance, attach it to the response so that
|
// If there is an Astro.cookies instance, attach it to the response so that
|
||||||
// adapters can grab the Set-Cookie headers.
|
// adapters can grab the Set-Cookie headers.
|
||||||
if (result.cookies) {
|
if (result.cookies) {
|
||||||
attachToResponse(response, result.cookies);
|
attachCookiesToResponse(response, result.cookies);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
|
@ -85,9 +85,9 @@ async function renderPage({ mod, renderContext, env, cookies }: RenderPage) {
|
||||||
* ## Errors
|
* ## Errors
|
||||||
*
|
*
|
||||||
* It throws an error if the page can't be rendered.
|
* It throws an error if the page can't be rendered.
|
||||||
|
* @deprecated Use the pipeline instead
|
||||||
*/
|
*/
|
||||||
export async function tryRenderRoute<MiddlewareReturnType = Response>(
|
export async function tryRenderRoute<MiddlewareReturnType = Response>(
|
||||||
routeType: RouteType,
|
|
||||||
renderContext: Readonly<RenderContext>,
|
renderContext: Readonly<RenderContext>,
|
||||||
env: Readonly<Environment>,
|
env: Readonly<Environment>,
|
||||||
mod: Readonly<ComponentInstance>,
|
mod: Readonly<ComponentInstance>,
|
||||||
|
@ -101,7 +101,7 @@ export async function tryRenderRoute<MiddlewareReturnType = Response>(
|
||||||
adapterName: env.adapterName,
|
adapterName: env.adapterName,
|
||||||
});
|
});
|
||||||
|
|
||||||
switch (routeType) {
|
switch (renderContext.route.type) {
|
||||||
case 'page':
|
case 'page':
|
||||||
case 'redirect': {
|
case 'redirect': {
|
||||||
if (onRequest) {
|
if (onRequest) {
|
||||||
|
@ -137,7 +137,7 @@ export async function tryRenderRoute<MiddlewareReturnType = Response>(
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
default:
|
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: Request;
|
request: Request;
|
||||||
/** optional, in case we need to render something outside of a dev server */
|
/** optional, in case we need to render something outside of a dev server */
|
||||||
route?: RouteData;
|
route: RouteData;
|
||||||
/**
|
/**
|
||||||
* Optional middlewares
|
* Optional middlewares
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -8,7 +8,7 @@ import type {
|
||||||
SSRElement,
|
SSRElement,
|
||||||
SSRManifest,
|
SSRManifest,
|
||||||
} from '../@types/astro';
|
} 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 { AstroErrorData, isAstroError } from '../core/errors/index.js';
|
||||||
import { warn } from '../core/logger/core.js';
|
import { warn } from '../core/logger/core.js';
|
||||||
import { loadMiddleware } from '../core/middleware/loadMiddleware.js';
|
import { loadMiddleware } from '../core/middleware/loadMiddleware.js';
|
||||||
|
@ -49,18 +49,18 @@ export interface MatchedRoute {
|
||||||
mod: ComponentInstance;
|
mod: ComponentInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCustom404Route(manifest: ManifestData): RouteData | undefined {
|
function getCustom404Route(manifestData: ManifestData): RouteData | undefined {
|
||||||
const route404 = /^\/404\/?$/;
|
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(
|
export async function matchRoute(
|
||||||
pathname: string,
|
pathname: string,
|
||||||
env: DevelopmentEnvironment,
|
env: DevelopmentEnvironment,
|
||||||
manifest: ManifestData
|
manifestData: ManifestData
|
||||||
): Promise<MatchedRoute | undefined> {
|
): Promise<MatchedRoute | undefined> {
|
||||||
const { logging, settings, routeCache } = env;
|
const { logging, settings, routeCache } = env;
|
||||||
const matches = matchAllRoutes(pathname, manifest);
|
const matches = matchAllRoutes(pathname, manifestData);
|
||||||
const preloadedMatches = await getSortedPreloadedMatches({ env, matches, settings });
|
const preloadedMatches = await getSortedPreloadedMatches({ env, matches, settings });
|
||||||
|
|
||||||
for await (const { preloadedComponent, route: maybeRoute, filePath } of preloadedMatches) {
|
for await (const { preloadedComponent, route: maybeRoute, filePath } of preloadedMatches) {
|
||||||
|
@ -96,7 +96,7 @@ export async function matchRoute(
|
||||||
// build formats, and is necessary based on how the manifest tracks build targets.
|
// build formats, and is necessary based on how the manifest tracks build targets.
|
||||||
const altPathname = pathname.replace(/(index)?\.html$/, '');
|
const altPathname = pathname.replace(/(index)?\.html$/, '');
|
||||||
if (altPathname !== pathname) {
|
if (altPathname !== pathname) {
|
||||||
return await matchRoute(altPathname, env, manifest);
|
return await matchRoute(altPathname, env, manifestData);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matches.length) {
|
if (matches.length) {
|
||||||
|
@ -112,7 +112,7 @@ export async function matchRoute(
|
||||||
}
|
}
|
||||||
|
|
||||||
log404(logging, pathname);
|
log404(logging, pathname);
|
||||||
const custom404 = getCustom404Route(manifest);
|
const custom404 = getCustom404Route(manifestData);
|
||||||
|
|
||||||
if (custom404) {
|
if (custom404) {
|
||||||
const filePath = new URL(`./${custom404.component}`, settings.config.root);
|
const filePath = new URL(`./${custom404.component}`, settings.config.root);
|
||||||
|
@ -216,7 +216,7 @@ export async function handleRoute({
|
||||||
});
|
});
|
||||||
const onRequest = options.middleware?.onRequest as MiddlewareResponseHandler | undefined;
|
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 (isEndpointResult(result, route.type)) {
|
||||||
if (result.type === 'response') {
|
if (result.type === 'response') {
|
||||||
if (result.response.headers.get('X-Astro-Response') === 'Not-Found') {
|
if (result.response.headers.get('X-Astro-Response') === 'Not-Found') {
|
||||||
|
@ -255,7 +255,7 @@ export async function handleRoute({
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
attachToResponse(response, result.cookies);
|
attachCookiesToResponse(response, result.cookies);
|
||||||
await writeWebResponse(incomingResponse, response);
|
await writeWebResponse(incomingResponse, response);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -90,13 +90,14 @@ describe('core/render', () => {
|
||||||
|
|
||||||
const PageModule = createAstroModule(Page);
|
const PageModule = createAstroModule(Page);
|
||||||
const ctx = await createRenderContext({
|
const ctx = await createRenderContext({
|
||||||
|
route: { type: 'page', pathname: '/index' },
|
||||||
request: new Request('http://example.com/'),
|
request: new Request('http://example.com/'),
|
||||||
links: [{ name: 'link', props: { rel: 'stylesheet', href: '/main.css' }, children: '' }],
|
links: [{ name: 'link', props: { rel: 'stylesheet', href: '/main.css' }, children: '' }],
|
||||||
mod: PageModule,
|
mod: PageModule,
|
||||||
env,
|
env,
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await tryRenderRoute('page', ctx, env, PageModule);
|
const response = await tryRenderRoute(ctx, env, PageModule);
|
||||||
|
|
||||||
const html = await response.text();
|
const html = await response.text();
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerio.load(html);
|
||||||
|
@ -170,13 +171,14 @@ describe('core/render', () => {
|
||||||
|
|
||||||
const PageModule = createAstroModule(Page);
|
const PageModule = createAstroModule(Page);
|
||||||
const ctx = await createRenderContext({
|
const ctx = await createRenderContext({
|
||||||
|
route: { type: 'page', pathname: '/index' },
|
||||||
request: new Request('http://example.com/'),
|
request: new Request('http://example.com/'),
|
||||||
links: [{ name: 'link', props: { rel: 'stylesheet', href: '/main.css' }, children: '' }],
|
links: [{ name: 'link', props: { rel: 'stylesheet', href: '/main.css' }, children: '' }],
|
||||||
env,
|
env,
|
||||||
mod: PageModule,
|
mod: PageModule,
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await tryRenderRoute('page', ctx, env, PageModule);
|
const response = await tryRenderRoute(ctx, env, PageModule);
|
||||||
const html = await response.text();
|
const html = await response.text();
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerio.load(html);
|
||||||
|
|
||||||
|
@ -216,13 +218,14 @@ describe('core/render', () => {
|
||||||
|
|
||||||
const PageModule = createAstroModule(Page);
|
const PageModule = createAstroModule(Page);
|
||||||
const ctx = await createRenderContext({
|
const ctx = await createRenderContext({
|
||||||
|
route: { type: 'page', pathname: '/index' },
|
||||||
request: new Request('http://example.com/'),
|
request: new Request('http://example.com/'),
|
||||||
links: [{ name: 'link', props: { rel: 'stylesheet', href: '/main.css' }, children: '' }],
|
links: [{ name: 'link', props: { rel: 'stylesheet', href: '/main.css' }, children: '' }],
|
||||||
env,
|
env,
|
||||||
mod: PageModule,
|
mod: PageModule,
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await tryRenderRoute('page', ctx, env, PageModule);
|
const response = await tryRenderRoute(ctx, env, PageModule);
|
||||||
const html = await response.text();
|
const html = await response.text();
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerio.load(html);
|
||||||
|
|
||||||
|
|
|
@ -45,12 +45,13 @@ describe('core/render', () => {
|
||||||
|
|
||||||
const mod = createAstroModule(Page);
|
const mod = createAstroModule(Page);
|
||||||
const ctx = await createRenderContext({
|
const ctx = await createRenderContext({
|
||||||
|
route: { type: 'page', pathname: '/index' },
|
||||||
request: new Request('http://example.com/'),
|
request: new Request('http://example.com/'),
|
||||||
env,
|
env,
|
||||||
mod,
|
mod,
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await tryRenderRoute('page', ctx, env, mod);
|
const response = await tryRenderRoute(ctx, env, mod);
|
||||||
|
|
||||||
expect(response.status).to.equal(200);
|
expect(response.status).to.equal(200);
|
||||||
|
|
||||||
|
@ -90,11 +91,12 @@ describe('core/render', () => {
|
||||||
|
|
||||||
const mod = createAstroModule(Page);
|
const mod = createAstroModule(Page);
|
||||||
const ctx = await createRenderContext({
|
const ctx = await createRenderContext({
|
||||||
|
route: { type: 'page', pathname: '/index' },
|
||||||
request: new Request('http://example.com/'),
|
request: new Request('http://example.com/'),
|
||||||
env,
|
env,
|
||||||
mod,
|
mod,
|
||||||
});
|
});
|
||||||
const response = await tryRenderRoute('page', ctx, env, mod);
|
const response = await tryRenderRoute(ctx, env, mod);
|
||||||
|
|
||||||
expect(response.status).to.equal(200);
|
expect(response.status).to.equal(200);
|
||||||
|
|
||||||
|
@ -115,12 +117,13 @@ describe('core/render', () => {
|
||||||
|
|
||||||
const mod = createAstroModule(Page);
|
const mod = createAstroModule(Page);
|
||||||
const ctx = await createRenderContext({
|
const ctx = await createRenderContext({
|
||||||
|
route: { type: 'page', pathname: '/index' },
|
||||||
request: new Request('http://example.com/'),
|
request: new Request('http://example.com/'),
|
||||||
env,
|
env,
|
||||||
mod,
|
mod,
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await tryRenderRoute('page', ctx, env, mod);
|
const response = await tryRenderRoute(ctx, env, mod);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await response.text();
|
await response.text();
|
||||||
|
|
Loading…
Reference in a new issue