Refactor rendering for testing (#5071)

* Refactor rendering for testing

* Correctly pass the mod for endpoints
This commit is contained in:
Matthew Phillips 2022-10-13 15:18:28 -04:00 committed by GitHub
parent 4866ff882a
commit df453e420f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 498 additions and 301 deletions

View file

@ -14,7 +14,7 @@ import { call as callEndpoint } from '../endpoint/index.js';
import { consoleLogDestination } from '../logger/console.js';
import { error } from '../logger/core.js';
import { joinPaths, prependForwardSlash } from '../path.js';
import { render } from '../render/core.js';
import { createEnvironment, Environment, createRenderContext, renderPage } from '../render/index.js';
import { RouteCache } from '../render/route-cache.js';
import {
createLinkStylesheetElementSet,
@ -31,16 +31,15 @@ export interface MatchOptions {
}
export class App {
#env: Environment;
#manifest: Manifest;
#manifestData: ManifestData;
#routeDataToRouteInfo: Map<RouteData, RouteInfo>;
#routeCache: RouteCache;
#encoder = new TextEncoder();
#logging: LogOptions = {
dest: consoleLogDestination,
level: 'info',
};
#streaming: boolean;
constructor(manifest: Manifest, streaming = true) {
this.#manifest = manifest;
@ -48,8 +47,32 @@ export class App {
routes: manifest.routes.map((route) => route.routeData),
};
this.#routeDataToRouteInfo = new Map(manifest.routes.map((route) => [route.routeData, route]));
this.#routeCache = new RouteCache(this.#logging);
this.#streaming = streaming;
this.#env = createEnvironment({
adapterName: manifest.adapterName,
logging: this.#logging,
markdown: manifest.markdown,
mode: 'production',
renderers: manifest.renderers,
async resolve(specifier: string) {
if (!(specifier in manifest.entryModules)) {
throw new Error(`Unable to resolve [${specifier}]`);
}
const bundlePath = manifest.entryModules[specifier];
switch (true) {
case bundlePath.startsWith('data:'):
case bundlePath.length === 0: {
return bundlePath;
}
default: {
return prependForwardSlash(joinPaths(manifest.base, bundlePath));
}
}
},
routeCache: new RouteCache(this.#logging),
site: this.#manifest.site,
ssr: true,
streaming,
});
}
match(request: Request, { matchNotFound = false }: MatchOptions = {}): RouteData | undefined {
const url = new URL(request.url);
@ -148,41 +171,17 @@ export class App {
}
try {
const response = await render({
adapterName: manifest.adapterName,
links,
logging: this.#logging,
markdown: manifest.markdown,
mod,
mode: 'production',
const ctx = createRenderContext({
request,
origin: url.origin,
pathname: url.pathname,
scripts,
renderers,
async resolve(specifier: string) {
if (!(specifier in manifest.entryModules)) {
throw new Error(`Unable to resolve [${specifier}]`);
}
const bundlePath = manifest.entryModules[specifier];
switch (true) {
case bundlePath.startsWith('data:'):
case bundlePath.length === 0: {
return bundlePath;
}
default: {
return prependForwardSlash(joinPaths(manifest.base, bundlePath));
}
}
},
links,
route: routeData,
routeCache: this.#routeCache,
site: this.#manifest.site,
ssr: true,
request,
streaming: this.#streaming,
status,
});
const response = await renderPage(mod, ctx, this.#env);
return response;
} catch (err: any) {
error(this.#logging, 'ssr', err.stack || err.message || String(err));
@ -201,17 +200,17 @@ export class App {
): Promise<Response> {
const url = new URL(request.url);
const handler = mod as unknown as EndpointHandler;
const result = await callEndpoint(handler, {
logging: this.#logging,
const ctx = createRenderContext({
request,
origin: url.origin,
pathname: url.pathname,
request,
route: routeData,
routeCache: this.#routeCache,
ssr: true,
status,
});
const result = await callEndpoint(handler, this.#env, ctx);
if (result.type === 'response') {
if (result.response.headers.get('X-Astro-Response') === 'Not-Found') {
const fourOhFourRequest = new Request(new URL('/404', request.url));

View file

@ -19,12 +19,11 @@ import {
removeLeadingForwardSlash,
removeTrailingForwardSlash,
} from '../../core/path.js';
import type { RenderOptions } from '../../core/render/core';
import { runHookBuildGenerated } from '../../integrations/index.js';
import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
import { call as callEndpoint } from '../endpoint/index.js';
import { debug, info } from '../logger/core.js';
import { render } from '../render/core.js';
import { createEnvironment, createRenderContext, renderPage } from '../render/index.js';
import { callGetStaticPaths } from '../render/route-cache.js';
import { createLinkStylesheetElementSet, createModuleScriptsSet } from '../render/ssr-element.js';
import { createRequest } from '../request.js';
@ -360,19 +359,14 @@ async function generatePath(
opts.settings.config.build.format,
pageData.route.type
);
const options: RenderOptions = {
const env = createEnvironment({
adapterName: undefined,
links,
logging,
markdown: {
...settings.config.markdown,
isAstroFlavoredMd: settings.config.legacy.astroFlavoredMarkdown,
},
mod,
mode: opts.mode,
origin,
pathname,
scripts,
renderers,
async resolve(specifier: string) {
const hashedFilePath = internals.entrySpecifierToBundleMap.get(specifier);
@ -386,20 +380,27 @@ async function generatePath(
}
return prependForwardSlash(npath.posix.join(settings.config.base, hashedFilePath));
},
request: createRequest({ url, headers: new Headers(), logging, ssr }),
route: pageData.route,
routeCache,
site: settings.config.site
? new URL(settings.config.base, settings.config.site).toString()
: settings.config.site,
ssr,
streaming: true,
};
});
const ctx = createRenderContext({
origin,
pathname,
request: createRequest({ url, headers: new Headers(), logging, ssr }),
scripts,
links,
route: pageData.route,
});
let body: string;
let encoding: BufferEncoding | undefined;
if (pageData.route.type === 'endpoint') {
const result = await callEndpoint(mod as unknown as EndpointHandler, options);
const endpointHandler = mod as unknown as EndpointHandler;
const result = await callEndpoint(endpointHandler, env, ctx);
if (result.type === 'response') {
throw new Error(`Returning a Response from an endpoint is not supported in SSG mode.`);
@ -407,7 +408,7 @@ async function generatePath(
body = result.body;
encoding = result.encoding;
} else {
const response = await render(options);
const response = await renderPage(mod, ctx, env);
// If there's a redirect or something, just do nothing.
if (response.status !== 200 || !response.body) {

View file

@ -1,14 +1,18 @@
import type { EndpointHandler } from '../../../@types/astro';
import type { SSROptions } from '../../render/dev';
import { preload } from '../../render/dev/index.js';
import { createRenderContext } from '../../render/index.js';
import { call as callEndpoint } from '../index.js';
export async function call(ssrOpts: SSROptions) {
const [, mod] = await preload(ssrOpts);
return await callEndpoint(mod as unknown as EndpointHandler, {
...ssrOpts,
ssr: ssrOpts.settings.config.output === 'server',
site: ssrOpts.settings.config.site,
adapterName: ssrOpts.settings.config.adapter?.name,
export async function call(options: SSROptions) {
const { env, preload: [,mod] } = options;
const endpointHandler = mod as unknown as EndpointHandler;
const ctx = createRenderContext({
request: options.request,
origin: options.origin,
pathname: options.pathname,
route: options.route
});
return await callEndpoint(endpointHandler, env, ctx);
}

View file

@ -1,5 +1,5 @@
import type { APIContext, EndpointHandler, Params } from '../../@types/astro';
import type { RenderOptions } from '../render/core';
import type { Environment, RenderContext } from '../render/index';
import { renderEndpoint } from '../../runtime/server/index.js';
import { ASTRO_VERSION } from '../constants.js';
@ -8,21 +8,6 @@ import { getParamsAndProps, GetParamsAndPropsError } from '../render/core.js';
const clientAddressSymbol = Symbol.for('astro.clientAddress');
export type EndpointOptions = Pick<
RenderOptions,
| 'logging'
| 'origin'
| 'request'
| 'route'
| 'routeCache'
| 'pathname'
| 'route'
| 'site'
| 'ssr'
| 'status'
| 'adapterName'
>;
type EndpointCallResult =
| {
type: 'simple';
@ -83,25 +68,34 @@ function createAPIContext({
export async function call(
mod: EndpointHandler,
opts: EndpointOptions
env: Environment,
ctx: RenderContext
): Promise<EndpointCallResult> {
const paramsAndPropsResp = await getParamsAndProps({ ...opts, mod: mod as any });
const paramsAndPropsResp = await getParamsAndProps({
mod: mod as any,
route: ctx.route,
routeCache: env.routeCache,
pathname: ctx.pathname,
logging: env.logging,
ssr: env.ssr
});
if (paramsAndPropsResp === GetParamsAndPropsError.NoMatchingStaticPath) {
throw new Error(
`[getStaticPath] route pattern matched, but no matching static path found. (${opts.pathname})`
`[getStaticPath] route pattern matched, but no matching static path found. (${ctx.pathname})`
);
}
const [params, props] = paramsAndPropsResp;
const context = createAPIContext({
request: opts.request,
request: ctx.request,
params,
props,
site: opts.site,
adapterName: opts.adapterName,
site: env.site,
adapterName: env.adapterName,
});
const response = await renderEndpoint(mod, context, opts.ssr);
const response = await renderEndpoint(mod, context, env.ssr);
if (response instanceof Response) {
attachToResponse(response, context.cookies);

View file

@ -0,0 +1,45 @@
import type { MarkdownRenderingOptions } from '@astrojs/markdown-remark';
import type {
ComponentInstance,
Params,
Props,
RouteData,
RuntimeMode,
SSRElement,
SSRLoadedRenderer,
} from '../../@types/astro';
import type { LogOptions } from '../logger/core.js';
import type { Environment } from './environment.js';
/**
* The RenderContext represents the parts of rendering that are specific to one request.
*/
export interface RenderContext {
request: Request;
origin: string;
pathname: string;
url: URL;
scripts?: Set<SSRElement>;
links?: Set<SSRElement>;
styles?: Set<SSRElement>;
route?: RouteData;
status?: number;
}
export type CreateRenderContextArgs = Partial<RenderContext> & {
origin?: string;
request: RenderContext['request'];
}
export function createRenderContext(options: CreateRenderContextArgs): RenderContext {
const request = options.request;
const url = new URL(request.url);
const origin = options.origin ?? url.origin;
const pathname = options.pathname ?? url.pathname;
return {
...options,
origin,
pathname,
url
};
}

View file

@ -1,16 +1,14 @@
import type { MarkdownRenderingOptions } from '@astrojs/markdown-remark';
import type {
ComponentInstance,
Params,
Props,
RouteData,
RuntimeMode,
SSRElement,
SSRLoadedRenderer,
} from '../../@types/astro';
import type { LogOptions } from '../logger/core.js';
import type { Environment } from './environment.js';
import type { RenderContext } from './context.js';
import { Fragment, renderPage } from '../../runtime/server/index.js';
import { Fragment, renderPage as runtimeRenderPage } from '../../runtime/server/index.js';
import { attachToResponse } from '../cookies/index.js';
import { getParams } from '../routing/params.js';
import { createResult } from './result.js';
@ -67,90 +65,46 @@ export async function getParamsAndProps(
return [params, pageProps];
}
export interface RenderOptions {
adapterName?: string;
logging: LogOptions;
links: Set<SSRElement>;
styles?: Set<SSRElement>;
markdown: MarkdownRenderingOptions;
mod: ComponentInstance;
mode: RuntimeMode;
origin: string;
pathname: string;
scripts: Set<SSRElement>;
resolve: (s: string) => Promise<string>;
renderers: SSRLoadedRenderer[];
route?: RouteData;
routeCache: RouteCache;
site?: string;
ssr: boolean;
streaming: boolean;
request: Request;
status?: number;
}
export async function render(opts: RenderOptions): Promise<Response> {
const {
adapterName,
links,
styles,
logging,
origin,
markdown,
mod,
mode,
pathname,
scripts,
renderers,
request,
resolve,
route,
routeCache,
site,
ssr,
streaming,
status = 200,
} = opts;
export async function renderPage(mod: ComponentInstance, ctx: RenderContext, env: Environment) {
const paramsAndPropsRes = await getParamsAndProps({
logging,
logging: env.logging,
mod,
route,
routeCache,
pathname,
ssr,
route: ctx.route,
routeCache: env.routeCache,
pathname: ctx.pathname,
ssr: env.ssr,
});
if (paramsAndPropsRes === GetParamsAndPropsError.NoMatchingStaticPath) {
throw new Error(
`[getStaticPath] route pattern matched, but no matching static path found. (${pathname})`
`[getStaticPath] route pattern matched, but no matching static path found. (${ctx.pathname})`
);
}
const [params, pageProps] = paramsAndPropsRes;
// Validate the page component before rendering the page
const Component = await mod.default;
const Component = mod.default;
if (!Component)
throw new Error(`Expected an exported Astro component but received typeof ${typeof Component}`);
const result = createResult({
adapterName,
links,
styles,
logging,
markdown,
mode,
origin,
adapterName: env.adapterName,
links: ctx.links,
styles: ctx.styles,
logging: env.logging,
markdown: env.markdown,
mode: env.mode,
origin: ctx.origin,
params,
props: pageProps,
pathname,
resolve,
renderers,
request,
site,
scripts,
ssr,
status,
pathname: ctx.pathname,
resolve: env.resolve,
renderers: env.renderers,
request: ctx.request,
site: env.site,
scripts: ctx.scripts,
ssr: env.ssr,
status: ctx.status ?? 200,
});
// Support `export const components` for `MDX` pages
@ -165,7 +119,7 @@ export async function render(opts: RenderOptions): Promise<Response> {
});
}
const response = await renderPage(result, Component, pageProps, null, streaming);
const response = await runtimeRenderPage(result, Component, pageProps, null, env.streaming);
// If there is an Astro.cookies instance, attach it to the response so that
// adapters can grab the Set-Cookie headers.

View file

@ -0,0 +1,47 @@
import type { MarkdownRenderingOptions } from '@astrojs/markdown-remark';
import type { ViteDevServer } from 'vite';
import type {
AstroSettings,
RuntimeMode,
SSRLoadedRenderer,
} from '../../../@types/astro';
import type { Environment } from '../index';
import type { LogOptions } from '../../logger/core.js';
import { RouteCache } from '../route-cache.js';
import { createEnvironment } from '../index.js';
import { createResolve } from './resolve.js';
export type DevelopmentEnvironment = Environment & {
settings: AstroSettings;
viteServer: ViteDevServer;
}
export function createDevelopmentEnvironment(
settings: AstroSettings,
logging: LogOptions,
viteServer: ViteDevServer
): DevelopmentEnvironment {
const mode: RuntimeMode = 'development';
let env = createEnvironment({
adapterName: settings.adapter?.name,
logging,
markdown: {
...settings.config.markdown,
isAstroFlavoredMd: settings.config.legacy.astroFlavoredMarkdown,
},
mode,
// This will be overridden in the dev server
renderers: [],
resolve: createResolve(viteServer),
routeCache: new RouteCache(logging, mode),
site: settings.config.site,
ssr: settings.config.output === 'server',
streaming: true,
});
return {
...env,
viteServer,
settings
};
}

View file

@ -1,7 +1,6 @@
import { fileURLToPath } from 'url';
import type { ViteDevServer } from 'vite';
import type {
AstroRenderer,
AstroSettings,
ComponentInstance,
RouteData,
@ -9,16 +8,20 @@ import type {
SSRElement,
SSRLoadedRenderer,
} from '../../../@types/astro';
import type { DevelopmentEnvironment } from './environment';
import { PAGE_SCRIPT_ID } from '../../../vite-plugin-scripts/index.js';
import { LogOptions } from '../../logger/core.js';
import { isPage, resolveIdToUrl } from '../../util.js';
import { render as coreRender } from '../core.js';
import { renderPage as coreRenderPage, createRenderContext } from '../index.js';
import { RouteCache } from '../route-cache.js';
import { collectMdMetadata } from '../util.js';
import { getStylesForURL } from './css.js';
import { getScriptsForURL } from './scripts.js';
import { loadRenderer, filterFoundRenderers } from '../renderer.js';
export { createDevelopmentEnvironment } from './environment.js';
export type { DevelopmentEnvironment };
export interface SSROptions {
export interface SSROptionsOld {
/** an instance of the AstroSettings */
settings: AstroSettings;
/** location of file on disk */
@ -41,72 +44,81 @@ export interface SSROptions {
request: Request;
}
export type ComponentPreload = [SSRLoadedRenderer[], ComponentInstance];
/*
filePath: options.filePath
});
const svelteStylesRE = /svelte\?svelte&type=style/;
const ctx = createRenderContext({
request: options.request,
origin: options.origin,
pathname: options.pathname,
scripts,
links,
styles,
route: options.route
*/
export interface SSROptions {
/** The environment instance */
env: DevelopmentEnvironment;
/** location of file on disk */
filePath: URL;
/** production website */
origin: string;
/** the web request (needed for dynamic routes) */
pathname: string;
/** The renderers and instance */
preload: ComponentPreload;
/** Request */
request: Request;
/** optional, in case we need to render something outside of a dev server */
route?: RouteData;
async function loadRenderer(
viteServer: ViteDevServer,
renderer: AstroRenderer
): Promise<SSRLoadedRenderer> {
const mod = (await viteServer.ssrLoadModule(renderer.serverEntrypoint)) as {
default: SSRLoadedRenderer['ssr'];
};
return { ...renderer, ssr: mod.default };
}
export type ComponentPreload = [SSRLoadedRenderer[], ComponentInstance];
export async function loadRenderers(
viteServer: ViteDevServer,
settings: AstroSettings
): Promise<SSRLoadedRenderer[]> {
return Promise.all(settings.renderers.map((r) => loadRenderer(viteServer, r)));
const loader = (entry: string) => viteServer.ssrLoadModule(entry);
const renderers = await Promise.all(settings.renderers.map(r => loadRenderer(r, loader)));
return filterFoundRenderers(renderers);
}
export async function preload({
settings,
env,
filePath,
viteServer,
}: Pick<SSROptions, 'settings' | 'filePath' | 'viteServer'>): Promise<ComponentPreload> {
}: Pick<SSROptions, 'env' | 'filePath'>): Promise<ComponentPreload> {
// Important: This needs to happen first, in case a renderer provides polyfills.
const renderers = await loadRenderers(viteServer, settings);
const renderers = await loadRenderers(env.viteServer, env.settings);
// Load the module from the Vite SSR Runtime.
const mod = (await viteServer.ssrLoadModule(fileURLToPath(filePath))) as ComponentInstance;
if (viteServer.config.mode === 'development' || !mod?.$$metadata) {
const mod = (await env.viteServer.ssrLoadModule(fileURLToPath(filePath))) as ComponentInstance;
if (env.viteServer.config.mode === 'development' || !mod?.$$metadata) {
return [renderers, mod];
}
// append all nested markdown metadata to mod.$$metadata
const modGraph = await viteServer.moduleGraph.getModuleByUrl(fileURLToPath(filePath));
const modGraph = await env.viteServer.moduleGraph.getModuleByUrl(fileURLToPath(filePath));
if (modGraph) {
await collectMdMetadata(mod.$$metadata, modGraph, viteServer);
await collectMdMetadata(mod.$$metadata, modGraph, env.viteServer);
}
return [renderers, mod];
}
/** use Vite to SSR */
export async function render(
renderers: SSRLoadedRenderer[],
mod: ComponentInstance,
ssrOpts: SSROptions
): Promise<Response> {
const {
settings,
filePath,
logging,
mode,
origin,
pathname,
request,
route,
routeCache,
viteServer,
} = ssrOpts;
interface GetScriptsAndStylesParams {
env: DevelopmentEnvironment;
filePath: URL;
}
async function getScriptsAndStyles({ env, filePath }: GetScriptsAndStylesParams) {
// Add hoisted script tags
const scripts = await getScriptsForURL(filePath, viteServer);
const scripts = await getScriptsForURL(filePath, env.viteServer);
// Inject HMR scripts
if (isPage(filePath, settings) && mode === 'development') {
if (isPage(filePath, env.settings) && env.mode === 'development') {
scripts.add({
props: { type: 'module', src: '/@vite/client' },
children: '',
@ -114,20 +126,20 @@ export async function render(
scripts.add({
props: {
type: 'module',
src: await resolveIdToUrl(viteServer, 'astro/runtime/client/hmr.js'),
src: await resolveIdToUrl(env.viteServer, 'astro/runtime/client/hmr.js'),
},
children: '',
});
}
// TODO: We should allow adding generic HTML elements to the head, not just scripts
for (const script of settings.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, settings)) {
} else if (script.stage === 'page' && isPage(filePath, env.settings)) {
scripts.add({
props: { type: 'module', src: `/@id/${PAGE_SCRIPT_ID}` },
children: '',
@ -136,7 +148,7 @@ export async function render(
}
// Pass framework CSS in as style tags to be appended to the page.
const { urls: styleUrls, stylesMap } = await getStylesForURL(filePath, viteServer, mode);
const { urls: styleUrls, stylesMap } = await getStylesForURL(filePath, env.viteServer, env.mode);
let links = new Set<SSRElement>();
[...styleUrls].forEach((href) => {
links.add({
@ -164,54 +176,31 @@ export async function render(
children: content,
});
});
return { scripts, styles, links };
}
let response = await coreRender({
adapterName: settings.config.adapter?.name,
links,
styles,
logging,
markdown: {
...settings.config.markdown,
isAstroFlavoredMd: settings.config.legacy.astroFlavoredMarkdown,
},
mod,
mode,
origin,
pathname,
scripts,
// Resolves specifiers in the inline hydrated scripts, such as:
// - @astrojs/preact/client.js
// - @/components/Foo.vue
// - /Users/macos/project/src/Foo.vue
// - C:/Windows/project/src/Foo.vue (normalized slash)
async resolve(s: string) {
const url = await resolveIdToUrl(viteServer, s);
// Vite does not resolve .jsx -> .tsx when coming from hydration script import,
// clip it so Vite is able to resolve implicitly.
if (url.startsWith('/@fs') && url.endsWith('.jsx')) {
return url.slice(0, -4);
} else {
return url;
}
},
renderers,
request,
route,
routeCache,
site: settings.config.site
? new URL(settings.config.base, settings.config.site).toString()
: undefined,
ssr: settings.config.output === 'server',
streaming: true,
export async function renderPage(options: SSROptions): Promise<Response> {
const [renderers, mod] = options.preload;
// Override the environment's renderers. This ensures that if renderers change (HMR)
// The new instances are passed through.
options.env.renderers = renderers;
const { scripts, links, styles } = await getScriptsAndStyles({
env: options.env,
filePath: options.filePath
});
return response;
}
const ctx = createRenderContext({
request: options.request,
origin: options.origin,
pathname: options.pathname,
scripts,
links,
styles,
route: options.route
});
export async function ssr(
preloadedComponent: ComponentPreload,
ssrOpts: SSROptions
): Promise<Response> {
const [renderers, mod] = preloadedComponent;
return await render(renderers, mod, ssrOpts); // NOTE: without "await", errors wont get caught below
return await coreRenderPage(mod, ctx, options.env); // NOTE: without "await", errors wont get caught below
}

View file

@ -0,0 +1,20 @@
import type { ViteDevServer } from 'vite';
import { isPage, resolveIdToUrl } from '../../util.js';
export function createResolve(viteServer: ViteDevServer) {
// Resolves specifiers in the inline hydrated scripts, such as:
// - @astrojs/preact/client.js
// - @/components/Foo.vue
// - /Users/macos/project/src/Foo.vue
// - C:/Windows/project/src/Foo.vue (normalized slash)
return async function(s: string) {
const url = await resolveIdToUrl(viteServer, s);
// Vite does not resolve .jsx -> .tsx when coming from hydration script import,
// clip it so Vite is able to resolve implicitly.
if (url.startsWith('/@fs') && url.endsWith('.jsx')) {
return url.slice(0, -4);
} else {
return url;
}
};
}

View file

@ -0,0 +1,52 @@
import type { MarkdownRenderingOptions } from '@astrojs/markdown-remark';
import type {
RuntimeMode,
SSRLoadedRenderer,
} from '../../@types/astro';
import type { LogOptions } from '../logger/core.js';
import { RouteCache } from './route-cache.js';
/**
* An environment represents the static parts of rendering that do not change
* between requests. These are mostly known when the server first starts up and do not change.
* Thus they can be created once and passed through to renderPage on each request.
*/
export interface Environment {
adapterName?: string;
/** logging options */
logging: LogOptions;
markdown: MarkdownRenderingOptions;
/** "development" or "production" */
mode: RuntimeMode;
renderers: SSRLoadedRenderer[];
resolve: (s: string) => Promise<string>;
routeCache: RouteCache;
site?: string;
ssr: boolean;
streaming: boolean;
}
export type CreateEnvironmentArgs = Environment;
export function createEnvironment(options: CreateEnvironmentArgs): Environment {
return options;
}
export type CreateBasicEnvironmentArgs = Partial<Environment> & {
logging: CreateEnvironmentArgs['logging'];
}
export function createBasicEnvironment(options: CreateBasicEnvironmentArgs): Environment {
const mode = options.mode ?? 'development';
return createEnvironment({
...options,
markdown: options.markdown ?? {},
mode,
renderers: options.renderers ?? [],
resolve: options.resolve ?? ((s: string) => Promise.resolve(s)),
routeCache: new RouteCache(options.logging, mode),
ssr: options.ssr ?? true,
streaming: options.streaming ?? true
});
}

View file

@ -0,0 +1,22 @@
export type {
Environment
} from './environment';
export type {
RenderContext
} from './context';
export {
createBasicEnvironment,
createEnvironment
} from './environment.js';
export {
createRenderContext
} from './context.js';
export {
getParamsAndProps,
GetParamsAndPropsError,
renderPage,
} from './core.js';
export {
loadRenderer
} from './renderer.js';

View file

@ -0,0 +1,28 @@
import type { AstroRenderer, SSRLoadedRenderer } from '../../@types/astro';
export type RendererServerEntrypointModule = {
default: SSRLoadedRenderer['ssr'];
};
export type MaybeRendererServerEntrypointModule = Partial<RendererServerEntrypointModule>;
export type RendererLoader = (entryPoint: string) => Promise<MaybeRendererServerEntrypointModule>;
export async function loadRenderer(renderer: AstroRenderer, loader: RendererLoader): Promise<SSRLoadedRenderer | undefined> {
const mod = await loader(renderer.serverEntrypoint);
if(typeof mod.default !== 'undefined') {
return createLoadedRenderer(renderer, mod as RendererServerEntrypointModule);
}
return undefined;
}
export function filterFoundRenderers(renderers: Array<SSRLoadedRenderer | undefined>): SSRLoadedRenderer[] {
return renderers.filter((renderer): renderer is SSRLoadedRenderer => {
return !!renderer;
});
}
export function createLoadedRenderer(renderer: AstroRenderer, mod: RendererServerEntrypointModule): SSRLoadedRenderer {
return {
...renderer,
ssr: mod.default
};
}

View file

@ -129,7 +129,7 @@ class Slots {
let renderMarkdown: any = null;
export function createResult(args: CreateResultArgs): SSRResult {
const { markdown, params, pathname, props: pageProps, renderers, request, resolve } = args;
const { markdown, params, pathname, renderers, request, resolve } = args;
const url = new URL(request.url);
const headers = new Headers();

View file

@ -0,0 +1,9 @@
import renderer from './renderer.js';
import { __astro_tag_component__ } from '../runtime/server/index.js';
const ASTRO_JSX_RENDERER_NAME = renderer.name;
export function createAstroJSXComponent(factory: (...args: any[]) => any) {
__astro_tag_component__(factory, ASTRO_JSX_RENDERER_NAME);
return factory;
}

View file

@ -0,0 +1,6 @@
export {
default as renderer
} from './renderer.js';
export {
createAstroJSXComponent
} from './component.js';

View file

@ -21,6 +21,7 @@ export {
stringifyChunk,
voidElementNames,
} from './render/index.js';
export { renderJSX } from './jsx.js';
export type { AstroComponentFactory, RenderInstruction } from './render/index.js';
import type { AstroComponentFactory } from './render/index.js';

View file

@ -2,7 +2,7 @@ import type http from 'http';
import mime from 'mime';
import type * as vite from 'vite';
import type { AstroSettings, ManifestData } from '../@types/astro';
import type { SSROptions } from '../core/render/dev/index';
import { DevelopmentEnvironment, SSROptions } from '../core/render/dev/index';
import { Readable } from 'stream';
import { getSetCookiesFromResponse } from '../core/cookies/index.js';
@ -16,9 +16,8 @@ import {
import { error, info, LogOptions, warn } from '../core/logger/core.js';
import * as msg from '../core/messages.js';
import { appendForwardSlash } from '../core/path.js';
import { getParamsAndProps, GetParamsAndPropsError } from '../core/render/core.js';
import { preload, ssr } from '../core/render/dev/index.js';
import { RouteCache } from '../core/render/route-cache.js';
import { getParamsAndProps, GetParamsAndPropsError } from '../core/render/index.js';
import { createDevelopmentEnvironment, preload, renderPage } from '../core/render/dev/index.js';
import { createRequest } from '../core/request.js';
import { createRouteManifest, matchAllRoutes } from '../core/routing/index.js';
import { resolvePages } from '../core/util.js';
@ -100,7 +99,6 @@ async function writeSSRResult(webResponse: Response, res: http.ServerResponse) {
async function handle404Response(
origin: string,
settings: AstroSettings,
req: http.IncomingMessage,
res: http.ServerResponse
) {
@ -187,17 +185,15 @@ export function baseMiddleware(
async function matchRoute(
pathname: string,
routeCache: RouteCache,
viteServer: vite.ViteDevServer,
logging: LogOptions,
env: DevelopmentEnvironment,
manifest: ManifestData,
settings: AstroSettings
) {
const { logging, settings, routeCache } = env;
const matches = matchAllRoutes(pathname, manifest);
for await (const maybeRoute of matches) {
const filePath = new URL(`./${maybeRoute.component}`, settings.config.root);
const preloadedComponent = await preload({ settings, filePath, viteServer });
const preloadedComponent = await preload({ env, filePath });
const [, mod] = preloadedComponent;
// attempt to get static paths
// if this fails, we have a bad URL match!
@ -233,7 +229,7 @@ async function matchRoute(
if (custom404) {
const filePath = new URL(`./${custom404.component}`, settings.config.root);
const preloadedComponent = await preload({ settings, filePath, viteServer });
const preloadedComponent = await preload({ env, filePath });
const [, mod] = preloadedComponent;
return {
@ -249,14 +245,12 @@ async function matchRoute(
/** The main logic to route dev server requests to pages in Astro. */
async function handleRequest(
routeCache: RouteCache,
viteServer: vite.ViteDevServer,
logging: LogOptions,
env: DevelopmentEnvironment,
manifest: ManifestData,
settings: AstroSettings,
req: http.IncomingMessage,
res: http.ServerResponse
) {
const { settings, viteServer } = env;
const { config } = settings;
const origin = `${viteServer.config.server.https ? 'https' : 'http'}://${req.headers.host}`;
const buildingToSSR = config.output === 'server';
@ -296,11 +290,8 @@ async function handleRequest(
try {
const matchedRoute = await matchRoute(
pathname,
routeCache,
viteServer,
logging,
env,
manifest,
settings
);
filePath = matchedRoute?.filePath;
@ -310,18 +301,15 @@ async function handleRequest(
pathname,
body,
origin,
routeCache,
viteServer,
env,
manifest,
logging,
settings,
req,
res
);
} catch (_err) {
const err = fixViteErrorMessage(_err, viteServer, filePath);
const errorWithMetadata = collectErrorMetadata(err);
error(logging, null, msg.formatErrorMessage(errorWithMetadata));
error(env.logging, null, msg.formatErrorMessage(errorWithMetadata));
handle500Response(viteServer, origin, req, res, errorWithMetadata);
}
}
@ -332,16 +320,14 @@ async function handleRoute(
pathname: string,
body: ArrayBuffer | undefined,
origin: string,
routeCache: RouteCache,
viteServer: vite.ViteDevServer,
env: DevelopmentEnvironment,
manifest: ManifestData,
logging: LogOptions,
settings: AstroSettings,
req: http.IncomingMessage,
res: http.ServerResponse
): Promise<void> {
const { logging, settings } = env;
if (!matchedRoute) {
return handle404Response(origin, settings, req, res);
return handle404Response(origin, req, res);
}
const { config } = settings;
@ -365,23 +351,20 @@ async function handleRoute(
const paramsAndPropsRes = await getParamsAndProps({
mod,
route,
routeCache,
routeCache: env.routeCache,
pathname: pathname,
logging,
ssr: config.output === 'server',
});
const options: SSROptions = {
settings,
filePath,
logging,
mode: 'development',
origin,
pathname: pathname,
route,
routeCache,
viteServer,
request,
env,
filePath,
origin,
preload: preloadedComponent,
pathname,
request,
route
};
// Route successfully matched! Render it.
@ -391,11 +374,8 @@ async function handleRoute(
if (result.response.headers.get('X-Astro-Response') === 'Not-Found') {
const fourOhFourRoute = await matchRoute(
'/404',
routeCache,
viteServer,
logging,
manifest,
settings
env,
manifest
);
return handleRoute(
fourOhFourRoute,
@ -403,11 +383,8 @@ async function handleRoute(
'/404',
body,
origin,
routeCache,
viteServer,
env,
manifest,
logging,
settings,
req,
res
);
@ -427,7 +404,7 @@ async function handleRoute(
res.end(result.body);
}
} else {
const result = await ssr(preloadedComponent, options);
const result = await renderPage(options);
return await writeSSRResult(result, res);
}
}
@ -436,11 +413,12 @@ export default function createPlugin({ settings, logging }: AstroPluginOptions):
return {
name: 'astro:server',
configureServer(viteServer) {
let routeCache = new RouteCache(logging, 'development');
let env = createDevelopmentEnvironment(settings, logging, viteServer);
let manifest: ManifestData = createRouteManifest({ settings }, logging);
/** rebuild the route cache + manifest, as needed. */
function rebuildManifest(needsManifestRebuild: boolean, file: string) {
routeCache.clearAll();
env.routeCache.clearAll();
if (needsManifestRebuild) {
manifest = createRouteManifest({ settings }, logging);
}
@ -461,7 +439,7 @@ export default function createPlugin({ settings, logging }: AstroPluginOptions):
if (!req.url || !req.method) {
throw new Error('Incomplete request');
}
handleRequest(routeCache, viteServer, logging, manifest, settings, req, res);
handleRequest(env, manifest, req, res);
});
};
},

View file

@ -0,0 +1,48 @@
import { expect } from 'chai';
import { createComponent, render, renderSlot } from '../../../dist/runtime/server/index.js';
import { jsx } from '../../../dist/jsx-runtime/index.js';
import { createBasicEnvironment, createRenderContext, renderPage, loadRenderer } from '../../../dist/core/render/index.js';
import { createAstroJSXComponent, renderer as jsxRenderer } from '../../../dist/jsx/index.js';
import { defaultLogging as logging } from '../../test-utils.js';
const createAstroModule = AstroComponent => ({ default: AstroComponent });
const loadJSXRenderer = () => loadRenderer(jsxRenderer, s => import(s));
describe('core/render', () => {
describe('Astro JSX components', () => {
let env;
before(async () => {
env = createBasicEnvironment({
logging,
renderers: [await loadJSXRenderer()]
});
})
it('Can render slots', async () => {
const Wrapper = createComponent((result, _props, slots = {}) => {
return render`<div>${renderSlot(result, slots['myslot'])}</div>`;
});
const Page = createAstroJSXComponent(() => {
return jsx(Wrapper, {
children: [
jsx('p', {
slot: 'myslot',
className: 'n',
children: 'works'
})
]
})
});
const ctx = createRenderContext({ request: new Request('http://example.com/' )});
const response = await renderPage(createAstroModule(Page), ctx, env);
expect(response.status).to.equal(200);
const html = await response.text();
expect(html).to.include('<div><p class="n">works</p></div>');
});
});
});