refactor: unify renderPage
and callEndpoint
in one single function (#7703)
* refactor: unify `renderPage` and `callEndpoint` in one single function * chore: lint * don't return the response * chore: update error * rebase
This commit is contained in:
parent
30cdc28057
commit
3043f98723
9 changed files with 325 additions and 299 deletions
|
@ -8,7 +8,6 @@ import type {
|
||||||
} 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 { attachToResponse, getSetCookiesFromResponse } from '../cookies/index.js';
|
||||||
import { callEndpoint } from '../endpoint/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 { prependForwardSlash, removeTrailingForwardSlash } from '../path.js';
|
import { prependForwardSlash, removeTrailingForwardSlash } from '../path.js';
|
||||||
|
@ -16,8 +15,9 @@ import { RedirectSinglePageBuiltModule } from '../redirects/index.js';
|
||||||
import {
|
import {
|
||||||
createEnvironment,
|
createEnvironment,
|
||||||
createRenderContext,
|
createRenderContext,
|
||||||
tryRenderPage,
|
tryRenderRoute,
|
||||||
type Environment,
|
type Environment,
|
||||||
|
type RenderContext,
|
||||||
} from '../render/index.js';
|
} from '../render/index.js';
|
||||||
import { RouteCache } from '../render/route-cache.js';
|
import { RouteCache } from '../render/route-cache.js';
|
||||||
import {
|
import {
|
||||||
|
@ -27,6 +27,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 { isResponse } from '../render/core';
|
||||||
export { deserializeManifest } from './common.js';
|
export { deserializeManifest } from './common.js';
|
||||||
|
|
||||||
const clientLocalsSymbol = Symbol.for('astro.locals');
|
const clientLocalsSymbol = Symbol.for('astro.locals');
|
||||||
|
@ -157,30 +158,90 @@ export class App {
|
||||||
|
|
||||||
let mod = await this.#getModuleForRoute(routeData);
|
let mod = await this.#getModuleForRoute(routeData);
|
||||||
|
|
||||||
if (routeData.type === 'page' || routeData.type === 'redirect') {
|
const pageModule = (await mod.page()) as any;
|
||||||
let response = await this.#renderPage(request, routeData, mod, defaultStatus);
|
const url = new URL(request.url);
|
||||||
|
|
||||||
|
const renderContext = await this.#createRenderContext(
|
||||||
|
url,
|
||||||
|
request,
|
||||||
|
routeData,
|
||||||
|
mod,
|
||||||
|
defaultStatus
|
||||||
|
);
|
||||||
|
let response;
|
||||||
|
try {
|
||||||
|
response = await tryRenderRoute(
|
||||||
|
routeData.type,
|
||||||
|
renderContext,
|
||||||
|
this.#env,
|
||||||
|
pageModule,
|
||||||
|
mod.onRequest
|
||||||
|
);
|
||||||
|
} catch (err: any) {
|
||||||
|
error(this.#logging, 'ssr', err.stack || err.message || String(err));
|
||||||
|
response = new Response(null, {
|
||||||
|
status: 500,
|
||||||
|
statusText: 'Internal server error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isResponse(response, routeData.type)) {
|
||||||
// If there was a known error code, try sending the according page (e.g. 404.astro / 500.astro).
|
// If there was a known error code, try sending the according page (e.g. 404.astro / 500.astro).
|
||||||
if (response.status === 500 || response.status === 404) {
|
if (response.status === 500 || response.status === 404) {
|
||||||
const errorRouteData = matchRoute('/' + response.status, this.#manifestData);
|
const errorRouteData = matchRoute('/' + response.status, this.#manifestData);
|
||||||
if (errorRouteData && errorRouteData.route !== routeData.route) {
|
if (errorRouteData && errorRouteData.route !== routeData.route) {
|
||||||
mod = await this.#getModuleForRoute(errorRouteData);
|
mod = await this.#getModuleForRoute(errorRouteData);
|
||||||
try {
|
try {
|
||||||
let errorResponse = await this.#renderPage(
|
const newRenderContext = await this.#createRenderContext(
|
||||||
|
url,
|
||||||
request,
|
request,
|
||||||
errorRouteData,
|
routeData,
|
||||||
mod,
|
mod,
|
||||||
response.status
|
response.status
|
||||||
);
|
);
|
||||||
return errorResponse;
|
const page = (await mod.page()) as any;
|
||||||
|
const errorResponse = await tryRenderRoute(
|
||||||
|
routeData.type,
|
||||||
|
newRenderContext,
|
||||||
|
this.#env,
|
||||||
|
page
|
||||||
|
);
|
||||||
|
return errorResponse as Response;
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reflect.set(response, responseSentSymbol, true);
|
||||||
return response;
|
return response;
|
||||||
} else if (routeData.type === 'endpoint') {
|
|
||||||
return this.#callEndpoint(request, routeData, mod, defaultStatus);
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unsupported route type [${routeData.type}].`);
|
if (response.type === 'response') {
|
||||||
|
if (response.response.headers.get('X-Astro-Response') === 'Not-Found') {
|
||||||
|
const fourOhFourRequest = new Request(new URL('/404', request.url));
|
||||||
|
const fourOhFourRouteData = this.match(fourOhFourRequest);
|
||||||
|
if (fourOhFourRouteData) {
|
||||||
|
return this.render(fourOhFourRequest, fourOhFourRouteData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response.response;
|
||||||
|
} else {
|
||||||
|
const body = response.body;
|
||||||
|
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 = this.#encoder.encode(body);
|
||||||
|
headers.set('Content-Length', bytes.byteLength.toString());
|
||||||
|
|
||||||
|
const newResponse = new Response(bytes, {
|
||||||
|
status: 200,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
attachToResponse(newResponse, response.cookies);
|
||||||
|
return newResponse;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,6 +249,64 @@ export class App {
|
||||||
return getSetCookiesFromResponse(response);
|
return getSetCookiesFromResponse(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the render context of the current route
|
||||||
|
*/
|
||||||
|
async #createRenderContext(
|
||||||
|
url: URL,
|
||||||
|
request: Request,
|
||||||
|
routeData: RouteData,
|
||||||
|
page: SinglePageBuiltModule,
|
||||||
|
status = 200
|
||||||
|
): Promise<RenderContext> {
|
||||||
|
if (routeData.type === 'endpoint') {
|
||||||
|
const pathname = '/' + this.removeBase(url.pathname);
|
||||||
|
const mod = await page.page();
|
||||||
|
const handler = mod as unknown as EndpointHandler;
|
||||||
|
return await createRenderContext({
|
||||||
|
request,
|
||||||
|
pathname,
|
||||||
|
route: routeData,
|
||||||
|
status,
|
||||||
|
env: this.#env,
|
||||||
|
mod: handler as any,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const pathname = prependForwardSlash(this.removeBase(url.pathname));
|
||||||
|
const info = this.#routeDataToRouteInfo.get(routeData)!;
|
||||||
|
// may be used in the future for handling rel=modulepreload, rel=icon, rel=manifest etc.
|
||||||
|
const links = new Set<never>();
|
||||||
|
const styles = createStylesheetElementSet(info.styles);
|
||||||
|
|
||||||
|
let scripts = new Set<SSRElement>();
|
||||||
|
for (const script of info.scripts) {
|
||||||
|
if ('stage' in script) {
|
||||||
|
if (script.stage === 'head-inline') {
|
||||||
|
scripts.add({
|
||||||
|
props: {},
|
||||||
|
children: script.children,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
scripts.add(createModuleScriptElement(script));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const mod = await page.page();
|
||||||
|
return await createRenderContext({
|
||||||
|
request,
|
||||||
|
pathname,
|
||||||
|
componentMetadata: this.#manifest.componentMetadata,
|
||||||
|
scripts,
|
||||||
|
styles,
|
||||||
|
links,
|
||||||
|
route: routeData,
|
||||||
|
status,
|
||||||
|
mod,
|
||||||
|
env: this.#env,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async #getModuleForRoute(route: RouteData): Promise<SinglePageBuiltModule> {
|
async #getModuleForRoute(route: RouteData): Promise<SinglePageBuiltModule> {
|
||||||
if (route.type === 'redirect') {
|
if (route.type === 'redirect') {
|
||||||
return RedirectSinglePageBuiltModule;
|
return RedirectSinglePageBuiltModule;
|
||||||
|
@ -211,112 +330,4 @@ export class App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async #renderPage(
|
|
||||||
request: Request,
|
|
||||||
routeData: RouteData,
|
|
||||||
page: SinglePageBuiltModule,
|
|
||||||
status = 200
|
|
||||||
): Promise<Response> {
|
|
||||||
const url = new URL(request.url);
|
|
||||||
const pathname = prependForwardSlash(this.removeBase(url.pathname));
|
|
||||||
const info = this.#routeDataToRouteInfo.get(routeData)!;
|
|
||||||
// may be used in the future for handling rel=modulepreload, rel=icon, rel=manifest etc.
|
|
||||||
const links = new Set<never>();
|
|
||||||
const styles = createStylesheetElementSet(info.styles);
|
|
||||||
|
|
||||||
let scripts = new Set<SSRElement>();
|
|
||||||
for (const script of info.scripts) {
|
|
||||||
if ('stage' in script) {
|
|
||||||
if (script.stage === 'head-inline') {
|
|
||||||
scripts.add({
|
|
||||||
props: {},
|
|
||||||
children: script.children,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
scripts.add(createModuleScriptElement(script));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const mod = (await page.page()) as any;
|
|
||||||
const renderContext = await createRenderContext({
|
|
||||||
request,
|
|
||||||
pathname,
|
|
||||||
componentMetadata: this.#manifest.componentMetadata,
|
|
||||||
scripts,
|
|
||||||
styles,
|
|
||||||
links,
|
|
||||||
route: routeData,
|
|
||||||
status,
|
|
||||||
mod,
|
|
||||||
env: this.#env,
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await tryRenderPage(renderContext, this.#env, mod, page.onRequest);
|
|
||||||
|
|
||||||
Reflect.set(request, responseSentSymbol, true);
|
|
||||||
return response;
|
|
||||||
} catch (err: any) {
|
|
||||||
error(this.#logging, 'ssr', err.stack || err.message || String(err));
|
|
||||||
return new Response(null, {
|
|
||||||
status: 500,
|
|
||||||
statusText: 'Internal server error',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async #callEndpoint(
|
|
||||||
request: Request,
|
|
||||||
routeData: RouteData,
|
|
||||||
page: SinglePageBuiltModule,
|
|
||||||
status = 200
|
|
||||||
): Promise<Response> {
|
|
||||||
const url = new URL(request.url);
|
|
||||||
const pathname = '/' + this.removeBase(url.pathname);
|
|
||||||
const mod = await page.page();
|
|
||||||
const handler = mod as unknown as EndpointHandler;
|
|
||||||
|
|
||||||
const ctx = await createRenderContext({
|
|
||||||
request,
|
|
||||||
pathname,
|
|
||||||
route: routeData,
|
|
||||||
status,
|
|
||||||
env: this.#env,
|
|
||||||
mod: handler as any,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await callEndpoint(handler, this.#env, ctx, page.onRequest);
|
|
||||||
|
|
||||||
if (result.type === 'response') {
|
|
||||||
if (result.response.headers.get('X-Astro-Response') === 'Not-Found') {
|
|
||||||
const fourOhFourRequest = new Request(new URL('/404', request.url));
|
|
||||||
const fourOhFourRouteData = this.match(fourOhFourRequest);
|
|
||||||
if (fourOhFourRouteData) {
|
|
||||||
return this.render(fourOhFourRequest, fourOhFourRouteData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result.response;
|
|
||||||
} else {
|
|
||||||
const body = result.body;
|
|
||||||
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 = this.#encoder.encode(body);
|
|
||||||
headers.set('Content-Length', bytes.byteLength.toString());
|
|
||||||
|
|
||||||
const response = new Response(bytes, {
|
|
||||||
status: 200,
|
|
||||||
headers,
|
|
||||||
});
|
|
||||||
|
|
||||||
attachToResponse(response, result.cookies);
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,8 +7,6 @@ import type {
|
||||||
AstroConfig,
|
AstroConfig,
|
||||||
AstroSettings,
|
AstroSettings,
|
||||||
ComponentInstance,
|
ComponentInstance,
|
||||||
EndpointHandler,
|
|
||||||
EndpointOutput,
|
|
||||||
GetStaticPathsItem,
|
GetStaticPathsItem,
|
||||||
ImageTransform,
|
ImageTransform,
|
||||||
MiddlewareHandler,
|
MiddlewareHandler,
|
||||||
|
@ -37,11 +35,10 @@ import {
|
||||||
import { runHookBuildGenerated } from '../../integrations/index.js';
|
import { runHookBuildGenerated } from '../../integrations/index.js';
|
||||||
import { isServerLikeOutput } from '../../prerender/utils.js';
|
import { isServerLikeOutput } from '../../prerender/utils.js';
|
||||||
import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
|
import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
|
||||||
import { callEndpoint } from '../endpoint/index.js';
|
|
||||||
import { AstroError, AstroErrorData } from '../errors/index.js';
|
import { AstroError, AstroErrorData } from '../errors/index.js';
|
||||||
import { debug, info } from '../logger/core.js';
|
import { debug, info } from '../logger/core.js';
|
||||||
import { getRedirectLocationOrThrow, RedirectSinglePageBuiltModule } from '../redirects/index.js';
|
import { getRedirectLocationOrThrow, RedirectSinglePageBuiltModule } from '../redirects/index.js';
|
||||||
import { createEnvironment, createRenderContext, tryRenderPage } from '../render/index.js';
|
import { createEnvironment, createRenderContext, tryRenderRoute } from '../render/index.js';
|
||||||
import { callGetStaticPaths } from '../render/route-cache.js';
|
import { callGetStaticPaths } from '../render/route-cache.js';
|
||||||
import {
|
import {
|
||||||
createAssetLink,
|
createAssetLink,
|
||||||
|
@ -65,6 +62,7 @@ import type {
|
||||||
StylesheetAsset,
|
StylesheetAsset,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { getTimeStat } from './util.js';
|
import { getTimeStat } from './util.js';
|
||||||
|
import { isEndpointResult } from '../render/core.js';
|
||||||
|
|
||||||
function createEntryURL(filePath: string, outFolder: URL) {
|
function createEntryURL(filePath: string, outFolder: URL) {
|
||||||
return new URL('./' + filePath + `?time=${Date.now()}`, outFolder);
|
return new URL('./' + filePath + `?time=${Date.now()}`, outFolder);
|
||||||
|
@ -542,36 +540,28 @@ async function generatePath(
|
||||||
|
|
||||||
let body: string | Uint8Array;
|
let body: string | Uint8Array;
|
||||||
let encoding: BufferEncoding | undefined;
|
let encoding: BufferEncoding | undefined;
|
||||||
if (pageData.route.type === 'endpoint') {
|
|
||||||
const endpointHandler = mod as unknown as EndpointHandler;
|
|
||||||
|
|
||||||
const result = await callEndpoint(
|
let response;
|
||||||
endpointHandler,
|
try {
|
||||||
env,
|
response = await tryRenderRoute(pageData.route.type, renderContext, env, mod, onRequest);
|
||||||
renderContext,
|
} catch (err) {
|
||||||
onRequest as MiddlewareHandler<Response | EndpointOutput>
|
if (!AstroError.is(err) && !(err as SSRError).id && typeof err === 'object') {
|
||||||
);
|
(err as SSRError).id = pageData.component;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
if (result.type === 'response') {
|
if (isEndpointResult(response, pageData.route.type)) {
|
||||||
|
if (response.type === 'response') {
|
||||||
// If there's no body, do nothing
|
// If there's no body, do nothing
|
||||||
if (!result.response.body) return;
|
if (!response.response.body) return;
|
||||||
const ab = await result.response.arrayBuffer();
|
const ab = await response.response.arrayBuffer();
|
||||||
body = new Uint8Array(ab);
|
body = new Uint8Array(ab);
|
||||||
} else {
|
} else {
|
||||||
body = result.body;
|
body = response.body;
|
||||||
encoding = result.encoding;
|
encoding = response.encoding;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let response: Response;
|
|
||||||
try {
|
|
||||||
response = await tryRenderPage(renderContext, env, mod, onRequest);
|
|
||||||
} catch (err) {
|
|
||||||
if (!AstroError.is(err) && !(err as SSRError).id && typeof err === 'object') {
|
|
||||||
(err as SSRError).id = pageData.component;
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.status >= 300 && response.status < 400) {
|
if (response.status >= 300 && response.status < 400) {
|
||||||
// If redirects is set to false, don't output the HTML
|
// If redirects is set to false, don't output the HTML
|
||||||
if (!opts.settings.config.build.redirects) {
|
if (!opts.settings.config.build.redirects) {
|
||||||
|
|
|
@ -17,7 +17,7 @@ import { callMiddleware } from '../middleware/callMiddleware.js';
|
||||||
const clientAddressSymbol = Symbol.for('astro.clientAddress');
|
const clientAddressSymbol = Symbol.for('astro.clientAddress');
|
||||||
const clientLocalsSymbol = Symbol.for('astro.locals');
|
const clientLocalsSymbol = Symbol.for('astro.locals');
|
||||||
|
|
||||||
type EndpointCallResult =
|
export type EndpointCallResult =
|
||||||
| {
|
| {
|
||||||
type: 'simple';
|
type: 'simple';
|
||||||
body: string;
|
body: string;
|
||||||
|
|
|
@ -3,15 +3,17 @@ import type {
|
||||||
ComponentInstance,
|
ComponentInstance,
|
||||||
MiddlewareHandler,
|
MiddlewareHandler,
|
||||||
MiddlewareResponseHandler,
|
MiddlewareResponseHandler,
|
||||||
|
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 { attachToResponse } from '../cookies/index.js';
|
||||||
import { createAPIContext } 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';
|
||||||
import type { RenderContext } from './context.js';
|
import type { RenderContext } from './context.js';
|
||||||
import type { Environment } from './environment.js';
|
import type { Environment } from './environment.js';
|
||||||
import { createResult } from './result.js';
|
import { createResult } from './result.js';
|
||||||
|
import type { EndpointHandler } from '../../@types/astro';
|
||||||
|
|
||||||
export type RenderPage = {
|
export type RenderPage = {
|
||||||
mod: ComponentInstance;
|
mod: ComponentInstance;
|
||||||
|
@ -81,18 +83,22 @@ async function renderPage({ mod, renderContext, env, cookies }: RenderPage) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* It attempts to render a page.
|
* It attempts to render a route. A route can be a:
|
||||||
|
* - page
|
||||||
|
* - redirect
|
||||||
|
* - endpoint
|
||||||
*
|
*
|
||||||
* ## Errors
|
* ## Errors
|
||||||
*
|
*
|
||||||
* It throws an error if the page can't be rendered.
|
* It throws an error if the page can't be rendered.
|
||||||
*/
|
*/
|
||||||
export async function tryRenderPage<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>,
|
||||||
onRequest?: MiddlewareHandler<MiddlewareReturnType>
|
onRequest?: MiddlewareHandler<MiddlewareReturnType>
|
||||||
): Promise<Response> {
|
): Promise<Response | EndpointCallResult> {
|
||||||
const apiContext = createAPIContext({
|
const apiContext = createAPIContext({
|
||||||
request: renderContext.request,
|
request: renderContext.request,
|
||||||
params: renderContext.params,
|
params: renderContext.params,
|
||||||
|
@ -101,26 +107,50 @@ export async function tryRenderPage<MiddlewareReturnType = Response>(
|
||||||
adapterName: env.adapterName,
|
adapterName: env.adapterName,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (onRequest) {
|
switch (routeType) {
|
||||||
return await callMiddleware<Response>(
|
case 'page':
|
||||||
env.logging,
|
case 'redirect': {
|
||||||
onRequest as MiddlewareResponseHandler,
|
if (onRequest) {
|
||||||
apiContext,
|
return await callMiddleware<Response>(
|
||||||
() => {
|
env.logging,
|
||||||
return renderPage({
|
onRequest as MiddlewareResponseHandler,
|
||||||
|
apiContext,
|
||||||
|
() => {
|
||||||
|
return renderPage({
|
||||||
|
mod,
|
||||||
|
renderContext,
|
||||||
|
env,
|
||||||
|
cookies: apiContext.cookies,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return await renderPage({
|
||||||
mod,
|
mod,
|
||||||
renderContext,
|
renderContext,
|
||||||
env,
|
env,
|
||||||
cookies: apiContext.cookies,
|
cookies: apiContext.cookies,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
}
|
||||||
} else {
|
case 'endpoint': {
|
||||||
return await renderPage({
|
const result = await callEndpoint(
|
||||||
mod,
|
mod as any as EndpointHandler,
|
||||||
renderContext,
|
env,
|
||||||
env,
|
renderContext,
|
||||||
cookies: apiContext.cookies,
|
onRequest
|
||||||
});
|
);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error(`Couldn't find route of type [${routeType}]`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isEndpointResult(result: any, routeType: RouteType): result is EndpointCallResult {
|
||||||
|
return !(result instanceof Response) && routeType === 'endpoint';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isResponse(result: any, routeType: RouteType): result is Response {
|
||||||
|
return result instanceof Response && (routeType === 'page' || routeType === 'redirect');
|
||||||
|
}
|
||||||
|
|
|
@ -1,19 +1,9 @@
|
||||||
import type {
|
import type { AstroMiddlewareInstance, ComponentInstance, RouteData } from '../../../@types/astro';
|
||||||
AstroMiddlewareInstance,
|
|
||||||
ComponentInstance,
|
|
||||||
MiddlewareResponseHandler,
|
|
||||||
RouteData,
|
|
||||||
SSRElement,
|
|
||||||
} from '../../../@types/astro';
|
|
||||||
import { PAGE_SCRIPT_ID } from '../../../vite-plugin-scripts/index.js';
|
|
||||||
import { enhanceViteSSRError } from '../../errors/dev/index.js';
|
import { enhanceViteSSRError } from '../../errors/dev/index.js';
|
||||||
import { AggregateError, CSSError, MarkdownError } from '../../errors/index.js';
|
import { AggregateError, CSSError, MarkdownError } from '../../errors/index.js';
|
||||||
import { isPage, resolveIdToUrl, viteID } from '../../util.js';
|
import { viteID } from '../../util.js';
|
||||||
import { createRenderContext, loadRenderers, tryRenderPage } from '../index.js';
|
import { loadRenderers } from '../index.js';
|
||||||
import { getStylesForURL } from './css.js';
|
|
||||||
import type { DevelopmentEnvironment } from './environment';
|
import type { DevelopmentEnvironment } from './environment';
|
||||||
import { getComponentMetadata } from './metadata.js';
|
|
||||||
import { getScriptsForURL } from './scripts.js';
|
|
||||||
export { createDevelopmentEnvironment } from './environment.js';
|
export { createDevelopmentEnvironment } from './environment.js';
|
||||||
export type { DevelopmentEnvironment };
|
export type { DevelopmentEnvironment };
|
||||||
|
|
||||||
|
@ -63,106 +53,3 @@ export async function preload({
|
||||||
throw enhanceViteSSRError({ error, filePath, loader: env.loader });
|
throw enhanceViteSSRError({ error, filePath, loader: env.loader });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GetScriptsAndStylesParams {
|
|
||||||
env: DevelopmentEnvironment;
|
|
||||||
filePath: URL;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getScriptsAndStyles({ env, filePath }: GetScriptsAndStylesParams) {
|
|
||||||
// Add hoisted script tags
|
|
||||||
const scripts = await getScriptsForURL(filePath, env.settings.config.root, env.loader);
|
|
||||||
|
|
||||||
// Inject HMR scripts
|
|
||||||
if (isPage(filePath, env.settings) && env.mode === 'development') {
|
|
||||||
scripts.add({
|
|
||||||
props: { type: 'module', src: '/@vite/client' },
|
|
||||||
children: '',
|
|
||||||
});
|
|
||||||
scripts.add({
|
|
||||||
props: {
|
|
||||||
type: 'module',
|
|
||||||
src: await resolveIdToUrl(env.loader, 'astro/runtime/client/hmr.js'),
|
|
||||||
},
|
|
||||||
children: '',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: We should allow adding generic HTML elements to the head, not just scripts
|
|
||||||
for (const script of env.settings.scripts) {
|
|
||||||
if (script.stage === 'head-inline') {
|
|
||||||
scripts.add({
|
|
||||||
props: {},
|
|
||||||
children: script.content,
|
|
||||||
});
|
|
||||||
} else if (script.stage === 'page' && isPage(filePath, env.settings)) {
|
|
||||||
scripts.add({
|
|
||||||
props: { type: 'module', src: `/@id/${PAGE_SCRIPT_ID}` },
|
|
||||||
children: '',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pass framework CSS in as style tags to be appended to the page.
|
|
||||||
const { urls: styleUrls, stylesMap } = await getStylesForURL(filePath, env.loader, env.mode);
|
|
||||||
let links = new Set<SSRElement>();
|
|
||||||
[...styleUrls].forEach((href) => {
|
|
||||||
links.add({
|
|
||||||
props: {
|
|
||||||
rel: 'stylesheet',
|
|
||||||
href,
|
|
||||||
},
|
|
||||||
children: '',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
let styles = new Set<SSRElement>();
|
|
||||||
[...stylesMap].forEach(([url, content]) => {
|
|
||||||
// Vite handles HMR for styles injected as scripts
|
|
||||||
scripts.add({
|
|
||||||
props: {
|
|
||||||
type: 'module',
|
|
||||||
src: url,
|
|
||||||
},
|
|
||||||
children: '',
|
|
||||||
});
|
|
||||||
// But we still want to inject the styles to avoid FOUC
|
|
||||||
styles.add({
|
|
||||||
props: {
|
|
||||||
type: 'text/css',
|
|
||||||
// Track the ID so we can match it to Vite's injected style later
|
|
||||||
'data-astro-dev-id': viteID(new URL(`.${url}`, env.settings.config.root)),
|
|
||||||
},
|
|
||||||
children: content,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const metadata = await getComponentMetadata(filePath, env.loader);
|
|
||||||
|
|
||||||
return { scripts, styles, links, metadata };
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function renderPage(options: SSROptions): Promise<Response> {
|
|
||||||
const mod = options.preload;
|
|
||||||
|
|
||||||
const { scripts, links, styles, metadata } = await getScriptsAndStyles({
|
|
||||||
env: options.env,
|
|
||||||
filePath: options.filePath,
|
|
||||||
});
|
|
||||||
const { env } = options;
|
|
||||||
|
|
||||||
const renderContext = await createRenderContext({
|
|
||||||
request: options.request,
|
|
||||||
pathname: options.pathname,
|
|
||||||
scripts,
|
|
||||||
links,
|
|
||||||
styles,
|
|
||||||
componentMetadata: metadata,
|
|
||||||
route: options.route,
|
|
||||||
mod,
|
|
||||||
env,
|
|
||||||
});
|
|
||||||
const onRequest = options.middleware?.onRequest as MiddlewareResponseHandler | undefined;
|
|
||||||
|
|
||||||
return tryRenderPage(renderContext, env, mod, onRequest);
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
export { createRenderContext } from './context.js';
|
export { createRenderContext } from './context.js';
|
||||||
export type { RenderContext } from './context.js';
|
export type { RenderContext } from './context.js';
|
||||||
export { tryRenderPage } from './core.js';
|
export { tryRenderRoute } from './core.js';
|
||||||
export type { Environment } from './environment';
|
export type { Environment } from './environment';
|
||||||
export { createEnvironment } from './environment.js';
|
export { createEnvironment } from './environment.js';
|
||||||
export { getParamsAndProps } from './params-and-props.js';
|
export { getParamsAndProps } from './params-and-props.js';
|
||||||
|
|
|
@ -1,20 +1,31 @@
|
||||||
import mime from 'mime';
|
import mime from 'mime';
|
||||||
import type http from 'node:http';
|
import type http from 'node:http';
|
||||||
import type { ComponentInstance, ManifestData, RouteData, SSRManifest } from '../@types/astro';
|
import type {
|
||||||
|
ComponentInstance,
|
||||||
|
ManifestData,
|
||||||
|
MiddlewareResponseHandler,
|
||||||
|
RouteData,
|
||||||
|
SSRElement,
|
||||||
|
SSRManifest,
|
||||||
|
} from '../@types/astro';
|
||||||
import { attachToResponse } from '../core/cookies/index.js';
|
import { attachToResponse } from '../core/cookies/index.js';
|
||||||
import { call as callEndpoint } from '../core/endpoint/dev/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';
|
||||||
import type { DevelopmentEnvironment, SSROptions } from '../core/render/dev/index';
|
import type { DevelopmentEnvironment, SSROptions } from '../core/render/dev/index';
|
||||||
import { preload, renderPage } from '../core/render/dev/index.js';
|
import { preload } from '../core/render/dev/index.js';
|
||||||
import { getParamsAndProps } from '../core/render/index.js';
|
import { createRenderContext, getParamsAndProps, tryRenderRoute } from '../core/render/index.js';
|
||||||
import { createRequest } from '../core/request.js';
|
import { createRequest } from '../core/request.js';
|
||||||
import { matchAllRoutes } from '../core/routing/index.js';
|
import { matchAllRoutes } from '../core/routing/index.js';
|
||||||
import { getSortedPreloadedMatches } from '../prerender/routing.js';
|
import { getSortedPreloadedMatches } from '../prerender/routing.js';
|
||||||
import { isServerLikeOutput } from '../prerender/utils.js';
|
import { isServerLikeOutput } from '../prerender/utils.js';
|
||||||
import { log404 } from './common.js';
|
import { log404 } from './common.js';
|
||||||
import { handle404Response, writeSSRResult, writeWebResponse } from './response.js';
|
import { handle404Response, writeSSRResult, writeWebResponse } from './response.js';
|
||||||
|
import { getScriptsForURL } from '../core/render/dev/scripts.js';
|
||||||
|
import { isPage, resolveIdToUrl, viteID } from '../core/util.js';
|
||||||
|
import { PAGE_SCRIPT_ID } from '../vite-plugin-scripts/index.js';
|
||||||
|
import { getStylesForURL } from '../core/render/dev/css.js';
|
||||||
|
import { getComponentMetadata } from '../core/render/dev/metadata.js';
|
||||||
|
|
||||||
const clientLocalsSymbol = Symbol.for('astro.locals');
|
const clientLocalsSymbol = Symbol.for('astro.locals');
|
||||||
|
|
||||||
|
@ -179,9 +190,28 @@ export async function handleRoute({
|
||||||
if (middleware) {
|
if (middleware) {
|
||||||
options.middleware = middleware;
|
options.middleware = middleware;
|
||||||
}
|
}
|
||||||
// Route successfully matched! Render it.
|
const mod = options.preload;
|
||||||
if (route.type === 'endpoint') {
|
|
||||||
const result = await callEndpoint(options);
|
const { scripts, links, styles, metadata } = await getScriptsAndStyles({
|
||||||
|
env: options.env,
|
||||||
|
filePath: options.filePath,
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderContext = await createRenderContext({
|
||||||
|
request: options.request,
|
||||||
|
pathname: options.pathname,
|
||||||
|
scripts,
|
||||||
|
links,
|
||||||
|
styles,
|
||||||
|
componentMetadata: metadata,
|
||||||
|
route: options.route,
|
||||||
|
mod,
|
||||||
|
env,
|
||||||
|
});
|
||||||
|
const onRequest = options.middleware?.onRequest as MiddlewareResponseHandler | undefined;
|
||||||
|
|
||||||
|
const result = await tryRenderRoute(route.type, renderContext, env, mod, onRequest);
|
||||||
|
if (route.type === 'endpoint' && !(result instanceof Response)) {
|
||||||
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') {
|
||||||
const fourOhFourRoute = await matchRoute('/404', env, manifestData);
|
const fourOhFourRoute = await matchRoute('/404', env, manifestData);
|
||||||
|
@ -219,8 +249,7 @@ export async function handleRoute({
|
||||||
attachToResponse(response, result.cookies);
|
attachToResponse(response, result.cookies);
|
||||||
await writeWebResponse(incomingResponse, response);
|
await writeWebResponse(incomingResponse, response);
|
||||||
}
|
}
|
||||||
} else {
|
} else if (result instanceof Response) {
|
||||||
const result = await renderPage(options);
|
|
||||||
if (result.status === 404) {
|
if (result.status === 404) {
|
||||||
const fourOhFourRoute = await matchRoute('/404', env, manifestData);
|
const fourOhFourRoute = await matchRoute('/404', env, manifestData);
|
||||||
return handleRoute({
|
return handleRoute({
|
||||||
|
@ -245,6 +274,85 @@ export async function handleRoute({
|
||||||
}
|
}
|
||||||
await writeSSRResult(request, response, incomingResponse);
|
await writeSSRResult(request, response, incomingResponse);
|
||||||
}
|
}
|
||||||
|
// unreachable
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetScriptsAndStylesParams {
|
||||||
|
env: DevelopmentEnvironment;
|
||||||
|
filePath: URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getScriptsAndStyles({ env, filePath }: GetScriptsAndStylesParams) {
|
||||||
|
// Add hoisted script tags
|
||||||
|
const scripts = await getScriptsForURL(filePath, env.settings.config.root, env.loader);
|
||||||
|
|
||||||
|
// Inject HMR scripts
|
||||||
|
if (isPage(filePath, env.settings) && env.mode === 'development') {
|
||||||
|
scripts.add({
|
||||||
|
props: { type: 'module', src: '/@vite/client' },
|
||||||
|
children: '',
|
||||||
|
});
|
||||||
|
scripts.add({
|
||||||
|
props: {
|
||||||
|
type: 'module',
|
||||||
|
src: await resolveIdToUrl(env.loader, 'astro/runtime/client/hmr.js'),
|
||||||
|
},
|
||||||
|
children: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: We should allow adding generic HTML elements to the head, not just scripts
|
||||||
|
for (const script of env.settings.scripts) {
|
||||||
|
if (script.stage === 'head-inline') {
|
||||||
|
scripts.add({
|
||||||
|
props: {},
|
||||||
|
children: script.content,
|
||||||
|
});
|
||||||
|
} else if (script.stage === 'page' && isPage(filePath, env.settings)) {
|
||||||
|
scripts.add({
|
||||||
|
props: { type: 'module', src: `/@id/${PAGE_SCRIPT_ID}` },
|
||||||
|
children: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass framework CSS in as style tags to be appended to the page.
|
||||||
|
const { urls: styleUrls, stylesMap } = await getStylesForURL(filePath, env.loader, env.mode);
|
||||||
|
let links = new Set<SSRElement>();
|
||||||
|
[...styleUrls].forEach((href) => {
|
||||||
|
links.add({
|
||||||
|
props: {
|
||||||
|
rel: 'stylesheet',
|
||||||
|
href,
|
||||||
|
},
|
||||||
|
children: '',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let styles = new Set<SSRElement>();
|
||||||
|
[...stylesMap].forEach(([url, content]) => {
|
||||||
|
// Vite handles HMR for styles injected as scripts
|
||||||
|
scripts.add({
|
||||||
|
props: {
|
||||||
|
type: 'module',
|
||||||
|
src: url,
|
||||||
|
},
|
||||||
|
children: '',
|
||||||
|
});
|
||||||
|
// But we still want to inject the styles to avoid FOUC
|
||||||
|
styles.add({
|
||||||
|
props: {
|
||||||
|
type: 'text/css',
|
||||||
|
// Track the ID so we can match it to Vite's injected style later
|
||||||
|
'data-astro-dev-id': viteID(new URL(`.${url}`, env.settings.config.root)),
|
||||||
|
},
|
||||||
|
children: content,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const metadata = await getComponentMetadata(filePath, env.loader);
|
||||||
|
|
||||||
|
return { scripts, styles, links, metadata };
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStatus(matchedRoute?: MatchedRoute): number | undefined {
|
function getStatus(matchedRoute?: MatchedRoute): number | undefined {
|
||||||
|
|
|
@ -9,7 +9,7 @@ import {
|
||||||
renderHead,
|
renderHead,
|
||||||
Fragment,
|
Fragment,
|
||||||
} from '../../../dist/runtime/server/index.js';
|
} from '../../../dist/runtime/server/index.js';
|
||||||
import { createRenderContext, tryRenderPage } from '../../../dist/core/render/index.js';
|
import { createRenderContext, tryRenderRoute } from '../../../dist/core/render/index.js';
|
||||||
import { createBasicEnvironment } from '../test-utils.js';
|
import { createBasicEnvironment } from '../test-utils.js';
|
||||||
import * as cheerio from 'cheerio';
|
import * as cheerio from 'cheerio';
|
||||||
|
|
||||||
|
@ -96,7 +96,7 @@ describe('core/render', () => {
|
||||||
env,
|
env,
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await tryRenderPage(ctx, env, PageModule);
|
const response = await tryRenderRoute('page', ctx, env, PageModule);
|
||||||
|
|
||||||
const html = await response.text();
|
const html = await response.text();
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerio.load(html);
|
||||||
|
@ -176,7 +176,7 @@ describe('core/render', () => {
|
||||||
mod: PageModule,
|
mod: PageModule,
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await tryRenderPage(ctx, env, PageModule);
|
const response = await tryRenderRoute('page', ctx, env, PageModule);
|
||||||
const html = await response.text();
|
const html = await response.text();
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerio.load(html);
|
||||||
|
|
||||||
|
@ -222,7 +222,7 @@ describe('core/render', () => {
|
||||||
mod: PageModule,
|
mod: PageModule,
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await tryRenderPage(ctx, env, PageModule);
|
const response = await tryRenderRoute('page', ctx, env, PageModule);
|
||||||
const html = await response.text();
|
const html = await response.text();
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerio.load(html);
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ import {
|
||||||
import { jsx } from '../../../dist/jsx-runtime/index.js';
|
import { jsx } from '../../../dist/jsx-runtime/index.js';
|
||||||
import {
|
import {
|
||||||
createRenderContext,
|
createRenderContext,
|
||||||
tryRenderPage,
|
tryRenderRoute,
|
||||||
loadRenderer,
|
loadRenderer,
|
||||||
} from '../../../dist/core/render/index.js';
|
} from '../../../dist/core/render/index.js';
|
||||||
import { createAstroJSXComponent, renderer as jsxRenderer } from '../../../dist/jsx/index.js';
|
import { createAstroJSXComponent, renderer as jsxRenderer } from '../../../dist/jsx/index.js';
|
||||||
|
@ -50,7 +50,7 @@ describe('core/render', () => {
|
||||||
mod,
|
mod,
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await tryRenderPage(ctx, env, mod);
|
const response = await tryRenderRoute('page', ctx, env, mod);
|
||||||
|
|
||||||
expect(response.status).to.equal(200);
|
expect(response.status).to.equal(200);
|
||||||
|
|
||||||
|
@ -94,7 +94,7 @@ describe('core/render', () => {
|
||||||
env,
|
env,
|
||||||
mod,
|
mod,
|
||||||
});
|
});
|
||||||
const response = await tryRenderPage(ctx, env, mod);
|
const response = await tryRenderRoute('page', ctx, env, mod);
|
||||||
|
|
||||||
expect(response.status).to.equal(200);
|
expect(response.status).to.equal(200);
|
||||||
|
|
||||||
|
@ -120,7 +120,7 @@ describe('core/render', () => {
|
||||||
mod,
|
mod,
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await tryRenderPage(ctx, env, mod);
|
const response = await tryRenderRoute('page', ctx, env, mod);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await response.text();
|
await response.text();
|
||||||
|
|
Loading…
Reference in a new issue