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';
|
||||
import type { SinglePageBuiltModule } from '../build/types';
|
||||
import { attachToResponse, getSetCookiesFromResponse } from '../cookies/index.js';
|
||||
import { callEndpoint } from '../endpoint/index.js';
|
||||
import { consoleLogDestination } from '../logger/console.js';
|
||||
import { error, type LogOptions } from '../logger/core.js';
|
||||
import { prependForwardSlash, removeTrailingForwardSlash } from '../path.js';
|
||||
|
@ -16,8 +15,9 @@ import { RedirectSinglePageBuiltModule } from '../redirects/index.js';
|
|||
import {
|
||||
createEnvironment,
|
||||
createRenderContext,
|
||||
tryRenderPage,
|
||||
tryRenderRoute,
|
||||
type Environment,
|
||||
type RenderContext,
|
||||
} from '../render/index.js';
|
||||
import { RouteCache } from '../render/route-cache.js';
|
||||
import {
|
||||
|
@ -27,6 +27,7 @@ import {
|
|||
} from '../render/ssr-element.js';
|
||||
import { matchRoute } from '../routing/match.js';
|
||||
import type { RouteInfo } from './types';
|
||||
import { isResponse } from '../render/core';
|
||||
export { deserializeManifest } from './common.js';
|
||||
|
||||
const clientLocalsSymbol = Symbol.for('astro.locals');
|
||||
|
@ -157,30 +158,90 @@ export class App {
|
|||
|
||||
let mod = await this.#getModuleForRoute(routeData);
|
||||
|
||||
if (routeData.type === 'page' || routeData.type === 'redirect') {
|
||||
let response = await this.#renderPage(request, routeData, mod, defaultStatus);
|
||||
const pageModule = (await mod.page()) as any;
|
||||
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 (response.status === 500 || response.status === 404) {
|
||||
const errorRouteData = matchRoute('/' + response.status, this.#manifestData);
|
||||
if (errorRouteData && errorRouteData.route !== routeData.route) {
|
||||
mod = await this.#getModuleForRoute(errorRouteData);
|
||||
try {
|
||||
let errorResponse = await this.#renderPage(
|
||||
const newRenderContext = await this.#createRenderContext(
|
||||
url,
|
||||
request,
|
||||
errorRouteData,
|
||||
routeData,
|
||||
mod,
|
||||
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 {}
|
||||
}
|
||||
}
|
||||
Reflect.set(response, responseSentSymbol, true);
|
||||
return response;
|
||||
} else if (routeData.type === 'endpoint') {
|
||||
return this.#callEndpoint(request, routeData, mod, defaultStatus);
|
||||
} 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
if (route.type === 'redirect') {
|
||||
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,
|
||||
AstroSettings,
|
||||
ComponentInstance,
|
||||
EndpointHandler,
|
||||
EndpointOutput,
|
||||
GetStaticPathsItem,
|
||||
ImageTransform,
|
||||
MiddlewareHandler,
|
||||
|
@ -37,11 +35,10 @@ import {
|
|||
import { runHookBuildGenerated } from '../../integrations/index.js';
|
||||
import { isServerLikeOutput } from '../../prerender/utils.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 { debug, info } from '../logger/core.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 {
|
||||
createAssetLink,
|
||||
|
@ -65,6 +62,7 @@ import type {
|
|||
StylesheetAsset,
|
||||
} from './types';
|
||||
import { getTimeStat } from './util.js';
|
||||
import { isEndpointResult } from '../render/core.js';
|
||||
|
||||
function createEntryURL(filePath: string, outFolder: URL) {
|
||||
return new URL('./' + filePath + `?time=${Date.now()}`, outFolder);
|
||||
|
@ -542,36 +540,28 @@ async function generatePath(
|
|||
|
||||
let body: string | Uint8Array;
|
||||
let encoding: BufferEncoding | undefined;
|
||||
if (pageData.route.type === 'endpoint') {
|
||||
const endpointHandler = mod as unknown as EndpointHandler;
|
||||
|
||||
const result = await callEndpoint(
|
||||
endpointHandler,
|
||||
env,
|
||||
renderContext,
|
||||
onRequest as MiddlewareHandler<Response | EndpointOutput>
|
||||
);
|
||||
let response;
|
||||
try {
|
||||
response = await tryRenderRoute(pageData.route.type, 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 (result.type === 'response') {
|
||||
if (isEndpointResult(response, pageData.route.type)) {
|
||||
if (response.type === 'response') {
|
||||
// If there's no body, do nothing
|
||||
if (!result.response.body) return;
|
||||
const ab = await result.response.arrayBuffer();
|
||||
if (!response.response.body) return;
|
||||
const ab = await response.response.arrayBuffer();
|
||||
body = new Uint8Array(ab);
|
||||
} else {
|
||||
body = result.body;
|
||||
encoding = result.encoding;
|
||||
body = response.body;
|
||||
encoding = response.encoding;
|
||||
}
|
||||
} 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 redirects is set to false, don't output the HTML
|
||||
if (!opts.settings.config.build.redirects) {
|
||||
|
|
|
@ -17,7 +17,7 @@ import { callMiddleware } from '../middleware/callMiddleware.js';
|
|||
const clientAddressSymbol = Symbol.for('astro.clientAddress');
|
||||
const clientLocalsSymbol = Symbol.for('astro.locals');
|
||||
|
||||
type EndpointCallResult =
|
||||
export type EndpointCallResult =
|
||||
| {
|
||||
type: 'simple';
|
||||
body: string;
|
||||
|
|
|
@ -3,15 +3,17 @@ import type {
|
|||
ComponentInstance,
|
||||
MiddlewareHandler,
|
||||
MiddlewareResponseHandler,
|
||||
RouteType,
|
||||
} from '../../@types/astro';
|
||||
import { renderPage as runtimeRenderPage } from '../../runtime/server/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 { redirectRouteGenerate, redirectRouteStatus, routeIsRedirect } from '../redirects/index.js';
|
||||
import type { RenderContext } from './context.js';
|
||||
import type { Environment } from './environment.js';
|
||||
import { createResult } from './result.js';
|
||||
import type { EndpointHandler } from '../../@types/astro';
|
||||
|
||||
export type RenderPage = {
|
||||
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
|
||||
*
|
||||
* 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>,
|
||||
env: Readonly<Environment>,
|
||||
mod: Readonly<ComponentInstance>,
|
||||
onRequest?: MiddlewareHandler<MiddlewareReturnType>
|
||||
): Promise<Response> {
|
||||
): Promise<Response | EndpointCallResult> {
|
||||
const apiContext = createAPIContext({
|
||||
request: renderContext.request,
|
||||
params: renderContext.params,
|
||||
|
@ -101,26 +107,50 @@ export async function tryRenderPage<MiddlewareReturnType = Response>(
|
|||
adapterName: env.adapterName,
|
||||
});
|
||||
|
||||
if (onRequest) {
|
||||
return await callMiddleware<Response>(
|
||||
env.logging,
|
||||
onRequest as MiddlewareResponseHandler,
|
||||
apiContext,
|
||||
() => {
|
||||
return renderPage({
|
||||
switch (routeType) {
|
||||
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,
|
||||
});
|
||||
}
|
||||
);
|
||||
} 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 [${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 {
|
||||
AstroMiddlewareInstance,
|
||||
ComponentInstance,
|
||||
MiddlewareResponseHandler,
|
||||
RouteData,
|
||||
SSRElement,
|
||||
} from '../../../@types/astro';
|
||||
import { PAGE_SCRIPT_ID } from '../../../vite-plugin-scripts/index.js';
|
||||
import type { AstroMiddlewareInstance, ComponentInstance, RouteData } from '../../../@types/astro';
|
||||
import { enhanceViteSSRError } from '../../errors/dev/index.js';
|
||||
import { AggregateError, CSSError, MarkdownError } from '../../errors/index.js';
|
||||
import { isPage, resolveIdToUrl, viteID } from '../../util.js';
|
||||
import { createRenderContext, loadRenderers, tryRenderPage } from '../index.js';
|
||||
import { getStylesForURL } from './css.js';
|
||||
import { viteID } from '../../util.js';
|
||||
import { loadRenderers } from '../index.js';
|
||||
import type { DevelopmentEnvironment } from './environment';
|
||||
import { getComponentMetadata } from './metadata.js';
|
||||
import { getScriptsForURL } from './scripts.js';
|
||||
export { createDevelopmentEnvironment } from './environment.js';
|
||||
export type { DevelopmentEnvironment };
|
||||
|
||||
|
@ -63,106 +53,3 @@ export async function preload({
|
|||
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 type { RenderContext } from './context.js';
|
||||
export { tryRenderPage } from './core.js';
|
||||
export { tryRenderRoute } from './core.js';
|
||||
export type { Environment } from './environment';
|
||||
export { createEnvironment } from './environment.js';
|
||||
export { getParamsAndProps } from './params-and-props.js';
|
||||
|
|
|
@ -1,20 +1,31 @@
|
|||
import mime from 'mime';
|
||||
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 { call as callEndpoint } from '../core/endpoint/dev/index.js';
|
||||
import { AstroErrorData, isAstroError } from '../core/errors/index.js';
|
||||
import { warn } from '../core/logger/core.js';
|
||||
import { loadMiddleware } from '../core/middleware/loadMiddleware.js';
|
||||
import type { DevelopmentEnvironment, SSROptions } from '../core/render/dev/index';
|
||||
import { preload, renderPage } from '../core/render/dev/index.js';
|
||||
import { getParamsAndProps } from '../core/render/index.js';
|
||||
import { preload } from '../core/render/dev/index.js';
|
||||
import { createRenderContext, getParamsAndProps, tryRenderRoute } from '../core/render/index.js';
|
||||
import { createRequest } from '../core/request.js';
|
||||
import { matchAllRoutes } from '../core/routing/index.js';
|
||||
import { getSortedPreloadedMatches } from '../prerender/routing.js';
|
||||
import { isServerLikeOutput } from '../prerender/utils.js';
|
||||
import { log404 } from './common.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');
|
||||
|
||||
|
@ -179,9 +190,28 @@ export async function handleRoute({
|
|||
if (middleware) {
|
||||
options.middleware = middleware;
|
||||
}
|
||||
// Route successfully matched! Render it.
|
||||
if (route.type === 'endpoint') {
|
||||
const result = await callEndpoint(options);
|
||||
const mod = options.preload;
|
||||
|
||||
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.response.headers.get('X-Astro-Response') === 'Not-Found') {
|
||||
const fourOhFourRoute = await matchRoute('/404', env, manifestData);
|
||||
|
@ -219,8 +249,7 @@ export async function handleRoute({
|
|||
attachToResponse(response, result.cookies);
|
||||
await writeWebResponse(incomingResponse, response);
|
||||
}
|
||||
} else {
|
||||
const result = await renderPage(options);
|
||||
} else if (result instanceof Response) {
|
||||
if (result.status === 404) {
|
||||
const fourOhFourRoute = await matchRoute('/404', env, manifestData);
|
||||
return handleRoute({
|
||||
|
@ -245,6 +274,85 @@ export async function handleRoute({
|
|||
}
|
||||
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 {
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
renderHead,
|
||||
Fragment,
|
||||
} 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 * as cheerio from 'cheerio';
|
||||
|
||||
|
@ -96,7 +96,7 @@ describe('core/render', () => {
|
|||
env,
|
||||
});
|
||||
|
||||
const response = await tryRenderPage(ctx, env, PageModule);
|
||||
const response = await tryRenderRoute('page', ctx, env, PageModule);
|
||||
|
||||
const html = await response.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
@ -176,7 +176,7 @@ describe('core/render', () => {
|
|||
mod: PageModule,
|
||||
});
|
||||
|
||||
const response = await tryRenderPage(ctx, env, PageModule);
|
||||
const response = await tryRenderRoute('page', ctx, env, PageModule);
|
||||
const html = await response.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
|
@ -222,7 +222,7 @@ describe('core/render', () => {
|
|||
mod: PageModule,
|
||||
});
|
||||
|
||||
const response = await tryRenderPage(ctx, env, PageModule);
|
||||
const response = await tryRenderRoute('page', ctx, env, PageModule);
|
||||
const html = await response.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
import { jsx } from '../../../dist/jsx-runtime/index.js';
|
||||
import {
|
||||
createRenderContext,
|
||||
tryRenderPage,
|
||||
tryRenderRoute,
|
||||
loadRenderer,
|
||||
} from '../../../dist/core/render/index.js';
|
||||
import { createAstroJSXComponent, renderer as jsxRenderer } from '../../../dist/jsx/index.js';
|
||||
|
@ -50,7 +50,7 @@ describe('core/render', () => {
|
|||
mod,
|
||||
});
|
||||
|
||||
const response = await tryRenderPage(ctx, env, mod);
|
||||
const response = await tryRenderRoute('page', ctx, env, mod);
|
||||
|
||||
expect(response.status).to.equal(200);
|
||||
|
||||
|
@ -94,7 +94,7 @@ describe('core/render', () => {
|
|||
env,
|
||||
mod,
|
||||
});
|
||||
const response = await tryRenderPage(ctx, env, mod);
|
||||
const response = await tryRenderRoute('page', ctx, env, mod);
|
||||
|
||||
expect(response.status).to.equal(200);
|
||||
|
||||
|
@ -120,7 +120,7 @@ describe('core/render', () => {
|
|||
mod,
|
||||
});
|
||||
|
||||
const response = await tryRenderPage(ctx, env, mod);
|
||||
const response = await tryRenderRoute('page', ctx, env, mod);
|
||||
|
||||
try {
|
||||
await response.text();
|
||||
|
|
Loading…
Reference in a new issue