Refactor SSRResult and RenderContext (#7575)

This commit is contained in:
Bjorn Lu 2023-07-06 00:25:24 +08:00 committed by GitHub
parent 44e56bb3bc
commit 30d04db981
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 59 additions and 77 deletions

View file

@ -0,0 +1,5 @@
---
'@astrojs/markdoc': patch
---
Handle internal access change

View file

@ -1957,16 +1957,6 @@ export interface SSRElement {
children: string; children: string;
} }
export interface SSRMetadata {
renderers: SSRLoadedRenderer[];
pathname: string;
hasHydrationScript: boolean;
hasDirectives: Set<string>;
hasRenderedHead: boolean;
headInTree: boolean;
clientDirectives: Map<string, string>;
}
/** /**
* A hint on whether the Astro runtime needs to wait on a component to render head * A hint on whether the Astro runtime needs to wait on a component to render head
* content. The meanings: * content. The meanings:
@ -1989,9 +1979,6 @@ export interface SSRResult {
scripts: Set<SSRElement>; scripts: Set<SSRElement>;
links: Set<SSRElement>; links: Set<SSRElement>;
componentMetadata: Map<string, SSRComponentMetadata>; componentMetadata: Map<string, SSRComponentMetadata>;
propagators: Map<AstroComponentFactory, AstroComponentInstance>;
extraHead: Array<string>;
cookies: AstroCookies | undefined;
createAstro( createAstro(
Astro: AstroGlobalPartial, Astro: AstroGlobalPartial,
props: Record<string, any>, props: Record<string, any>,
@ -1999,13 +1986,32 @@ export interface SSRResult {
): AstroGlobal; ): AstroGlobal;
resolve: (s: string) => Promise<string>; resolve: (s: string) => Promise<string>;
response: ResponseInit; response: ResponseInit;
// Bits 1 = astro, 2 = jsx, 4 = slot renderers: SSRLoadedRenderer[];
// As rendering occurs these bits are manipulated to determine where content /**
// is within a slot. This is used for head injection. * Map of directive name (e.g. `load`) to the directive script code
scope: number; */
clientDirectives: Map<string, string>;
/**
* Only used for logging
*/
pathname: string;
cookies: AstroCookies | undefined;
_metadata: SSRMetadata; _metadata: SSRMetadata;
} }
/**
* Ephemeral and mutable state during rendering that doesn't rely
* on external configuration
*/
export interface SSRMetadata {
hasHydrationScript: boolean;
hasDirectives: Set<string>;
hasRenderedHead: boolean;
headInTree: boolean;
extraHead: string[];
propagators: Map<AstroComponentFactory, AstroComponentInstance>;
}
/* Preview server stuff */ /* Preview server stuff */
export interface PreviewServer { export interface PreviewServer {
host?: string; host?: string;

View file

@ -227,7 +227,6 @@ export class App {
const mod = (await page.page()) as any; const mod = (await page.page()) as any;
const renderContext = await createRenderContext({ const renderContext = await createRenderContext({
request, request,
origin: url.origin,
pathname, pathname,
componentMetadata: this.#manifest.componentMetadata, componentMetadata: this.#manifest.componentMetadata,
scripts, scripts,
@ -295,7 +294,6 @@ export class App {
const ctx = await createRenderContext({ const ctx = await createRenderContext({
request, request,
origin: url.origin,
pathname, pathname,
route: routeData, route: routeData,
status, status,

View file

@ -539,7 +539,6 @@ async function generatePath(
}); });
const renderContext = await createRenderContext({ const renderContext = await createRenderContext({
origin,
pathname, pathname,
request: createRequest({ url, headers: new Headers(), logging, ssr }), request: createRequest({ url, headers: new Headers(), logging, ssr }),
componentMetadata: manifest.componentMetadata, componentMetadata: manifest.componentMetadata,

View file

@ -10,7 +10,6 @@ export async function call(options: SSROptions, logging: LogOptions) {
const ctx = await createRenderContext({ const ctx = await createRenderContext({
request: options.request, request: options.request,
origin: options.origin,
pathname: options.pathname, pathname: options.pathname,
route: options.route, route: options.route,
env, env,

View file

@ -1,5 +1,4 @@
import type { import type {
AstroCookies,
ComponentInstance, ComponentInstance,
Params, Params,
Props, Props,
@ -18,23 +17,21 @@ const clientLocalsSymbol = Symbol.for('astro.locals');
*/ */
export interface RenderContext { export interface RenderContext {
request: Request; request: Request;
origin: string;
pathname: string; pathname: string;
url: URL;
scripts?: Set<SSRElement>; scripts?: Set<SSRElement>;
links?: Set<SSRElement>; links?: Set<SSRElement>;
styles?: Set<SSRElement>; styles?: Set<SSRElement>;
componentMetadata?: SSRResult['componentMetadata']; componentMetadata?: SSRResult['componentMetadata'];
route?: RouteData; route?: RouteData;
status?: number; status?: number;
cookies?: AstroCookies;
params: Params; params: Params;
props: Props; props: Props;
locals?: object; locals?: object;
} }
export type CreateRenderContextArgs = Partial<RenderContext> & { export type CreateRenderContextArgs = Partial<
origin?: string; Omit<RenderContext, 'params' | 'props' | 'locals'>
> & {
request: RenderContext['request']; request: RenderContext['request'];
mod: ComponentInstance; mod: ComponentInstance;
env: Environment; env: Environment;
@ -44,9 +41,7 @@ export async function createRenderContext(
options: CreateRenderContextArgs options: CreateRenderContextArgs
): Promise<RenderContext> { ): Promise<RenderContext> {
const request = options.request; const request = options.request;
const url = new URL(request.url); const pathname = options.pathname ?? new URL(request.url).pathname;
const origin = options.origin ?? url.origin;
const pathname = options.pathname ?? url.pathname;
const [params, props] = await getParamsAndProps({ const [params, props] = await getParamsAndProps({
mod: options.mod as any, mod: options.mod as any,
route: options.route, route: options.route,
@ -56,11 +51,9 @@ export async function createRenderContext(
ssr: options.env.ssr, ssr: options.env.ssr,
}); });
let context = { const context: RenderContext = {
...options, ...options,
origin,
pathname, pathname,
url,
params, params,
props, props,
}; };

View file

@ -41,10 +41,7 @@ export async function renderPage({
styles: renderContext.styles, styles: renderContext.styles,
logging: env.logging, logging: env.logging,
markdown: env.markdown, markdown: env.markdown,
mode: env.mode,
origin: renderContext.origin,
params: renderContext.params, params: renderContext.params,
props: renderContext.props,
pathname: renderContext.pathname, pathname: renderContext.pathname,
componentMetadata: renderContext.componentMetadata, componentMetadata: renderContext.componentMetadata,
resolve: env.resolve, resolve: env.resolve,

View file

@ -24,8 +24,6 @@ export interface SSROptions {
env: DevelopmentEnvironment; env: DevelopmentEnvironment;
/** location of file on disk */ /** location of file on disk */
filePath: URL; filePath: URL;
/** production website */
origin: string;
/** the web request (needed for dynamic routes) */ /** the web request (needed for dynamic routes) */
pathname: string; pathname: string;
/** The runtime component instance */ /** The runtime component instance */
@ -157,7 +155,6 @@ export async function renderPage(options: SSROptions): Promise<Response> {
const renderContext = await createRenderContext({ const renderContext = await createRenderContext({
request: options.request, request: options.request,
origin: options.origin,
pathname: options.pathname, pathname: options.pathname,
scripts, scripts,
links, links,

View file

@ -3,8 +3,6 @@ import type {
AstroGlobal, AstroGlobal,
AstroGlobalPartial, AstroGlobalPartial,
Params, Params,
Props,
RuntimeMode,
SSRElement, SSRElement,
SSRLoadedRenderer, SSRLoadedRenderer,
SSRResult, SSRResult,
@ -33,15 +31,12 @@ export interface CreateResultArgs {
*/ */
ssr: boolean; ssr: boolean;
logging: LogOptions; logging: LogOptions;
origin: string;
/** /**
* Used to support `Astro.__renderMarkdown` for legacy `<Markdown />` component * Used to support `Astro.__renderMarkdown` for legacy `<Markdown />` component
*/ */
markdown: MarkdownRenderingOptions; markdown: MarkdownRenderingOptions;
mode: RuntimeMode;
params: Params; params: Params;
pathname: string; pathname: string;
props: Props;
renderers: SSRLoadedRenderer[]; renderers: SSRLoadedRenderer[];
clientDirectives: Map<string, string>; clientDirectives: Map<string, string>;
resolve: (s: string) => Promise<string>; resolve: (s: string) => Promise<string>;
@ -170,9 +165,9 @@ export function createResult(args: CreateResultArgs): SSRResult {
scripts: args.scripts ?? new Set<SSRElement>(), scripts: args.scripts ?? new Set<SSRElement>(),
links: args.links ?? new Set<SSRElement>(), links: args.links ?? new Set<SSRElement>(),
componentMetadata, componentMetadata,
propagators: new Map(), renderers,
extraHead: [], clientDirectives,
scope: 0, pathname,
cookies, cookies,
/** This function returns the `Astro` faux-global */ /** This function returns the `Astro` faux-global */
createAstro( createAstro(
@ -259,16 +254,15 @@ export function createResult(args: CreateResultArgs): SSRResult {
return Astro; return Astro;
}, },
resolve, resolve,
response,
_metadata: { _metadata: {
renderers,
pathname,
hasHydrationScript: false, hasHydrationScript: false,
hasRenderedHead: false, hasRenderedHead: false,
hasDirectives: new Set(), hasDirectives: new Set(),
headInTree: false, headInTree: false,
clientDirectives, extraHead: [],
propagators: new Map(),
}, },
response,
}; };
return result; return result;

View file

@ -26,7 +26,7 @@ interface ExtractedProps {
// Finds these special props and removes them from what gets passed into the component. // Finds these special props and removes them from what gets passed into the component.
export function extractDirectives( export function extractDirectives(
inputProps: Record<string | number | symbol, any>, inputProps: Record<string | number | symbol, any>,
clientDirectives: SSRResult['_metadata']['clientDirectives'] clientDirectives: SSRResult['clientDirectives']
): ExtractedProps { ): ExtractedProps {
let extracted: ExtractedProps = { let extracted: ExtractedProps = {
isPage: false, isPage: false,

View file

@ -76,7 +76,7 @@ async function renderJSXVNode(result: SSRResult, vnode: AstroVNode, skip: Skip):
if (isVNode(vnode)) { if (isVNode(vnode)) {
switch (true) { switch (true) {
case !vnode.type: { case !vnode.type: {
throw new Error(`Unable to render ${result._metadata.pathname} because it contains an undefined Component! throw new Error(`Unable to render ${result.pathname} because it contains an undefined Component!
Did you forget to import the component or is it possible there is a typo?`); Did you forget to import the component or is it possible there is a typo?`);
} }
case (vnode.type as any) === Symbol.for('astro:fragment'): case (vnode.type as any) === Symbol.for('astro:fragment'):

View file

@ -80,8 +80,8 @@ export function createAstroComponentInstance(
) { ) {
validateComponentProps(props, displayName); validateComponentProps(props, displayName);
const instance = new AstroComponentInstance(result, props, slots, factory); const instance = new AstroComponentInstance(result, props, slots, factory);
if (isAPropagatingComponent(result, factory) && !result.propagators.has(factory)) { if (isAPropagatingComponent(result, factory) && !result._metadata.propagators.has(factory)) {
result.propagators.set(factory, instance); result._metadata.propagators.set(factory, instance);
} }
return instance; return instance;
} }

View file

@ -74,7 +74,7 @@ async function renderFrameworkComponent(
); );
} }
const { renderers, clientDirectives } = result._metadata; const { renderers, clientDirectives } = result;
const metadata: AstroComponentMetadata = { const metadata: AstroComponentMetadata = {
astroStaticSlot: true, astroStaticSlot: true,
displayName, displayName,

View file

@ -1,6 +1,7 @@
import type { SSRResult } from '../../../@types/astro'; import type { SSRResult } from '../../../@types/astro';
import { markHTMLString } from '../escape.js'; import { markHTMLString } from '../escape.js';
import type { MaybeRenderHeadInstruction, RenderHeadInstruction } from './types';
import { renderElement } from './util.js'; import { renderElement } from './util.js';
// Filter out duplicate elements in our set // Filter out duplicate elements in our set
@ -34,8 +35,8 @@ export function renderAllHeadContent(result: SSRResult) {
let content = links.join('\n') + styles.join('\n') + scripts.join('\n'); let content = links.join('\n') + styles.join('\n') + scripts.join('\n');
if (result.extraHead.length > 0) { if (result._metadata.extraHead.length > 0) {
for (const part of result.extraHead) { for (const part of result._metadata.extraHead) {
content += part; content += part;
} }
} }
@ -43,20 +44,16 @@ export function renderAllHeadContent(result: SSRResult) {
return markHTMLString(content); return markHTMLString(content);
} }
export function* renderHead(result: SSRResult) { export function* renderHead(): Generator<RenderHeadInstruction> {
yield { type: 'head', result } as const; yield { type: 'head' };
} }
// This function is called by Astro components that do not contain a <head> component // This function is called by Astro components that do not contain a <head> component
// This accommodates the fact that using a <head> is optional in Astro, so this // This accommodates the fact that using a <head> is optional in Astro, so this
// is called before a component's first non-head HTML element. If the head was // is called before a component's first non-head HTML element. If the head was
// already injected it is a noop. // already injected it is a noop.
export function* maybeRenderHead(result: SSRResult) { export function* maybeRenderHead(): Generator<MaybeRenderHeadInstruction> {
if (result._metadata.hasRenderedHead) {
return;
}
// This is an instruction informing the page rendering that head might need rendering. // This is an instruction informing the page rendering that head might need rendering.
// This allows the page to deduplicate head injections. // This allows the page to deduplicate head injections.
yield { type: 'maybe-head', result, scope: result.scope } as const; yield { type: 'maybe-head' };
} }

View file

@ -55,7 +55,7 @@ async function iterableToHTMLBytes(
// Recursively calls component instances that might have head content // Recursively calls component instances that might have head content
// to be propagated up. // to be propagated up.
async function bufferHeadContent(result: SSRResult) { async function bufferHeadContent(result: SSRResult) {
const iterator = result.propagators.values(); const iterator = result._metadata.propagators.values();
while (true) { while (true) {
const { value, done } = iterator.next(); const { value, done } = iterator.next();
if (done) { if (done) {
@ -63,7 +63,7 @@ async function bufferHeadContent(result: SSRResult) {
} }
const returnValue = await value.init(result); const returnValue = await value.init(result);
if (isHeadAndContent(returnValue)) { if (isHeadAndContent(returnValue)) {
result.extraHead.push(returnValue.head); result._metadata.extraHead.push(returnValue.head);
} }
} }
} }
@ -86,7 +86,7 @@ export async function renderPage(
try { try {
if (nonAstroPageNeedsHeadInjection(componentFactory)) { if (nonAstroPageNeedsHeadInjection(componentFactory)) {
const parts = new HTMLParts(); const parts = new HTMLParts();
for await (const chunk of maybeRenderHead(result)) { for await (const chunk of maybeRenderHead()) {
parts.append(chunk, result); parts.append(chunk, result);
} }
head = parts.toString(); head = parts.toString();

View file

@ -1,21 +1,16 @@
import type { SSRResult } from '../../../@types/astro';
import type { HydrationMetadata } from '../hydration.js'; import type { HydrationMetadata } from '../hydration.js';
export type RenderDirectiveInstruction = { export type RenderDirectiveInstruction = {
type: 'directive'; type: 'directive';
result: SSRResult;
hydration: HydrationMetadata; hydration: HydrationMetadata;
}; };
export type RenderHeadInstruction = { export type RenderHeadInstruction = {
type: 'head'; type: 'head';
result: SSRResult;
}; };
export type MaybeRenderHeadInstruction = { export type MaybeRenderHeadInstruction = {
type: 'maybe-head'; type: 'maybe-head';
result: SSRResult;
scope: number;
}; };
export type RenderInstruction = export type RenderInstruction =

View file

@ -21,7 +21,7 @@ export function determinesIfNeedsDirectiveScript(result: SSRResult, directive: s
export type PrescriptType = null | 'both' | 'directive'; export type PrescriptType = null | 'both' | 'directive';
function getDirectiveScriptText(result: SSRResult, directive: string): string { function getDirectiveScriptText(result: SSRResult, directive: string): string {
const clientDirectives = result._metadata.clientDirectives; const clientDirectives = result.clientDirectives;
const clientDirective = clientDirectives.get(directive); const clientDirective = clientDirectives.get(directive);
if (!clientDirective) { if (!clientDirective) {
throw new Error(`Unknown directive: ${directive}`); throw new Error(`Unknown directive: ${directive}`);

View file

@ -165,7 +165,6 @@ export async function handleRoute(
const options: SSROptions = { const options: SSROptions = {
env, env,
filePath, filePath,
origin,
preload: preloadedComponent, preload: preloadedComponent,
pathname, pathname,
request, request,

View file

@ -89,7 +89,10 @@ export const ComponentNode = createComponent({
); );
// Let the runtime know that this component is being used. // Let the runtime know that this component is being used.
result.propagators.set( // `result.propagators` has been moved to `result._metadata.propagators`
// TODO: remove this fallback in the next markdoc integration major
const propagators = result._metadata.propagators || result.propagators;
propagators.set(
{}, {},
{ {
init() { init() {