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:
Emanuele Stoppa 2023-07-20 09:04:53 +01:00 committed by GitHub
parent 30cdc28057
commit 3043f98723
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 325 additions and 299 deletions

View file

@ -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;
}
}
} }

View file

@ -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,29 +540,10 @@ 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,
env,
renderContext,
onRequest as MiddlewareHandler<Response | EndpointOutput>
);
if (result.type === 'response') {
// If there's no body, do nothing
if (!result.response.body) return;
const ab = await result.response.arrayBuffer();
body = new Uint8Array(ab);
} else {
body = result.body;
encoding = result.encoding;
}
} else {
let response: Response;
try { try {
response = await tryRenderPage(renderContext, env, mod, onRequest); response = await tryRenderRoute(pageData.route.type, 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;
@ -572,6 +551,17 @@ async function generatePath(
throw err; throw err;
} }
if (isEndpointResult(response, pageData.route.type)) {
if (response.type === 'response') {
// If there's no body, do nothing
if (!response.response.body) return;
const ab = await response.response.arrayBuffer();
body = new Uint8Array(ab);
} else {
body = response.body;
encoding = response.encoding;
}
} else {
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) {

View file

@ -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;

View file

@ -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,6 +107,9 @@ export async function tryRenderPage<MiddlewareReturnType = Response>(
adapterName: env.adapterName, adapterName: env.adapterName,
}); });
switch (routeType) {
case 'page':
case 'redirect': {
if (onRequest) { if (onRequest) {
return await callMiddleware<Response>( return await callMiddleware<Response>(
env.logging, env.logging,
@ -124,3 +133,24 @@ export async function tryRenderPage<MiddlewareReturnType = Response>(
}); });
} }
} }
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');
}

View file

@ -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);
}

View file

@ -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';

View file

@ -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 {

View file

@ -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);

View file

@ -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();