Refactor SSRResult
and RenderContext
(#7575)
This commit is contained in:
parent
44e56bb3bc
commit
30d04db981
19 changed files with 59 additions and 77 deletions
5
.changeset/many-nails-arrive.md
Normal file
5
.changeset/many-nails-arrive.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@astrojs/markdoc': patch
|
||||
---
|
||||
|
||||
Handle internal access change
|
|
@ -1957,16 +1957,6 @@ export interface SSRElement {
|
|||
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
|
||||
* content. The meanings:
|
||||
|
@ -1989,9 +1979,6 @@ export interface SSRResult {
|
|||
scripts: Set<SSRElement>;
|
||||
links: Set<SSRElement>;
|
||||
componentMetadata: Map<string, SSRComponentMetadata>;
|
||||
propagators: Map<AstroComponentFactory, AstroComponentInstance>;
|
||||
extraHead: Array<string>;
|
||||
cookies: AstroCookies | undefined;
|
||||
createAstro(
|
||||
Astro: AstroGlobalPartial,
|
||||
props: Record<string, any>,
|
||||
|
@ -1999,13 +1986,32 @@ export interface SSRResult {
|
|||
): AstroGlobal;
|
||||
resolve: (s: string) => Promise<string>;
|
||||
response: ResponseInit;
|
||||
// Bits 1 = astro, 2 = jsx, 4 = slot
|
||||
// As rendering occurs these bits are manipulated to determine where content
|
||||
// is within a slot. This is used for head injection.
|
||||
scope: number;
|
||||
renderers: SSRLoadedRenderer[];
|
||||
/**
|
||||
* Map of directive name (e.g. `load`) to the directive script code
|
||||
*/
|
||||
clientDirectives: Map<string, string>;
|
||||
/**
|
||||
* Only used for logging
|
||||
*/
|
||||
pathname: string;
|
||||
cookies: AstroCookies | undefined;
|
||||
_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 */
|
||||
export interface PreviewServer {
|
||||
host?: string;
|
||||
|
|
|
@ -227,7 +227,6 @@ export class App {
|
|||
const mod = (await page.page()) as any;
|
||||
const renderContext = await createRenderContext({
|
||||
request,
|
||||
origin: url.origin,
|
||||
pathname,
|
||||
componentMetadata: this.#manifest.componentMetadata,
|
||||
scripts,
|
||||
|
@ -295,7 +294,6 @@ export class App {
|
|||
|
||||
const ctx = await createRenderContext({
|
||||
request,
|
||||
origin: url.origin,
|
||||
pathname,
|
||||
route: routeData,
|
||||
status,
|
||||
|
|
|
@ -539,7 +539,6 @@ async function generatePath(
|
|||
});
|
||||
|
||||
const renderContext = await createRenderContext({
|
||||
origin,
|
||||
pathname,
|
||||
request: createRequest({ url, headers: new Headers(), logging, ssr }),
|
||||
componentMetadata: manifest.componentMetadata,
|
||||
|
|
|
@ -10,7 +10,6 @@ export async function call(options: SSROptions, logging: LogOptions) {
|
|||
|
||||
const ctx = await createRenderContext({
|
||||
request: options.request,
|
||||
origin: options.origin,
|
||||
pathname: options.pathname,
|
||||
route: options.route,
|
||||
env,
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import type {
|
||||
AstroCookies,
|
||||
ComponentInstance,
|
||||
Params,
|
||||
Props,
|
||||
|
@ -18,23 +17,21 @@ const clientLocalsSymbol = Symbol.for('astro.locals');
|
|||
*/
|
||||
export interface RenderContext {
|
||||
request: Request;
|
||||
origin: string;
|
||||
pathname: string;
|
||||
url: URL;
|
||||
scripts?: Set<SSRElement>;
|
||||
links?: Set<SSRElement>;
|
||||
styles?: Set<SSRElement>;
|
||||
componentMetadata?: SSRResult['componentMetadata'];
|
||||
route?: RouteData;
|
||||
status?: number;
|
||||
cookies?: AstroCookies;
|
||||
params: Params;
|
||||
props: Props;
|
||||
locals?: object;
|
||||
}
|
||||
|
||||
export type CreateRenderContextArgs = Partial<RenderContext> & {
|
||||
origin?: string;
|
||||
export type CreateRenderContextArgs = Partial<
|
||||
Omit<RenderContext, 'params' | 'props' | 'locals'>
|
||||
> & {
|
||||
request: RenderContext['request'];
|
||||
mod: ComponentInstance;
|
||||
env: Environment;
|
||||
|
@ -44,9 +41,7 @@ export async function createRenderContext(
|
|||
options: CreateRenderContextArgs
|
||||
): Promise<RenderContext> {
|
||||
const request = options.request;
|
||||
const url = new URL(request.url);
|
||||
const origin = options.origin ?? url.origin;
|
||||
const pathname = options.pathname ?? url.pathname;
|
||||
const pathname = options.pathname ?? new URL(request.url).pathname;
|
||||
const [params, props] = await getParamsAndProps({
|
||||
mod: options.mod as any,
|
||||
route: options.route,
|
||||
|
@ -56,11 +51,9 @@ export async function createRenderContext(
|
|||
ssr: options.env.ssr,
|
||||
});
|
||||
|
||||
let context = {
|
||||
const context: RenderContext = {
|
||||
...options,
|
||||
origin,
|
||||
pathname,
|
||||
url,
|
||||
params,
|
||||
props,
|
||||
};
|
||||
|
|
|
@ -41,10 +41,7 @@ export async function renderPage({
|
|||
styles: renderContext.styles,
|
||||
logging: env.logging,
|
||||
markdown: env.markdown,
|
||||
mode: env.mode,
|
||||
origin: renderContext.origin,
|
||||
params: renderContext.params,
|
||||
props: renderContext.props,
|
||||
pathname: renderContext.pathname,
|
||||
componentMetadata: renderContext.componentMetadata,
|
||||
resolve: env.resolve,
|
||||
|
|
|
@ -24,8 +24,6 @@ export interface SSROptions {
|
|||
env: DevelopmentEnvironment;
|
||||
/** location of file on disk */
|
||||
filePath: URL;
|
||||
/** production website */
|
||||
origin: string;
|
||||
/** the web request (needed for dynamic routes) */
|
||||
pathname: string;
|
||||
/** The runtime component instance */
|
||||
|
@ -157,7 +155,6 @@ export async function renderPage(options: SSROptions): Promise<Response> {
|
|||
|
||||
const renderContext = await createRenderContext({
|
||||
request: options.request,
|
||||
origin: options.origin,
|
||||
pathname: options.pathname,
|
||||
scripts,
|
||||
links,
|
||||
|
|
|
@ -3,8 +3,6 @@ import type {
|
|||
AstroGlobal,
|
||||
AstroGlobalPartial,
|
||||
Params,
|
||||
Props,
|
||||
RuntimeMode,
|
||||
SSRElement,
|
||||
SSRLoadedRenderer,
|
||||
SSRResult,
|
||||
|
@ -33,15 +31,12 @@ export interface CreateResultArgs {
|
|||
*/
|
||||
ssr: boolean;
|
||||
logging: LogOptions;
|
||||
origin: string;
|
||||
/**
|
||||
* Used to support `Astro.__renderMarkdown` for legacy `<Markdown />` component
|
||||
*/
|
||||
markdown: MarkdownRenderingOptions;
|
||||
mode: RuntimeMode;
|
||||
params: Params;
|
||||
pathname: string;
|
||||
props: Props;
|
||||
renderers: SSRLoadedRenderer[];
|
||||
clientDirectives: Map<string, string>;
|
||||
resolve: (s: string) => Promise<string>;
|
||||
|
@ -170,9 +165,9 @@ export function createResult(args: CreateResultArgs): SSRResult {
|
|||
scripts: args.scripts ?? new Set<SSRElement>(),
|
||||
links: args.links ?? new Set<SSRElement>(),
|
||||
componentMetadata,
|
||||
propagators: new Map(),
|
||||
extraHead: [],
|
||||
scope: 0,
|
||||
renderers,
|
||||
clientDirectives,
|
||||
pathname,
|
||||
cookies,
|
||||
/** This function returns the `Astro` faux-global */
|
||||
createAstro(
|
||||
|
@ -259,16 +254,15 @@ export function createResult(args: CreateResultArgs): SSRResult {
|
|||
return Astro;
|
||||
},
|
||||
resolve,
|
||||
response,
|
||||
_metadata: {
|
||||
renderers,
|
||||
pathname,
|
||||
hasHydrationScript: false,
|
||||
hasRenderedHead: false,
|
||||
hasDirectives: new Set(),
|
||||
headInTree: false,
|
||||
clientDirectives,
|
||||
extraHead: [],
|
||||
propagators: new Map(),
|
||||
},
|
||||
response,
|
||||
};
|
||||
|
||||
return result;
|
||||
|
|
|
@ -26,7 +26,7 @@ interface ExtractedProps {
|
|||
// Finds these special props and removes them from what gets passed into the component.
|
||||
export function extractDirectives(
|
||||
inputProps: Record<string | number | symbol, any>,
|
||||
clientDirectives: SSRResult['_metadata']['clientDirectives']
|
||||
clientDirectives: SSRResult['clientDirectives']
|
||||
): ExtractedProps {
|
||||
let extracted: ExtractedProps = {
|
||||
isPage: false,
|
||||
|
|
|
@ -76,7 +76,7 @@ async function renderJSXVNode(result: SSRResult, vnode: AstroVNode, skip: Skip):
|
|||
if (isVNode(vnode)) {
|
||||
switch (true) {
|
||||
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?`);
|
||||
}
|
||||
case (vnode.type as any) === Symbol.for('astro:fragment'):
|
||||
|
|
|
@ -80,8 +80,8 @@ export function createAstroComponentInstance(
|
|||
) {
|
||||
validateComponentProps(props, displayName);
|
||||
const instance = new AstroComponentInstance(result, props, slots, factory);
|
||||
if (isAPropagatingComponent(result, factory) && !result.propagators.has(factory)) {
|
||||
result.propagators.set(factory, instance);
|
||||
if (isAPropagatingComponent(result, factory) && !result._metadata.propagators.has(factory)) {
|
||||
result._metadata.propagators.set(factory, instance);
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
|
|
@ -74,7 +74,7 @@ async function renderFrameworkComponent(
|
|||
);
|
||||
}
|
||||
|
||||
const { renderers, clientDirectives } = result._metadata;
|
||||
const { renderers, clientDirectives } = result;
|
||||
const metadata: AstroComponentMetadata = {
|
||||
astroStaticSlot: true,
|
||||
displayName,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import type { SSRResult } from '../../../@types/astro';
|
||||
|
||||
import { markHTMLString } from '../escape.js';
|
||||
import type { MaybeRenderHeadInstruction, RenderHeadInstruction } from './types';
|
||||
import { renderElement } from './util.js';
|
||||
|
||||
// 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');
|
||||
|
||||
if (result.extraHead.length > 0) {
|
||||
for (const part of result.extraHead) {
|
||||
if (result._metadata.extraHead.length > 0) {
|
||||
for (const part of result._metadata.extraHead) {
|
||||
content += part;
|
||||
}
|
||||
}
|
||||
|
@ -43,20 +44,16 @@ export function renderAllHeadContent(result: SSRResult) {
|
|||
return markHTMLString(content);
|
||||
}
|
||||
|
||||
export function* renderHead(result: SSRResult) {
|
||||
yield { type: 'head', result } as const;
|
||||
export function* renderHead(): Generator<RenderHeadInstruction> {
|
||||
yield { type: 'head' };
|
||||
}
|
||||
|
||||
// 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
|
||||
// is called before a component's first non-head HTML element. If the head was
|
||||
// already injected it is a noop.
|
||||
export function* maybeRenderHead(result: SSRResult) {
|
||||
if (result._metadata.hasRenderedHead) {
|
||||
return;
|
||||
}
|
||||
|
||||
export function* maybeRenderHead(): Generator<MaybeRenderHeadInstruction> {
|
||||
// This is an instruction informing the page rendering that head might need rendering.
|
||||
// This allows the page to deduplicate head injections.
|
||||
yield { type: 'maybe-head', result, scope: result.scope } as const;
|
||||
yield { type: 'maybe-head' };
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@ async function iterableToHTMLBytes(
|
|||
// Recursively calls component instances that might have head content
|
||||
// to be propagated up.
|
||||
async function bufferHeadContent(result: SSRResult) {
|
||||
const iterator = result.propagators.values();
|
||||
const iterator = result._metadata.propagators.values();
|
||||
while (true) {
|
||||
const { value, done } = iterator.next();
|
||||
if (done) {
|
||||
|
@ -63,7 +63,7 @@ async function bufferHeadContent(result: SSRResult) {
|
|||
}
|
||||
const returnValue = await value.init(result);
|
||||
if (isHeadAndContent(returnValue)) {
|
||||
result.extraHead.push(returnValue.head);
|
||||
result._metadata.extraHead.push(returnValue.head);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -86,7 +86,7 @@ export async function renderPage(
|
|||
try {
|
||||
if (nonAstroPageNeedsHeadInjection(componentFactory)) {
|
||||
const parts = new HTMLParts();
|
||||
for await (const chunk of maybeRenderHead(result)) {
|
||||
for await (const chunk of maybeRenderHead()) {
|
||||
parts.append(chunk, result);
|
||||
}
|
||||
head = parts.toString();
|
||||
|
|
|
@ -1,21 +1,16 @@
|
|||
import type { SSRResult } from '../../../@types/astro';
|
||||
import type { HydrationMetadata } from '../hydration.js';
|
||||
|
||||
export type RenderDirectiveInstruction = {
|
||||
type: 'directive';
|
||||
result: SSRResult;
|
||||
hydration: HydrationMetadata;
|
||||
};
|
||||
|
||||
export type RenderHeadInstruction = {
|
||||
type: 'head';
|
||||
result: SSRResult;
|
||||
};
|
||||
|
||||
export type MaybeRenderHeadInstruction = {
|
||||
type: 'maybe-head';
|
||||
result: SSRResult;
|
||||
scope: number;
|
||||
};
|
||||
|
||||
export type RenderInstruction =
|
||||
|
|
|
@ -21,7 +21,7 @@ export function determinesIfNeedsDirectiveScript(result: SSRResult, directive: s
|
|||
export type PrescriptType = null | 'both' | 'directive';
|
||||
|
||||
function getDirectiveScriptText(result: SSRResult, directive: string): string {
|
||||
const clientDirectives = result._metadata.clientDirectives;
|
||||
const clientDirectives = result.clientDirectives;
|
||||
const clientDirective = clientDirectives.get(directive);
|
||||
if (!clientDirective) {
|
||||
throw new Error(`Unknown directive: ${directive}`);
|
||||
|
|
|
@ -165,7 +165,6 @@ export async function handleRoute(
|
|||
const options: SSROptions = {
|
||||
env,
|
||||
filePath,
|
||||
origin,
|
||||
preload: preloadedComponent,
|
||||
pathname,
|
||||
request,
|
||||
|
|
|
@ -89,7 +89,10 @@ export const ComponentNode = createComponent({
|
|||
);
|
||||
|
||||
// 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() {
|
||||
|
|
Loading…
Reference in a new issue