summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/astro/src/core/app/index.ts75
-rw-r--r--packages/astro/src/core/build/generate.ts27
-rw-r--r--packages/astro/src/core/endpoint/dev/index.ts20
-rw-r--r--packages/astro/src/core/endpoint/index.ts40
-rw-r--r--packages/astro/src/core/render/context.ts45
-rw-r--r--packages/astro/src/core/render/core.ts100
-rw-r--r--packages/astro/src/core/render/dev/environment.ts47
-rw-r--r--packages/astro/src/core/render/dev/index.ts171
-rw-r--r--packages/astro/src/core/render/dev/resolve.ts20
-rw-r--r--packages/astro/src/core/render/environment.ts52
-rw-r--r--packages/astro/src/core/render/index.ts22
-rw-r--r--packages/astro/src/core/render/renderer.ts28
-rw-r--r--packages/astro/src/core/render/result.ts2
-rw-r--r--packages/astro/src/jsx/component.ts9
-rw-r--r--packages/astro/src/jsx/index.ts6
-rw-r--r--packages/astro/src/runtime/server/index.ts1
-rw-r--r--packages/astro/src/vite-plugin-astro-server/index.ts84
-rw-r--r--packages/astro/test/units/render/jsx.test.js48
18 files changed, 497 insertions, 300 deletions
diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts
index d4197839f..d08266fb1 100644
--- a/packages/astro/src/core/app/index.ts
+++ b/packages/astro/src/core/app/index.ts
@@ -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));
diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts
index 97df739a0..8d2622cfe 100644
--- a/packages/astro/src/core/build/generate.ts
+++ b/packages/astro/src/core/build/generate.ts
@@ -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) {
diff --git a/packages/astro/src/core/endpoint/dev/index.ts b/packages/astro/src/core/endpoint/dev/index.ts
index b27127119..59bc3e730 100644
--- a/packages/astro/src/core/endpoint/dev/index.ts
+++ b/packages/astro/src/core/endpoint/dev/index.ts
@@ -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);
}
diff --git a/packages/astro/src/core/endpoint/index.ts b/packages/astro/src/core/endpoint/index.ts
index c4c1686e6..e73f98306 100644
--- a/packages/astro/src/core/endpoint/index.ts
+++ b/packages/astro/src/core/endpoint/index.ts
@@ -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);
diff --git a/packages/astro/src/core/render/context.ts b/packages/astro/src/core/render/context.ts
new file mode 100644
index 000000000..02e9b9439
--- /dev/null
+++ b/packages/astro/src/core/render/context.ts
@@ -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
+ };
+}
diff --git a/packages/astro/src/core/render/core.ts b/packages/astro/src/core/render/core.ts
index 5b7a3122a..ed2f39634 100644
--- a/packages/astro/src/core/render/core.ts
+++ b/packages/astro/src/core/render/core.ts
@@ -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.
diff --git a/packages/astro/src/core/render/dev/environment.ts b/packages/astro/src/core/render/dev/environment.ts
new file mode 100644
index 000000000..3b8daec75
--- /dev/null
+++ b/packages/astro/src/core/render/dev/environment.ts
@@ -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
+ };
+}
diff --git a/packages/astro/src/core/render/dev/index.ts b/packages/astro/src/core/render/dev/index.ts
index a5426b1b3..fb0482415 100644
--- a/packages/astro/src/core/render/dev/index.ts
+++ b/packages/astro/src/core/render/dev/index.ts
@@ -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 };
+}
+
+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
+ });
- let response = await coreRender({
- adapterName: settings.config.adapter?.name,
+ const ctx = createRenderContext({
+ request: options.request,
+ origin: options.origin,
+ pathname: options.pathname,
+ scripts,
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,
+ route: options.route
});
- return response;
-}
-
-export async function ssr(
- 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
+ return await coreRenderPage(mod, ctx, options.env); // NOTE: without "await", errors won’t get caught below
}
diff --git a/packages/astro/src/core/render/dev/resolve.ts b/packages/astro/src/core/render/dev/resolve.ts
new file mode 100644
index 000000000..c4fc4e6b3
--- /dev/null
+++ b/packages/astro/src/core/render/dev/resolve.ts
@@ -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;
+ }
+ };
+}
diff --git a/packages/astro/src/core/render/environment.ts b/packages/astro/src/core/render/environment.ts
new file mode 100644
index 000000000..0afad9517
--- /dev/null
+++ b/packages/astro/src/core/render/environment.ts
@@ -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
+ });
+}
+
diff --git a/packages/astro/src/core/render/index.ts b/packages/astro/src/core/render/index.ts
new file mode 100644
index 000000000..4c543b598
--- /dev/null
+++ b/packages/astro/src/core/render/index.ts
@@ -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';
diff --git a/packages/astro/src/core/render/renderer.ts b/packages/astro/src/core/render/renderer.ts
new file mode 100644
index 000000000..e82296a2a
--- /dev/null
+++ b/packages/astro/src/core/render/renderer.ts
@@ -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
+ };
+}
diff --git a/packages/astro/src/core/render/result.ts b/packages/astro/src/core/render/result.ts
index a87ea54c7..2d7c07d84 100644
--- a/packages/astro/src/core/render/result.ts
+++ b/packages/astro/src/core/render/result.ts
@@ -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();
diff --git a/packages/astro/src/jsx/component.ts b/packages/astro/src/jsx/component.ts
new file mode 100644
index 000000000..2c818334d
--- /dev/null
+++ b/packages/astro/src/jsx/component.ts
@@ -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;
+}
diff --git a/packages/astro/src/jsx/index.ts b/packages/astro/src/jsx/index.ts
new file mode 100644
index 000000000..00be71026
--- /dev/null
+++ b/packages/astro/src/jsx/index.ts
@@ -0,0 +1,6 @@
+export {
+ default as renderer
+} from './renderer.js';
+export {
+ createAstroJSXComponent
+} from './component.js';
diff --git a/packages/astro/src/runtime/server/index.ts b/packages/astro/src/runtime/server/index.ts
index 28ec19a92..065fbea0a 100644
--- a/packages/astro/src/runtime/server/index.ts
+++ b/packages/astro/src/runtime/server/index.ts
@@ -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';
diff --git a/packages/astro/src/vite-plugin-astro-server/index.ts b/packages/astro/src/vite-plugin-astro-server/index.ts
index deaade4b5..54bd42909 100644
--- a/packages/astro/src/vite-plugin-astro-server/index.ts
+++ b/packages/astro/src/vite-plugin-astro-server/index.ts
@@ -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);
});
};
},
diff --git a/packages/astro/test/units/render/jsx.test.js b/packages/astro/test/units/render/jsx.test.js
new file mode 100644
index 000000000..f2c2ceead
--- /dev/null
+++ b/packages/astro/test/units/render/jsx.test.js
@@ -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>');
+ });
+ });
+});