Refactor rendering for testing (#5071)
* Refactor rendering for testing * Correctly pass the mod for endpoints
This commit is contained in:
parent
4866ff882a
commit
df453e420f
18 changed files with 498 additions and 301 deletions
|
@ -14,7 +14,7 @@ import { call as callEndpoint } from '../endpoint/index.js';
|
||||||
import { consoleLogDestination } from '../logger/console.js';
|
import { consoleLogDestination } from '../logger/console.js';
|
||||||
import { error } from '../logger/core.js';
|
import { error } from '../logger/core.js';
|
||||||
import { joinPaths, prependForwardSlash } from '../path.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 { RouteCache } from '../render/route-cache.js';
|
||||||
import {
|
import {
|
||||||
createLinkStylesheetElementSet,
|
createLinkStylesheetElementSet,
|
||||||
|
@ -31,16 +31,15 @@ export interface MatchOptions {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class App {
|
export class App {
|
||||||
|
#env: Environment;
|
||||||
#manifest: Manifest;
|
#manifest: Manifest;
|
||||||
#manifestData: ManifestData;
|
#manifestData: ManifestData;
|
||||||
#routeDataToRouteInfo: Map<RouteData, RouteInfo>;
|
#routeDataToRouteInfo: Map<RouteData, RouteInfo>;
|
||||||
#routeCache: RouteCache;
|
|
||||||
#encoder = new TextEncoder();
|
#encoder = new TextEncoder();
|
||||||
#logging: LogOptions = {
|
#logging: LogOptions = {
|
||||||
dest: consoleLogDestination,
|
dest: consoleLogDestination,
|
||||||
level: 'info',
|
level: 'info',
|
||||||
};
|
};
|
||||||
#streaming: boolean;
|
|
||||||
|
|
||||||
constructor(manifest: Manifest, streaming = true) {
|
constructor(manifest: Manifest, streaming = true) {
|
||||||
this.#manifest = manifest;
|
this.#manifest = manifest;
|
||||||
|
@ -48,8 +47,32 @@ export class App {
|
||||||
routes: manifest.routes.map((route) => route.routeData),
|
routes: manifest.routes.map((route) => route.routeData),
|
||||||
};
|
};
|
||||||
this.#routeDataToRouteInfo = new Map(manifest.routes.map((route) => [route.routeData, route]));
|
this.#routeDataToRouteInfo = new Map(manifest.routes.map((route) => [route.routeData, route]));
|
||||||
this.#routeCache = new RouteCache(this.#logging);
|
this.#env = createEnvironment({
|
||||||
this.#streaming = streaming;
|
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 {
|
match(request: Request, { matchNotFound = false }: MatchOptions = {}): RouteData | undefined {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
|
@ -148,41 +171,17 @@ export class App {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await render({
|
const ctx = createRenderContext({
|
||||||
adapterName: manifest.adapterName,
|
request,
|
||||||
links,
|
|
||||||
logging: this.#logging,
|
|
||||||
markdown: manifest.markdown,
|
|
||||||
mod,
|
|
||||||
mode: 'production',
|
|
||||||
origin: url.origin,
|
origin: url.origin,
|
||||||
pathname: url.pathname,
|
pathname: url.pathname,
|
||||||
scripts,
|
scripts,
|
||||||
renderers,
|
links,
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
route: routeData,
|
route: routeData,
|
||||||
routeCache: this.#routeCache,
|
|
||||||
site: this.#manifest.site,
|
|
||||||
ssr: true,
|
|
||||||
request,
|
|
||||||
streaming: this.#streaming,
|
|
||||||
status,
|
status,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const response = await renderPage(mod, ctx, this.#env);
|
||||||
return response;
|
return response;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
error(this.#logging, 'ssr', err.stack || err.message || String(err));
|
error(this.#logging, 'ssr', err.stack || err.message || String(err));
|
||||||
|
@ -201,17 +200,17 @@ export class App {
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
const handler = mod as unknown as EndpointHandler;
|
const handler = mod as unknown as EndpointHandler;
|
||||||
const result = await callEndpoint(handler, {
|
|
||||||
logging: this.#logging,
|
const ctx = createRenderContext({
|
||||||
|
request,
|
||||||
origin: url.origin,
|
origin: url.origin,
|
||||||
pathname: url.pathname,
|
pathname: url.pathname,
|
||||||
request,
|
|
||||||
route: routeData,
|
route: routeData,
|
||||||
routeCache: this.#routeCache,
|
|
||||||
ssr: true,
|
|
||||||
status,
|
status,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const result = await callEndpoint(handler, this.#env, ctx);
|
||||||
|
|
||||||
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 fourOhFourRequest = new Request(new URL('/404', request.url));
|
const fourOhFourRequest = new Request(new URL('/404', request.url));
|
||||||
|
|
|
@ -19,12 +19,11 @@ import {
|
||||||
removeLeadingForwardSlash,
|
removeLeadingForwardSlash,
|
||||||
removeTrailingForwardSlash,
|
removeTrailingForwardSlash,
|
||||||
} from '../../core/path.js';
|
} from '../../core/path.js';
|
||||||
import type { RenderOptions } from '../../core/render/core';
|
|
||||||
import { runHookBuildGenerated } from '../../integrations/index.js';
|
import { runHookBuildGenerated } from '../../integrations/index.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 { call as callEndpoint } from '../endpoint/index.js';
|
import { call as callEndpoint } from '../endpoint/index.js';
|
||||||
import { debug, info } from '../logger/core.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 { callGetStaticPaths } from '../render/route-cache.js';
|
||||||
import { createLinkStylesheetElementSet, createModuleScriptsSet } from '../render/ssr-element.js';
|
import { createLinkStylesheetElementSet, createModuleScriptsSet } from '../render/ssr-element.js';
|
||||||
import { createRequest } from '../request.js';
|
import { createRequest } from '../request.js';
|
||||||
|
@ -360,19 +359,14 @@ async function generatePath(
|
||||||
opts.settings.config.build.format,
|
opts.settings.config.build.format,
|
||||||
pageData.route.type
|
pageData.route.type
|
||||||
);
|
);
|
||||||
const options: RenderOptions = {
|
const env = createEnvironment({
|
||||||
adapterName: undefined,
|
adapterName: undefined,
|
||||||
links,
|
|
||||||
logging,
|
logging,
|
||||||
markdown: {
|
markdown: {
|
||||||
...settings.config.markdown,
|
...settings.config.markdown,
|
||||||
isAstroFlavoredMd: settings.config.legacy.astroFlavoredMarkdown,
|
isAstroFlavoredMd: settings.config.legacy.astroFlavoredMarkdown,
|
||||||
},
|
},
|
||||||
mod,
|
|
||||||
mode: opts.mode,
|
mode: opts.mode,
|
||||||
origin,
|
|
||||||
pathname,
|
|
||||||
scripts,
|
|
||||||
renderers,
|
renderers,
|
||||||
async resolve(specifier: string) {
|
async resolve(specifier: string) {
|
||||||
const hashedFilePath = internals.entrySpecifierToBundleMap.get(specifier);
|
const hashedFilePath = internals.entrySpecifierToBundleMap.get(specifier);
|
||||||
|
@ -386,20 +380,27 @@ async function generatePath(
|
||||||
}
|
}
|
||||||
return prependForwardSlash(npath.posix.join(settings.config.base, hashedFilePath));
|
return prependForwardSlash(npath.posix.join(settings.config.base, hashedFilePath));
|
||||||
},
|
},
|
||||||
request: createRequest({ url, headers: new Headers(), logging, ssr }),
|
|
||||||
route: pageData.route,
|
|
||||||
routeCache,
|
routeCache,
|
||||||
site: settings.config.site
|
site: settings.config.site
|
||||||
? new URL(settings.config.base, settings.config.site).toString()
|
? new URL(settings.config.base, settings.config.site).toString()
|
||||||
: settings.config.site,
|
: settings.config.site,
|
||||||
ssr,
|
ssr,
|
||||||
streaming: true,
|
streaming: true,
|
||||||
};
|
});
|
||||||
|
const ctx = createRenderContext({
|
||||||
|
origin,
|
||||||
|
pathname,
|
||||||
|
request: createRequest({ url, headers: new Headers(), logging, ssr }),
|
||||||
|
scripts,
|
||||||
|
links,
|
||||||
|
route: pageData.route,
|
||||||
|
});
|
||||||
|
|
||||||
let body: string;
|
let body: string;
|
||||||
let encoding: BufferEncoding | undefined;
|
let encoding: BufferEncoding | undefined;
|
||||||
if (pageData.route.type === 'endpoint') {
|
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') {
|
if (result.type === 'response') {
|
||||||
throw new Error(`Returning a Response from an endpoint is not supported in SSG mode.`);
|
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;
|
body = result.body;
|
||||||
encoding = result.encoding;
|
encoding = result.encoding;
|
||||||
} else {
|
} else {
|
||||||
const response = await render(options);
|
const response = await renderPage(mod, ctx, env);
|
||||||
|
|
||||||
// If there's a redirect or something, just do nothing.
|
// If there's a redirect or something, just do nothing.
|
||||||
if (response.status !== 200 || !response.body) {
|
if (response.status !== 200 || !response.body) {
|
||||||
|
|
|
@ -1,14 +1,18 @@
|
||||||
import type { EndpointHandler } from '../../../@types/astro';
|
import type { EndpointHandler } from '../../../@types/astro';
|
||||||
import type { SSROptions } from '../../render/dev';
|
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';
|
import { call as callEndpoint } from '../index.js';
|
||||||
|
|
||||||
export async function call(ssrOpts: SSROptions) {
|
export async function call(options: SSROptions) {
|
||||||
const [, mod] = await preload(ssrOpts);
|
const { env, preload: [,mod] } = options;
|
||||||
return await callEndpoint(mod as unknown as EndpointHandler, {
|
const endpointHandler = mod as unknown as EndpointHandler;
|
||||||
...ssrOpts,
|
|
||||||
ssr: ssrOpts.settings.config.output === 'server',
|
const ctx = createRenderContext({
|
||||||
site: ssrOpts.settings.config.site,
|
request: options.request,
|
||||||
adapterName: ssrOpts.settings.config.adapter?.name,
|
origin: options.origin,
|
||||||
|
pathname: options.pathname,
|
||||||
|
route: options.route
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return await callEndpoint(endpointHandler, env, ctx);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import type { APIContext, EndpointHandler, Params } from '../../@types/astro';
|
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 { renderEndpoint } from '../../runtime/server/index.js';
|
||||||
import { ASTRO_VERSION } from '../constants.js';
|
import { ASTRO_VERSION } from '../constants.js';
|
||||||
|
@ -8,21 +8,6 @@ import { getParamsAndProps, GetParamsAndPropsError } from '../render/core.js';
|
||||||
|
|
||||||
const clientAddressSymbol = Symbol.for('astro.clientAddress');
|
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 EndpointCallResult =
|
||||||
| {
|
| {
|
||||||
type: 'simple';
|
type: 'simple';
|
||||||
|
@ -83,25 +68,34 @@ function createAPIContext({
|
||||||
|
|
||||||
export async function call(
|
export async function call(
|
||||||
mod: EndpointHandler,
|
mod: EndpointHandler,
|
||||||
opts: EndpointOptions
|
env: Environment,
|
||||||
|
ctx: RenderContext
|
||||||
): Promise<EndpointCallResult> {
|
): 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) {
|
if (paramsAndPropsResp === GetParamsAndPropsError.NoMatchingStaticPath) {
|
||||||
throw new Error(
|
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 [params, props] = paramsAndPropsResp;
|
||||||
|
|
||||||
const context = createAPIContext({
|
const context = createAPIContext({
|
||||||
request: opts.request,
|
request: ctx.request,
|
||||||
params,
|
params,
|
||||||
props,
|
props,
|
||||||
site: opts.site,
|
site: env.site,
|
||||||
adapterName: opts.adapterName,
|
adapterName: env.adapterName,
|
||||||
});
|
});
|
||||||
const response = await renderEndpoint(mod, context, opts.ssr);
|
|
||||||
|
const response = await renderEndpoint(mod, context, env.ssr);
|
||||||
|
|
||||||
if (response instanceof Response) {
|
if (response instanceof Response) {
|
||||||
attachToResponse(response, context.cookies);
|
attachToResponse(response, context.cookies);
|
||||||
|
|
45
packages/astro/src/core/render/context.ts
Normal file
45
packages/astro/src/core/render/context.ts
Normal 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
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,16 +1,14 @@
|
||||||
import type { MarkdownRenderingOptions } from '@astrojs/markdown-remark';
|
|
||||||
import type {
|
import type {
|
||||||
ComponentInstance,
|
ComponentInstance,
|
||||||
Params,
|
Params,
|
||||||
Props,
|
Props,
|
||||||
RouteData,
|
RouteData,
|
||||||
RuntimeMode,
|
|
||||||
SSRElement,
|
|
||||||
SSRLoadedRenderer,
|
|
||||||
} from '../../@types/astro';
|
} from '../../@types/astro';
|
||||||
import type { LogOptions } from '../logger/core.js';
|
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 { attachToResponse } from '../cookies/index.js';
|
||||||
import { getParams } from '../routing/params.js';
|
import { getParams } from '../routing/params.js';
|
||||||
import { createResult } from './result.js';
|
import { createResult } from './result.js';
|
||||||
|
@ -67,90 +65,46 @@ export async function getParamsAndProps(
|
||||||
return [params, pageProps];
|
return [params, pageProps];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RenderOptions {
|
export async function renderPage(mod: ComponentInstance, ctx: RenderContext, env: Environment) {
|
||||||
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;
|
|
||||||
|
|
||||||
const paramsAndPropsRes = await getParamsAndProps({
|
const paramsAndPropsRes = await getParamsAndProps({
|
||||||
logging,
|
logging: env.logging,
|
||||||
mod,
|
mod,
|
||||||
route,
|
route: ctx.route,
|
||||||
routeCache,
|
routeCache: env.routeCache,
|
||||||
pathname,
|
pathname: ctx.pathname,
|
||||||
ssr,
|
ssr: env.ssr,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (paramsAndPropsRes === GetParamsAndPropsError.NoMatchingStaticPath) {
|
if (paramsAndPropsRes === GetParamsAndPropsError.NoMatchingStaticPath) {
|
||||||
throw new Error(
|
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;
|
const [params, pageProps] = paramsAndPropsRes;
|
||||||
|
|
||||||
// Validate the page component before rendering the page
|
// Validate the page component before rendering the page
|
||||||
const Component = await mod.default;
|
const Component = mod.default;
|
||||||
if (!Component)
|
if (!Component)
|
||||||
throw new Error(`Expected an exported Astro component but received typeof ${typeof Component}`);
|
throw new Error(`Expected an exported Astro component but received typeof ${typeof Component}`);
|
||||||
|
|
||||||
const result = createResult({
|
const result = createResult({
|
||||||
adapterName,
|
adapterName: env.adapterName,
|
||||||
links,
|
links: ctx.links,
|
||||||
styles,
|
styles: ctx.styles,
|
||||||
logging,
|
logging: env.logging,
|
||||||
markdown,
|
markdown: env.markdown,
|
||||||
mode,
|
mode: env.mode,
|
||||||
origin,
|
origin: ctx.origin,
|
||||||
params,
|
params,
|
||||||
props: pageProps,
|
props: pageProps,
|
||||||
pathname,
|
pathname: ctx.pathname,
|
||||||
resolve,
|
resolve: env.resolve,
|
||||||
renderers,
|
renderers: env.renderers,
|
||||||
request,
|
request: ctx.request,
|
||||||
site,
|
site: env.site,
|
||||||
scripts,
|
scripts: ctx.scripts,
|
||||||
ssr,
|
ssr: env.ssr,
|
||||||
status,
|
status: ctx.status ?? 200,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Support `export const components` for `MDX` pages
|
// 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
|
// If there is an Astro.cookies instance, attach it to the response so that
|
||||||
// adapters can grab the Set-Cookie headers.
|
// adapters can grab the Set-Cookie headers.
|
||||||
|
|
47
packages/astro/src/core/render/dev/environment.ts
Normal file
47
packages/astro/src/core/render/dev/environment.ts
Normal 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
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import type { ViteDevServer } from 'vite';
|
import type { ViteDevServer } from 'vite';
|
||||||
import type {
|
import type {
|
||||||
AstroRenderer,
|
|
||||||
AstroSettings,
|
AstroSettings,
|
||||||
ComponentInstance,
|
ComponentInstance,
|
||||||
RouteData,
|
RouteData,
|
||||||
|
@ -9,16 +8,20 @@ import type {
|
||||||
SSRElement,
|
SSRElement,
|
||||||
SSRLoadedRenderer,
|
SSRLoadedRenderer,
|
||||||
} from '../../../@types/astro';
|
} from '../../../@types/astro';
|
||||||
|
import type { DevelopmentEnvironment } from './environment';
|
||||||
import { PAGE_SCRIPT_ID } from '../../../vite-plugin-scripts/index.js';
|
import { PAGE_SCRIPT_ID } from '../../../vite-plugin-scripts/index.js';
|
||||||
import { LogOptions } from '../../logger/core.js';
|
import { LogOptions } from '../../logger/core.js';
|
||||||
import { isPage, resolveIdToUrl } from '../../util.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 { RouteCache } from '../route-cache.js';
|
||||||
import { collectMdMetadata } from '../util.js';
|
import { collectMdMetadata } from '../util.js';
|
||||||
import { getStylesForURL } from './css.js';
|
import { getStylesForURL } from './css.js';
|
||||||
import { getScriptsForURL } from './scripts.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 */
|
/** an instance of the AstroSettings */
|
||||||
settings: AstroSettings;
|
settings: AstroSettings;
|
||||||
/** location of file on disk */
|
/** location of file on disk */
|
||||||
|
@ -41,72 +44,81 @@ export interface SSROptions {
|
||||||
request: Request;
|
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(
|
export async function loadRenderers(
|
||||||
viteServer: ViteDevServer,
|
viteServer: ViteDevServer,
|
||||||
settings: AstroSettings
|
settings: AstroSettings
|
||||||
): Promise<SSRLoadedRenderer[]> {
|
): 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({
|
export async function preload({
|
||||||
settings,
|
env,
|
||||||
filePath,
|
filePath,
|
||||||
viteServer,
|
}: Pick<SSROptions, 'env' | 'filePath'>): Promise<ComponentPreload> {
|
||||||
}: Pick<SSROptions, 'settings' | 'filePath' | 'viteServer'>): Promise<ComponentPreload> {
|
|
||||||
// Important: This needs to happen first, in case a renderer provides polyfills.
|
// 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.
|
// Load the module from the Vite SSR Runtime.
|
||||||
const mod = (await viteServer.ssrLoadModule(fileURLToPath(filePath))) as ComponentInstance;
|
const mod = (await env.viteServer.ssrLoadModule(fileURLToPath(filePath))) as ComponentInstance;
|
||||||
if (viteServer.config.mode === 'development' || !mod?.$$metadata) {
|
if (env.viteServer.config.mode === 'development' || !mod?.$$metadata) {
|
||||||
return [renderers, mod];
|
return [renderers, mod];
|
||||||
}
|
}
|
||||||
|
|
||||||
// append all nested markdown metadata to mod.$$metadata
|
// 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) {
|
if (modGraph) {
|
||||||
await collectMdMetadata(mod.$$metadata, modGraph, viteServer);
|
await collectMdMetadata(mod.$$metadata, modGraph, env.viteServer);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [renderers, mod];
|
return [renderers, mod];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** use Vite to SSR */
|
interface GetScriptsAndStylesParams {
|
||||||
export async function render(
|
env: DevelopmentEnvironment;
|
||||||
renderers: SSRLoadedRenderer[],
|
filePath: URL;
|
||||||
mod: ComponentInstance,
|
}
|
||||||
ssrOpts: SSROptions
|
|
||||||
): Promise<Response> {
|
async function getScriptsAndStyles({ env, filePath }: GetScriptsAndStylesParams) {
|
||||||
const {
|
|
||||||
settings,
|
|
||||||
filePath,
|
|
||||||
logging,
|
|
||||||
mode,
|
|
||||||
origin,
|
|
||||||
pathname,
|
|
||||||
request,
|
|
||||||
route,
|
|
||||||
routeCache,
|
|
||||||
viteServer,
|
|
||||||
} = ssrOpts;
|
|
||||||
// Add hoisted script tags
|
// Add hoisted script tags
|
||||||
const scripts = await getScriptsForURL(filePath, viteServer);
|
const scripts = await getScriptsForURL(filePath, env.viteServer);
|
||||||
|
|
||||||
// Inject HMR scripts
|
// Inject HMR scripts
|
||||||
if (isPage(filePath, settings) && mode === 'development') {
|
if (isPage(filePath, env.settings) && env.mode === 'development') {
|
||||||
scripts.add({
|
scripts.add({
|
||||||
props: { type: 'module', src: '/@vite/client' },
|
props: { type: 'module', src: '/@vite/client' },
|
||||||
children: '',
|
children: '',
|
||||||
|
@ -114,20 +126,20 @@ export async function render(
|
||||||
scripts.add({
|
scripts.add({
|
||||||
props: {
|
props: {
|
||||||
type: 'module',
|
type: 'module',
|
||||||
src: await resolveIdToUrl(viteServer, 'astro/runtime/client/hmr.js'),
|
src: await resolveIdToUrl(env.viteServer, 'astro/runtime/client/hmr.js'),
|
||||||
},
|
},
|
||||||
children: '',
|
children: '',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: We should allow adding generic HTML elements to the head, not just scripts
|
// 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') {
|
if (script.stage === 'head-inline') {
|
||||||
scripts.add({
|
scripts.add({
|
||||||
props: {},
|
props: {},
|
||||||
children: script.content,
|
children: script.content,
|
||||||
});
|
});
|
||||||
} else if (script.stage === 'page' && isPage(filePath, settings)) {
|
} else if (script.stage === 'page' && isPage(filePath, env.settings)) {
|
||||||
scripts.add({
|
scripts.add({
|
||||||
props: { type: 'module', src: `/@id/${PAGE_SCRIPT_ID}` },
|
props: { type: 'module', src: `/@id/${PAGE_SCRIPT_ID}` },
|
||||||
children: '',
|
children: '',
|
||||||
|
@ -136,7 +148,7 @@ export async function render(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass framework CSS in as style tags to be appended to the page.
|
// 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>();
|
let links = new Set<SSRElement>();
|
||||||
[...styleUrls].forEach((href) => {
|
[...styleUrls].forEach((href) => {
|
||||||
links.add({
|
links.add({
|
||||||
|
@ -164,54 +176,31 @@ export async function render(
|
||||||
children: content,
|
children: content,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return { scripts, styles, links };
|
||||||
|
}
|
||||||
|
|
||||||
let response = await coreRender({
|
export async function renderPage(options: SSROptions): Promise<Response> {
|
||||||
adapterName: settings.config.adapter?.name,
|
const [renderers, mod] = options.preload;
|
||||||
links,
|
|
||||||
styles,
|
// Override the environment's renderers. This ensures that if renderers change (HMR)
|
||||||
logging,
|
// The new instances are passed through.
|
||||||
markdown: {
|
options.env.renderers = renderers;
|
||||||
...settings.config.markdown,
|
|
||||||
isAstroFlavoredMd: settings.config.legacy.astroFlavoredMarkdown,
|
const { scripts, links, styles } = await getScriptsAndStyles({
|
||||||
},
|
env: options.env,
|
||||||
mod,
|
filePath: options.filePath
|
||||||
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,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return response;
|
const ctx = createRenderContext({
|
||||||
}
|
request: options.request,
|
||||||
|
origin: options.origin,
|
||||||
|
pathname: options.pathname,
|
||||||
|
scripts,
|
||||||
|
links,
|
||||||
|
styles,
|
||||||
|
route: options.route
|
||||||
|
});
|
||||||
|
|
||||||
export async function ssr(
|
return await coreRenderPage(mod, ctx, options.env); // NOTE: without "await", errors won’t get caught below
|
||||||
preloadedComponent: ComponentPreload,
|
|
||||||
ssrOpts: SSROptions
|
|
||||||
): Promise<Response> {
|
|
||||||
const [renderers, mod] = preloadedComponent;
|
|
||||||
return await render(renderers, mod, ssrOpts); // NOTE: without "await", errors won’t get caught below
|
|
||||||
}
|
}
|
||||||
|
|
20
packages/astro/src/core/render/dev/resolve.ts
Normal file
20
packages/astro/src/core/render/dev/resolve.ts
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
52
packages/astro/src/core/render/environment.ts
Normal file
52
packages/astro/src/core/render/environment.ts
Normal 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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
22
packages/astro/src/core/render/index.ts
Normal file
22
packages/astro/src/core/render/index.ts
Normal 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';
|
28
packages/astro/src/core/render/renderer.ts
Normal file
28
packages/astro/src/core/render/renderer.ts
Normal 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
|
||||||
|
};
|
||||||
|
}
|
|
@ -129,7 +129,7 @@ class Slots {
|
||||||
let renderMarkdown: any = null;
|
let renderMarkdown: any = null;
|
||||||
|
|
||||||
export function createResult(args: CreateResultArgs): SSRResult {
|
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 url = new URL(request.url);
|
||||||
const headers = new Headers();
|
const headers = new Headers();
|
||||||
|
|
9
packages/astro/src/jsx/component.ts
Normal file
9
packages/astro/src/jsx/component.ts
Normal 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;
|
||||||
|
}
|
6
packages/astro/src/jsx/index.ts
Normal file
6
packages/astro/src/jsx/index.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export {
|
||||||
|
default as renderer
|
||||||
|
} from './renderer.js';
|
||||||
|
export {
|
||||||
|
createAstroJSXComponent
|
||||||
|
} from './component.js';
|
|
@ -21,6 +21,7 @@ export {
|
||||||
stringifyChunk,
|
stringifyChunk,
|
||||||
voidElementNames,
|
voidElementNames,
|
||||||
} from './render/index.js';
|
} from './render/index.js';
|
||||||
|
export { renderJSX } from './jsx.js';
|
||||||
export type { AstroComponentFactory, RenderInstruction } from './render/index.js';
|
export type { AstroComponentFactory, RenderInstruction } from './render/index.js';
|
||||||
import type { AstroComponentFactory } from './render/index.js';
|
import type { AstroComponentFactory } from './render/index.js';
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import type http from 'http';
|
||||||
import mime from 'mime';
|
import mime from 'mime';
|
||||||
import type * as vite from 'vite';
|
import type * as vite from 'vite';
|
||||||
import type { AstroSettings, ManifestData } from '../@types/astro';
|
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 { Readable } from 'stream';
|
||||||
import { getSetCookiesFromResponse } from '../core/cookies/index.js';
|
import { getSetCookiesFromResponse } from '../core/cookies/index.js';
|
||||||
|
@ -16,9 +16,8 @@ import {
|
||||||
import { error, info, LogOptions, warn } from '../core/logger/core.js';
|
import { error, info, LogOptions, warn } from '../core/logger/core.js';
|
||||||
import * as msg from '../core/messages.js';
|
import * as msg from '../core/messages.js';
|
||||||
import { appendForwardSlash } from '../core/path.js';
|
import { appendForwardSlash } from '../core/path.js';
|
||||||
import { getParamsAndProps, GetParamsAndPropsError } from '../core/render/core.js';
|
import { getParamsAndProps, GetParamsAndPropsError } from '../core/render/index.js';
|
||||||
import { preload, ssr } from '../core/render/dev/index.js';
|
import { createDevelopmentEnvironment, preload, renderPage } from '../core/render/dev/index.js';
|
||||||
import { RouteCache } from '../core/render/route-cache.js';
|
|
||||||
import { createRequest } from '../core/request.js';
|
import { createRequest } from '../core/request.js';
|
||||||
import { createRouteManifest, matchAllRoutes } from '../core/routing/index.js';
|
import { createRouteManifest, matchAllRoutes } from '../core/routing/index.js';
|
||||||
import { resolvePages } from '../core/util.js';
|
import { resolvePages } from '../core/util.js';
|
||||||
|
@ -100,7 +99,6 @@ async function writeSSRResult(webResponse: Response, res: http.ServerResponse) {
|
||||||
|
|
||||||
async function handle404Response(
|
async function handle404Response(
|
||||||
origin: string,
|
origin: string,
|
||||||
settings: AstroSettings,
|
|
||||||
req: http.IncomingMessage,
|
req: http.IncomingMessage,
|
||||||
res: http.ServerResponse
|
res: http.ServerResponse
|
||||||
) {
|
) {
|
||||||
|
@ -187,17 +185,15 @@ export function baseMiddleware(
|
||||||
|
|
||||||
async function matchRoute(
|
async function matchRoute(
|
||||||
pathname: string,
|
pathname: string,
|
||||||
routeCache: RouteCache,
|
env: DevelopmentEnvironment,
|
||||||
viteServer: vite.ViteDevServer,
|
|
||||||
logging: LogOptions,
|
|
||||||
manifest: ManifestData,
|
manifest: ManifestData,
|
||||||
settings: AstroSettings
|
|
||||||
) {
|
) {
|
||||||
|
const { logging, settings, routeCache } = env;
|
||||||
const matches = matchAllRoutes(pathname, manifest);
|
const matches = matchAllRoutes(pathname, manifest);
|
||||||
|
|
||||||
for await (const maybeRoute of matches) {
|
for await (const maybeRoute of matches) {
|
||||||
const filePath = new URL(`./${maybeRoute.component}`, settings.config.root);
|
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;
|
const [, mod] = preloadedComponent;
|
||||||
// attempt to get static paths
|
// attempt to get static paths
|
||||||
// if this fails, we have a bad URL match!
|
// if this fails, we have a bad URL match!
|
||||||
|
@ -233,7 +229,7 @@ async function matchRoute(
|
||||||
|
|
||||||
if (custom404) {
|
if (custom404) {
|
||||||
const filePath = new URL(`./${custom404.component}`, settings.config.root);
|
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;
|
const [, mod] = preloadedComponent;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -249,14 +245,12 @@ async function matchRoute(
|
||||||
|
|
||||||
/** The main logic to route dev server requests to pages in Astro. */
|
/** The main logic to route dev server requests to pages in Astro. */
|
||||||
async function handleRequest(
|
async function handleRequest(
|
||||||
routeCache: RouteCache,
|
env: DevelopmentEnvironment,
|
||||||
viteServer: vite.ViteDevServer,
|
|
||||||
logging: LogOptions,
|
|
||||||
manifest: ManifestData,
|
manifest: ManifestData,
|
||||||
settings: AstroSettings,
|
|
||||||
req: http.IncomingMessage,
|
req: http.IncomingMessage,
|
||||||
res: http.ServerResponse
|
res: http.ServerResponse
|
||||||
) {
|
) {
|
||||||
|
const { settings, viteServer } = env;
|
||||||
const { config } = settings;
|
const { config } = settings;
|
||||||
const origin = `${viteServer.config.server.https ? 'https' : 'http'}://${req.headers.host}`;
|
const origin = `${viteServer.config.server.https ? 'https' : 'http'}://${req.headers.host}`;
|
||||||
const buildingToSSR = config.output === 'server';
|
const buildingToSSR = config.output === 'server';
|
||||||
|
@ -296,11 +290,8 @@ async function handleRequest(
|
||||||
try {
|
try {
|
||||||
const matchedRoute = await matchRoute(
|
const matchedRoute = await matchRoute(
|
||||||
pathname,
|
pathname,
|
||||||
routeCache,
|
env,
|
||||||
viteServer,
|
|
||||||
logging,
|
|
||||||
manifest,
|
manifest,
|
||||||
settings
|
|
||||||
);
|
);
|
||||||
filePath = matchedRoute?.filePath;
|
filePath = matchedRoute?.filePath;
|
||||||
|
|
||||||
|
@ -310,18 +301,15 @@ async function handleRequest(
|
||||||
pathname,
|
pathname,
|
||||||
body,
|
body,
|
||||||
origin,
|
origin,
|
||||||
routeCache,
|
env,
|
||||||
viteServer,
|
|
||||||
manifest,
|
manifest,
|
||||||
logging,
|
|
||||||
settings,
|
|
||||||
req,
|
req,
|
||||||
res
|
res
|
||||||
);
|
);
|
||||||
} catch (_err) {
|
} catch (_err) {
|
||||||
const err = fixViteErrorMessage(_err, viteServer, filePath);
|
const err = fixViteErrorMessage(_err, viteServer, filePath);
|
||||||
const errorWithMetadata = collectErrorMetadata(err);
|
const errorWithMetadata = collectErrorMetadata(err);
|
||||||
error(logging, null, msg.formatErrorMessage(errorWithMetadata));
|
error(env.logging, null, msg.formatErrorMessage(errorWithMetadata));
|
||||||
handle500Response(viteServer, origin, req, res, errorWithMetadata);
|
handle500Response(viteServer, origin, req, res, errorWithMetadata);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -332,16 +320,14 @@ async function handleRoute(
|
||||||
pathname: string,
|
pathname: string,
|
||||||
body: ArrayBuffer | undefined,
|
body: ArrayBuffer | undefined,
|
||||||
origin: string,
|
origin: string,
|
||||||
routeCache: RouteCache,
|
env: DevelopmentEnvironment,
|
||||||
viteServer: vite.ViteDevServer,
|
|
||||||
manifest: ManifestData,
|
manifest: ManifestData,
|
||||||
logging: LogOptions,
|
|
||||||
settings: AstroSettings,
|
|
||||||
req: http.IncomingMessage,
|
req: http.IncomingMessage,
|
||||||
res: http.ServerResponse
|
res: http.ServerResponse
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const { logging, settings } = env;
|
||||||
if (!matchedRoute) {
|
if (!matchedRoute) {
|
||||||
return handle404Response(origin, settings, req, res);
|
return handle404Response(origin, req, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { config } = settings;
|
const { config } = settings;
|
||||||
|
@ -365,23 +351,20 @@ async function handleRoute(
|
||||||
const paramsAndPropsRes = await getParamsAndProps({
|
const paramsAndPropsRes = await getParamsAndProps({
|
||||||
mod,
|
mod,
|
||||||
route,
|
route,
|
||||||
routeCache,
|
routeCache: env.routeCache,
|
||||||
pathname: pathname,
|
pathname: pathname,
|
||||||
logging,
|
logging,
|
||||||
ssr: config.output === 'server',
|
ssr: config.output === 'server',
|
||||||
});
|
});
|
||||||
|
|
||||||
const options: SSROptions = {
|
const options: SSROptions = {
|
||||||
settings,
|
env,
|
||||||
filePath,
|
filePath,
|
||||||
logging,
|
origin,
|
||||||
mode: 'development',
|
preload: preloadedComponent,
|
||||||
origin,
|
pathname,
|
||||||
pathname: pathname,
|
request,
|
||||||
route,
|
route
|
||||||
routeCache,
|
|
||||||
viteServer,
|
|
||||||
request,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Route successfully matched! Render it.
|
// Route successfully matched! Render it.
|
||||||
|
@ -391,11 +374,8 @@ async function handleRoute(
|
||||||
if (result.response.headers.get('X-Astro-Response') === 'Not-Found') {
|
if (result.response.headers.get('X-Astro-Response') === 'Not-Found') {
|
||||||
const fourOhFourRoute = await matchRoute(
|
const fourOhFourRoute = await matchRoute(
|
||||||
'/404',
|
'/404',
|
||||||
routeCache,
|
env,
|
||||||
viteServer,
|
manifest
|
||||||
logging,
|
|
||||||
manifest,
|
|
||||||
settings
|
|
||||||
);
|
);
|
||||||
return handleRoute(
|
return handleRoute(
|
||||||
fourOhFourRoute,
|
fourOhFourRoute,
|
||||||
|
@ -403,11 +383,8 @@ async function handleRoute(
|
||||||
'/404',
|
'/404',
|
||||||
body,
|
body,
|
||||||
origin,
|
origin,
|
||||||
routeCache,
|
env,
|
||||||
viteServer,
|
|
||||||
manifest,
|
manifest,
|
||||||
logging,
|
|
||||||
settings,
|
|
||||||
req,
|
req,
|
||||||
res
|
res
|
||||||
);
|
);
|
||||||
|
@ -427,7 +404,7 @@ async function handleRoute(
|
||||||
res.end(result.body);
|
res.end(result.body);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const result = await ssr(preloadedComponent, options);
|
const result = await renderPage(options);
|
||||||
return await writeSSRResult(result, res);
|
return await writeSSRResult(result, res);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -436,11 +413,12 @@ export default function createPlugin({ settings, logging }: AstroPluginOptions):
|
||||||
return {
|
return {
|
||||||
name: 'astro:server',
|
name: 'astro:server',
|
||||||
configureServer(viteServer) {
|
configureServer(viteServer) {
|
||||||
let routeCache = new RouteCache(logging, 'development');
|
let env = createDevelopmentEnvironment(settings, logging, viteServer);
|
||||||
let manifest: ManifestData = createRouteManifest({ settings }, logging);
|
let manifest: ManifestData = createRouteManifest({ settings }, logging);
|
||||||
|
|
||||||
/** rebuild the route cache + manifest, as needed. */
|
/** rebuild the route cache + manifest, as needed. */
|
||||||
function rebuildManifest(needsManifestRebuild: boolean, file: string) {
|
function rebuildManifest(needsManifestRebuild: boolean, file: string) {
|
||||||
routeCache.clearAll();
|
env.routeCache.clearAll();
|
||||||
if (needsManifestRebuild) {
|
if (needsManifestRebuild) {
|
||||||
manifest = createRouteManifest({ settings }, logging);
|
manifest = createRouteManifest({ settings }, logging);
|
||||||
}
|
}
|
||||||
|
@ -461,7 +439,7 @@ export default function createPlugin({ settings, logging }: AstroPluginOptions):
|
||||||
if (!req.url || !req.method) {
|
if (!req.url || !req.method) {
|
||||||
throw new Error('Incomplete request');
|
throw new Error('Incomplete request');
|
||||||
}
|
}
|
||||||
handleRequest(routeCache, viteServer, logging, manifest, settings, req, res);
|
handleRequest(env, manifest, req, res);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
48
packages/astro/test/units/render/jsx.test.js
Normal file
48
packages/astro/test/units/render/jsx.test.js
Normal 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>');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in a new issue