Refactor Astro rendering to write results directly (#7782)
This commit is contained in:
parent
ec40c8ccbe
commit
0f677c009d
17 changed files with 322 additions and 461 deletions
5
.changeset/lemon-snakes-invite.md
Normal file
5
.changeset/lemon-snakes-invite.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
"astro": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Refactor Astro rendering to write results directly. This improves the rendering performance for all Astro files.
|
|
@ -7,16 +7,12 @@ import type {
|
||||||
SSRLoadedRenderer,
|
SSRLoadedRenderer,
|
||||||
SSRResult,
|
SSRResult,
|
||||||
} from '../../@types/astro';
|
} from '../../@types/astro';
|
||||||
import { isHTMLString } from '../../runtime/server/escape.js';
|
import { renderSlotToString, type ComponentSlots } from '../../runtime/server/index.js';
|
||||||
import {
|
|
||||||
renderSlotToString,
|
|
||||||
stringifyChunk,
|
|
||||||
type ComponentSlots,
|
|
||||||
} from '../../runtime/server/index.js';
|
|
||||||
import { renderJSX } from '../../runtime/server/jsx.js';
|
import { renderJSX } from '../../runtime/server/jsx.js';
|
||||||
import { AstroCookies } from '../cookies/index.js';
|
import { AstroCookies } from '../cookies/index.js';
|
||||||
import { AstroError, AstroErrorData } from '../errors/index.js';
|
import { AstroError, AstroErrorData } from '../errors/index.js';
|
||||||
import { warn, type LogOptions } from '../logger/core.js';
|
import { warn, type LogOptions } from '../logger/core.js';
|
||||||
|
import { chunkToString } from '../../runtime/server/render/index.js';
|
||||||
|
|
||||||
const clientAddressSymbol = Symbol.for('astro.clientAddress');
|
const clientAddressSymbol = Symbol.for('astro.clientAddress');
|
||||||
const responseSentSymbol = Symbol.for('astro.responseSent');
|
const responseSentSymbol = Symbol.for('astro.responseSent');
|
||||||
|
@ -112,7 +108,7 @@ class Slots {
|
||||||
const expression = getFunctionExpression(component);
|
const expression = getFunctionExpression(component);
|
||||||
if (expression) {
|
if (expression) {
|
||||||
const slot = async () =>
|
const slot = async () =>
|
||||||
isHTMLString(await expression) ? expression : expression(...args);
|
typeof expression === 'function' ? expression(...args) : expression;
|
||||||
return await renderSlotToString(result, slot).then((res) => {
|
return await renderSlotToString(result, slot).then((res) => {
|
||||||
return res != null ? String(res) : res;
|
return res != null ? String(res) : res;
|
||||||
});
|
});
|
||||||
|
@ -126,7 +122,7 @@ class Slots {
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = await renderSlotToString(result, this.#slots[name]);
|
const content = await renderSlotToString(result, this.#slots[name]);
|
||||||
const outHTML = stringifyChunk(result, content);
|
const outHTML = chunkToString(result, content);
|
||||||
|
|
||||||
return outHTML;
|
return outHTML;
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,9 +17,7 @@ export {
|
||||||
Fragment,
|
Fragment,
|
||||||
maybeRenderHead,
|
maybeRenderHead,
|
||||||
renderTemplate as render,
|
renderTemplate as render,
|
||||||
renderAstroTemplateResult as renderAstroComponent,
|
|
||||||
renderComponent,
|
renderComponent,
|
||||||
renderComponentToIterable,
|
|
||||||
Renderer as Renderer,
|
Renderer as Renderer,
|
||||||
renderHead,
|
renderHead,
|
||||||
renderHTMLElement,
|
renderHTMLElement,
|
||||||
|
@ -30,7 +28,6 @@ export {
|
||||||
renderTemplate,
|
renderTemplate,
|
||||||
renderToString,
|
renderToString,
|
||||||
renderUniqueStylesheet,
|
renderUniqueStylesheet,
|
||||||
stringifyChunk,
|
|
||||||
voidElementNames,
|
voidElementNames,
|
||||||
} from './render/index.js';
|
} from './render/index.js';
|
||||||
export type {
|
export type {
|
||||||
|
|
|
@ -5,13 +5,11 @@ import {
|
||||||
HTMLString,
|
HTMLString,
|
||||||
escapeHTML,
|
escapeHTML,
|
||||||
markHTMLString,
|
markHTMLString,
|
||||||
renderComponentToIterable,
|
|
||||||
renderToString,
|
renderToString,
|
||||||
spreadAttributes,
|
spreadAttributes,
|
||||||
voidElementNames,
|
voidElementNames,
|
||||||
} from './index.js';
|
} from './index.js';
|
||||||
import { HTMLParts } from './render/common.js';
|
import { renderComponentToString } from './render/component.js';
|
||||||
import type { ComponentIterable } from './render/component';
|
|
||||||
|
|
||||||
const ClientOnlyPlaceholder = 'astro-client-only';
|
const ClientOnlyPlaceholder = 'astro-client-only';
|
||||||
|
|
||||||
|
@ -177,9 +175,9 @@ Did you forget to import the component or is it possible there is a typo?`);
|
||||||
await Promise.all(slotPromises);
|
await Promise.all(slotPromises);
|
||||||
|
|
||||||
props[Skip.symbol] = skip;
|
props[Skip.symbol] = skip;
|
||||||
let output: ComponentIterable;
|
let output: string;
|
||||||
if (vnode.type === ClientOnlyPlaceholder && vnode.props['client:only']) {
|
if (vnode.type === ClientOnlyPlaceholder && vnode.props['client:only']) {
|
||||||
output = await renderComponentToIterable(
|
output = await renderComponentToString(
|
||||||
result,
|
result,
|
||||||
vnode.props['client:display-name'] ?? '',
|
vnode.props['client:display-name'] ?? '',
|
||||||
null,
|
null,
|
||||||
|
@ -187,7 +185,7 @@ Did you forget to import the component or is it possible there is a typo?`);
|
||||||
slots
|
slots
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
output = await renderComponentToIterable(
|
output = await renderComponentToString(
|
||||||
result,
|
result,
|
||||||
typeof vnode.type === 'function' ? vnode.type.name : vnode.type,
|
typeof vnode.type === 'function' ? vnode.type.name : vnode.type,
|
||||||
vnode.type,
|
vnode.type,
|
||||||
|
@ -195,15 +193,7 @@ Did you forget to import the component or is it possible there is a typo?`);
|
||||||
slots
|
slots
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (typeof output !== 'string' && Symbol.asyncIterator in output) {
|
return markHTMLString(output);
|
||||||
let parts = new HTMLParts();
|
|
||||||
for await (const chunk of output) {
|
|
||||||
parts.append(chunk, result);
|
|
||||||
}
|
|
||||||
return markHTMLString(parts.toString());
|
|
||||||
} else {
|
|
||||||
return markHTMLString(output);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// numbers, plain objects, etc
|
// numbers, plain objects, etc
|
||||||
|
|
|
@ -1,47 +1,43 @@
|
||||||
import { escapeHTML, isHTMLString, markHTMLString } from '../escape.js';
|
import { escapeHTML, isHTMLString, markHTMLString } from '../escape.js';
|
||||||
import {
|
import { isAstroComponentInstance, isRenderTemplateResult } from './astro/index.js';
|
||||||
isAstroComponentInstance,
|
import { isRenderInstance, type RenderDestination } from './common.js';
|
||||||
isRenderTemplateResult,
|
|
||||||
renderAstroTemplateResult,
|
|
||||||
} from './astro/index.js';
|
|
||||||
import { SlotString } from './slot.js';
|
import { SlotString } from './slot.js';
|
||||||
import { bufferIterators } from './util.js';
|
|
||||||
|
|
||||||
export async function* renderChild(child: any): AsyncIterable<any> {
|
export async function renderChild(destination: RenderDestination, child: any) {
|
||||||
child = await child;
|
child = await child;
|
||||||
if (child instanceof SlotString) {
|
if (child instanceof SlotString) {
|
||||||
if (child.instructions) {
|
destination.write(child);
|
||||||
yield* child.instructions;
|
|
||||||
}
|
|
||||||
yield child;
|
|
||||||
} else if (isHTMLString(child)) {
|
} else if (isHTMLString(child)) {
|
||||||
yield child;
|
destination.write(child);
|
||||||
} else if (Array.isArray(child)) {
|
} else if (Array.isArray(child)) {
|
||||||
const bufferedIterators = bufferIterators(child.map((c) => renderChild(c)));
|
for (const c of child) {
|
||||||
for (const value of bufferedIterators) {
|
await renderChild(destination, c);
|
||||||
yield markHTMLString(await value);
|
|
||||||
}
|
}
|
||||||
} else if (typeof child === 'function') {
|
} else if (typeof child === 'function') {
|
||||||
// Special: If a child is a function, call it automatically.
|
// Special: If a child is a function, call it automatically.
|
||||||
// This lets you do {() => ...} without the extra boilerplate
|
// This lets you do {() => ...} without the extra boilerplate
|
||||||
// of wrapping it in a function and calling it.
|
// of wrapping it in a function and calling it.
|
||||||
yield* renderChild(child());
|
await renderChild(destination, child());
|
||||||
} else if (typeof child === 'string') {
|
} else if (typeof child === 'string') {
|
||||||
yield markHTMLString(escapeHTML(child));
|
destination.write(markHTMLString(escapeHTML(child)));
|
||||||
} else if (!child && child !== 0) {
|
} else if (!child && child !== 0) {
|
||||||
// do nothing, safe to ignore falsey values.
|
// do nothing, safe to ignore falsey values.
|
||||||
|
} else if (isRenderInstance(child)) {
|
||||||
|
await child.render(destination);
|
||||||
} else if (isRenderTemplateResult(child)) {
|
} else if (isRenderTemplateResult(child)) {
|
||||||
yield* renderAstroTemplateResult(child);
|
await child.render(destination);
|
||||||
} else if (isAstroComponentInstance(child)) {
|
} else if (isAstroComponentInstance(child)) {
|
||||||
yield* child.render();
|
await child.render(destination);
|
||||||
} else if (ArrayBuffer.isView(child)) {
|
} else if (ArrayBuffer.isView(child)) {
|
||||||
yield child;
|
destination.write(child);
|
||||||
} else if (
|
} else if (
|
||||||
typeof child === 'object' &&
|
typeof child === 'object' &&
|
||||||
(Symbol.asyncIterator in child || Symbol.iterator in child)
|
(Symbol.asyncIterator in child || Symbol.iterator in child)
|
||||||
) {
|
) {
|
||||||
yield* child;
|
for await (const value of child) {
|
||||||
|
await renderChild(destination, value);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
yield child;
|
destination.write(child);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,5 @@ export { isAstroComponentFactory } from './factory.js';
|
||||||
export { createHeadAndContent, isHeadAndContent } from './head-and-content.js';
|
export { createHeadAndContent, isHeadAndContent } from './head-and-content.js';
|
||||||
export type { AstroComponentInstance } from './instance';
|
export type { AstroComponentInstance } from './instance';
|
||||||
export { createAstroComponentInstance, isAstroComponentInstance } from './instance.js';
|
export { createAstroComponentInstance, isAstroComponentInstance } from './instance.js';
|
||||||
export {
|
export { isRenderTemplateResult, renderTemplate } from './render-template.js';
|
||||||
isRenderTemplateResult,
|
|
||||||
renderAstroTemplateResult,
|
|
||||||
renderTemplate,
|
|
||||||
} from './render-template.js';
|
|
||||||
export { renderToReadableStream, renderToString } from './render.js';
|
export { renderToReadableStream, renderToString } from './render.js';
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { isPromise } from '../../util.js';
|
||||||
import { renderChild } from '../any.js';
|
import { renderChild } from '../any.js';
|
||||||
import { isAPropagatingComponent } from './factory.js';
|
import { isAPropagatingComponent } from './factory.js';
|
||||||
import { isHeadAndContent } from './head-and-content.js';
|
import { isHeadAndContent } from './head-and-content.js';
|
||||||
|
import type { RenderDestination } from '../common.js';
|
||||||
|
|
||||||
type ComponentProps = Record<string | number, any>;
|
type ComponentProps = Record<string | number, any>;
|
||||||
|
|
||||||
|
@ -40,7 +41,7 @@ export class AstroComponentInstance {
|
||||||
return this.returnValue;
|
return this.returnValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
async *render() {
|
async render(destination: RenderDestination) {
|
||||||
if (this.returnValue === undefined) {
|
if (this.returnValue === undefined) {
|
||||||
await this.init(this.result);
|
await this.init(this.result);
|
||||||
}
|
}
|
||||||
|
@ -50,9 +51,9 @@ export class AstroComponentInstance {
|
||||||
value = await value;
|
value = await value;
|
||||||
}
|
}
|
||||||
if (isHeadAndContent(value)) {
|
if (isHeadAndContent(value)) {
|
||||||
yield* value.content;
|
await value.content.render(destination);
|
||||||
} else {
|
} else {
|
||||||
yield* renderChild(value);
|
await renderChild(destination, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -71,7 +72,7 @@ function validateComponentProps(props: any, displayName: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createAstroComponentInstance(
|
export async function createAstroComponentInstance(
|
||||||
result: SSRResult,
|
result: SSRResult,
|
||||||
displayName: string,
|
displayName: string,
|
||||||
factory: AstroComponentFactory,
|
factory: AstroComponentFactory,
|
||||||
|
@ -80,9 +81,16 @@ 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._metadata.propagators.has(factory)) {
|
if (isAPropagatingComponent(result, factory) && !result._metadata.propagators.has(factory)) {
|
||||||
result._metadata.propagators.set(factory, instance);
|
result._metadata.propagators.set(factory, instance);
|
||||||
|
// Call component instances that might have head content to be propagated up.
|
||||||
|
const returnValue = await instance.init(result);
|
||||||
|
if (isHeadAndContent(returnValue)) {
|
||||||
|
result._metadata.extraHead.push(returnValue.head);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import type { RenderInstruction } from '../types';
|
import { markHTMLString } from '../../escape.js';
|
||||||
|
|
||||||
import { HTMLBytes, markHTMLString } from '../../escape.js';
|
|
||||||
import { isPromise } from '../../util.js';
|
import { isPromise } from '../../util.js';
|
||||||
import { renderChild } from '../any.js';
|
import { renderChild } from '../any.js';
|
||||||
import { bufferIterators } from '../util.js';
|
import type { RenderDestination } from '../common.js';
|
||||||
|
|
||||||
const renderTemplateResultSym = Symbol.for('astro.renderTemplateResult');
|
const renderTemplateResultSym = Symbol.for('astro.renderTemplateResult');
|
||||||
|
|
||||||
|
@ -33,17 +31,15 @@ export class RenderTemplateResult {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async *[Symbol.asyncIterator]() {
|
async render(destination: RenderDestination) {
|
||||||
const { htmlParts, expressions } = this;
|
for (let i = 0; i < this.htmlParts.length; i++) {
|
||||||
|
const html = this.htmlParts[i];
|
||||||
|
const exp = this.expressions[i];
|
||||||
|
|
||||||
let iterables = bufferIterators(expressions.map((e) => renderChild(e)));
|
destination.write(markHTMLString(html));
|
||||||
for (let i = 0; i < htmlParts.length; i++) {
|
// Skip render if falsy, except the number 0
|
||||||
const html = htmlParts[i];
|
if (exp || exp === 0) {
|
||||||
const iterable = iterables[i];
|
await renderChild(destination, exp);
|
||||||
|
|
||||||
yield markHTMLString(html);
|
|
||||||
if (iterable) {
|
|
||||||
yield* iterable;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,27 +50,6 @@ export function isRenderTemplateResult(obj: unknown): obj is RenderTemplateResul
|
||||||
return typeof obj === 'object' && !!(obj as any)[renderTemplateResultSym];
|
return typeof obj === 'object' && !!(obj as any)[renderTemplateResultSym];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function* renderAstroTemplateResult(
|
|
||||||
component: RenderTemplateResult
|
|
||||||
): AsyncIterable<string | HTMLBytes | RenderInstruction> {
|
|
||||||
for await (const value of component) {
|
|
||||||
if (value || value === 0) {
|
|
||||||
for await (const chunk of renderChild(value)) {
|
|
||||||
switch (chunk.type) {
|
|
||||||
case 'directive': {
|
|
||||||
yield chunk;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
yield markHTMLString(chunk);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function renderTemplate(htmlParts: TemplateStringsArray, ...expressions: any[]) {
|
export function renderTemplate(htmlParts: TemplateStringsArray, ...expressions: any[]) {
|
||||||
return new RenderTemplateResult(htmlParts, expressions);
|
return new RenderTemplateResult(htmlParts, expressions);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { AstroError, AstroErrorData } from '../../../../core/errors/index.js';
|
||||||
import { chunkToByteArray, chunkToString, encoder, type RenderDestination } from '../common.js';
|
import { chunkToByteArray, chunkToString, encoder, type RenderDestination } from '../common.js';
|
||||||
import type { AstroComponentFactory } from './factory.js';
|
import type { AstroComponentFactory } from './factory.js';
|
||||||
import { isHeadAndContent } from './head-and-content.js';
|
import { isHeadAndContent } from './head-and-content.js';
|
||||||
import { isRenderTemplateResult, renderAstroTemplateResult } from './render-template.js';
|
import { isRenderTemplateResult } from './render-template.js';
|
||||||
|
|
||||||
// Calls a component and renders it into a string of HTML
|
// Calls a component and renders it into a string of HTML
|
||||||
export async function renderToString(
|
export async function renderToString(
|
||||||
|
@ -46,9 +46,7 @@ export async function renderToString(
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
for await (const chunk of renderAstroTemplateResult(templateResult)) {
|
await templateResult.render(destination);
|
||||||
destination.write(chunk);
|
|
||||||
}
|
|
||||||
|
|
||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
@ -73,10 +71,6 @@ export async function renderToReadableStream(
|
||||||
// If the Astro component returns a Response on init, return that response
|
// If the Astro component returns a Response on init, return that response
|
||||||
if (templateResult instanceof Response) return templateResult;
|
if (templateResult instanceof Response) return templateResult;
|
||||||
|
|
||||||
if (isPage) {
|
|
||||||
await bufferHeadContent(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
let renderedFirstPageChunk = false;
|
let renderedFirstPageChunk = false;
|
||||||
|
|
||||||
return new ReadableStream({
|
return new ReadableStream({
|
||||||
|
@ -108,9 +102,7 @@ export async function renderToReadableStream(
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
for await (const chunk of renderAstroTemplateResult(templateResult)) {
|
await templateResult.render(destination);
|
||||||
destination.write(chunk);
|
|
||||||
}
|
|
||||||
controller.close();
|
controller.close();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// We don't have a lot of information downstream, and upstream we can't catch the error properly
|
// We don't have a lot of information downstream, and upstream we can't catch the error properly
|
||||||
|
@ -120,7 +112,9 @@ export async function renderToReadableStream(
|
||||||
file: route?.component,
|
file: route?.component,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
controller.error(e);
|
|
||||||
|
// Queue error on next microtask to flush the remaining chunks written synchronously
|
||||||
|
setTimeout(() => controller.error(e), 0);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
},
|
},
|
||||||
|
@ -150,19 +144,3 @@ async function callComponentAsTemplateResultOrResponse(
|
||||||
|
|
||||||
return isHeadAndContent(factoryResult) ? factoryResult.content : factoryResult;
|
return isHeadAndContent(factoryResult) ? factoryResult.content : factoryResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recursively calls component instances that might have head content
|
|
||||||
// to be propagated up.
|
|
||||||
async function bufferHeadContent(result: SSRResult) {
|
|
||||||
const iterator = result._metadata.propagators.values();
|
|
||||||
while (true) {
|
|
||||||
const { value, done } = iterator.next();
|
|
||||||
if (done) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
const returnValue = await value.init(result);
|
|
||||||
if (isHeadAndContent(returnValue)) {
|
|
||||||
result._metadata.extraHead.push(returnValue.head);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import type { SSRResult } from '../../../@types/astro';
|
import type { SSRResult } from '../../../@types/astro';
|
||||||
import type { RenderInstruction } from './types.js';
|
import type { RenderInstruction } from './types.js';
|
||||||
|
|
||||||
import { HTMLBytes, markHTMLString } from '../escape.js';
|
import { HTMLBytes, HTMLString, markHTMLString } from '../escape.js';
|
||||||
import {
|
import {
|
||||||
determineIfNeedsHydrationScript,
|
determineIfNeedsHydrationScript,
|
||||||
determinesIfNeedsDirectiveScript,
|
determinesIfNeedsDirectiveScript,
|
||||||
|
@ -11,12 +11,32 @@ import {
|
||||||
import { renderAllHeadContent } from './head.js';
|
import { renderAllHeadContent } from './head.js';
|
||||||
import { isSlotString, type SlotString } from './slot.js';
|
import { isSlotString, type SlotString } from './slot.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Possible chunk types to be written to the destination, and it'll
|
||||||
|
* handle stringifying them at the end.
|
||||||
|
*
|
||||||
|
* NOTE: Try to reduce adding new types here. If possible, serialize
|
||||||
|
* the custom types to a string in `renderChild` in `any.ts`.
|
||||||
|
*/
|
||||||
|
export type RenderDestinationChunk =
|
||||||
|
| string
|
||||||
|
| HTMLBytes
|
||||||
|
| HTMLString
|
||||||
|
| SlotString
|
||||||
|
| ArrayBufferView
|
||||||
|
| RenderInstruction
|
||||||
|
| Response;
|
||||||
|
|
||||||
export interface RenderDestination {
|
export interface RenderDestination {
|
||||||
/**
|
/**
|
||||||
* Any rendering logic should call this to construct the HTML output.
|
* Any rendering logic should call this to construct the HTML output.
|
||||||
* See the `chunk` parameter for possible writable values
|
* See the `chunk` parameter for possible writable values.
|
||||||
*/
|
*/
|
||||||
write(chunk: string | HTMLBytes | RenderInstruction | Response): void;
|
write(chunk: RenderDestinationChunk): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RenderInstance {
|
||||||
|
render(destination: RenderDestination): Promise<void> | void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Fragment = Symbol.for('astro:fragment');
|
export const Fragment = Symbol.for('astro:fragment');
|
||||||
|
@ -28,9 +48,9 @@ export const decoder = new TextDecoder();
|
||||||
// Rendering produces either marked strings of HTML or instructions for hydration.
|
// Rendering produces either marked strings of HTML or instructions for hydration.
|
||||||
// These directive instructions bubble all the way up to renderPage so that we
|
// These directive instructions bubble all the way up to renderPage so that we
|
||||||
// can ensure they are added only once, and as soon as possible.
|
// can ensure they are added only once, and as soon as possible.
|
||||||
export function stringifyChunk(
|
function stringifyChunk(
|
||||||
result: SSRResult,
|
result: SSRResult,
|
||||||
chunk: string | SlotString | RenderInstruction
|
chunk: string | HTMLString | SlotString | RenderInstruction
|
||||||
): string {
|
): string {
|
||||||
if (typeof (chunk as any).type === 'string') {
|
if (typeof (chunk as any).type === 'string') {
|
||||||
const instruction = chunk as RenderInstruction;
|
const instruction = chunk as RenderInstruction;
|
||||||
|
@ -89,27 +109,7 @@ export function stringifyChunk(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class HTMLParts {
|
export function chunkToString(result: SSRResult, chunk: Exclude<RenderDestinationChunk, Response>) {
|
||||||
public parts: string;
|
|
||||||
constructor() {
|
|
||||||
this.parts = '';
|
|
||||||
}
|
|
||||||
append(part: string | HTMLBytes | RenderInstruction, result: SSRResult) {
|
|
||||||
if (ArrayBuffer.isView(part)) {
|
|
||||||
this.parts += decoder.decode(part);
|
|
||||||
} else {
|
|
||||||
this.parts += stringifyChunk(result, part);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
toString() {
|
|
||||||
return this.parts;
|
|
||||||
}
|
|
||||||
toArrayBuffer() {
|
|
||||||
return encoder.encode(this.parts);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function chunkToString(result: SSRResult, chunk: string | HTMLBytes | RenderInstruction) {
|
|
||||||
if (ArrayBuffer.isView(chunk)) {
|
if (ArrayBuffer.isView(chunk)) {
|
||||||
return decoder.decode(chunk);
|
return decoder.decode(chunk);
|
||||||
} else {
|
} else {
|
||||||
|
@ -119,7 +119,7 @@ export function chunkToString(result: SSRResult, chunk: string | HTMLBytes | Ren
|
||||||
|
|
||||||
export function chunkToByteArray(
|
export function chunkToByteArray(
|
||||||
result: SSRResult,
|
result: SSRResult,
|
||||||
chunk: string | HTMLBytes | RenderInstruction
|
chunk: Exclude<RenderDestinationChunk, Response>
|
||||||
): Uint8Array {
|
): Uint8Array {
|
||||||
if (ArrayBuffer.isView(chunk)) {
|
if (ArrayBuffer.isView(chunk)) {
|
||||||
return chunk as Uint8Array;
|
return chunk as Uint8Array;
|
||||||
|
@ -129,3 +129,7 @@ export function chunkToByteArray(
|
||||||
return encoder.encode(stringified.toString());
|
return encoder.encode(stringified.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isRenderInstance(obj: unknown): obj is RenderInstance {
|
||||||
|
return !!obj && typeof obj === 'object' && 'render' in obj && typeof obj.render === 'function';
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
import type { AstroComponentMetadata, SSRLoadedRenderer, SSRResult } from '../../../@types/astro';
|
import type {
|
||||||
|
AstroComponentMetadata,
|
||||||
|
RouteData,
|
||||||
|
SSRLoadedRenderer,
|
||||||
|
SSRResult,
|
||||||
|
} from '../../../@types/astro';
|
||||||
import type { RenderInstruction } from './types.js';
|
import type { RenderInstruction } from './types.js';
|
||||||
|
|
||||||
import { AstroError, AstroErrorData } from '../../../core/errors/index.js';
|
import { AstroError, AstroErrorData } from '../../../core/errors/index.js';
|
||||||
|
@ -10,16 +15,23 @@ import { isPromise } from '../util.js';
|
||||||
import {
|
import {
|
||||||
createAstroComponentInstance,
|
createAstroComponentInstance,
|
||||||
isAstroComponentFactory,
|
isAstroComponentFactory,
|
||||||
isAstroComponentInstance,
|
|
||||||
renderAstroTemplateResult,
|
|
||||||
renderTemplate,
|
renderTemplate,
|
||||||
type AstroComponentInstance,
|
type AstroComponentFactory,
|
||||||
} from './astro/index.js';
|
} from './astro/index.js';
|
||||||
import { Fragment, Renderer, stringifyChunk } from './common.js';
|
import {
|
||||||
|
Fragment,
|
||||||
|
Renderer,
|
||||||
|
type RenderDestination,
|
||||||
|
chunkToString,
|
||||||
|
type RenderInstance,
|
||||||
|
type RenderDestinationChunk,
|
||||||
|
} from './common.js';
|
||||||
import { componentIsHTMLElement, renderHTMLElement } from './dom.js';
|
import { componentIsHTMLElement, renderHTMLElement } from './dom.js';
|
||||||
import { renderSlotToString, renderSlots, type ComponentSlots } from './slot.js';
|
import { renderSlotToString, renderSlots, type ComponentSlots } from './slot.js';
|
||||||
import { formatList, internalSpreadAttributes, renderElement, voidElementNames } from './util.js';
|
import { formatList, internalSpreadAttributes, renderElement, voidElementNames } from './util.js';
|
||||||
|
import { maybeRenderHead } from './head.js';
|
||||||
|
|
||||||
|
const needsHeadRenderingSymbol = Symbol.for('astro.needsHeadRendering');
|
||||||
const rendererAliases = new Map([['solid', 'solid-js']]);
|
const rendererAliases = new Map([['solid', 'solid-js']]);
|
||||||
|
|
||||||
function guessRenderers(componentUrl?: string): string[] {
|
function guessRenderers(componentUrl?: string): string[] {
|
||||||
|
@ -67,7 +79,7 @@ async function renderFrameworkComponent(
|
||||||
Component: unknown,
|
Component: unknown,
|
||||||
_props: Record<string | number, any>,
|
_props: Record<string | number, any>,
|
||||||
slots: any = {}
|
slots: any = {}
|
||||||
): Promise<ComponentIterable> {
|
): Promise<RenderInstance> {
|
||||||
if (!Component && !_props['client:only']) {
|
if (!Component && !_props['client:only']) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unable to render ${displayName} because it is ${Component}!\nDid you forget to import the component or is it possible there is a typo?`
|
`Unable to render ${displayName} because it is ${Component}!\nDid you forget to import the component or is it possible there is a typo?`
|
||||||
|
@ -134,9 +146,17 @@ async function renderFrameworkComponent(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!renderer && typeof HTMLElement === 'function' && componentIsHTMLElement(Component)) {
|
if (!renderer && typeof HTMLElement === 'function' && componentIsHTMLElement(Component)) {
|
||||||
const output = renderHTMLElement(result, Component as typeof HTMLElement, _props, slots);
|
const output = await renderHTMLElement(
|
||||||
|
result,
|
||||||
return output;
|
Component as typeof HTMLElement,
|
||||||
|
_props,
|
||||||
|
slots
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
render(destination) {
|
||||||
|
destination.write(output);
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Attempt: use explicitly passed renderer name
|
// Attempt: use explicitly passed renderer name
|
||||||
|
@ -253,33 +273,43 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
|
||||||
// Sanitize tag name because some people might try to inject attributes 🙄
|
// Sanitize tag name because some people might try to inject attributes 🙄
|
||||||
const Tag = sanitizeElementName(Component);
|
const Tag = sanitizeElementName(Component);
|
||||||
const childSlots = Object.values(children).join('');
|
const childSlots = Object.values(children).join('');
|
||||||
const iterable = renderAstroTemplateResult(
|
|
||||||
await renderTemplate`<${Tag}${internalSpreadAttributes(props)}${markHTMLString(
|
const renderTemplateResult = renderTemplate`<${Tag}${internalSpreadAttributes(
|
||||||
childSlots === '' && voidElementNames.test(Tag) ? `/>` : `>${childSlots}</${Tag}>`
|
props
|
||||||
)}`
|
)}${markHTMLString(
|
||||||
);
|
childSlots === '' && voidElementNames.test(Tag) ? `/>` : `>${childSlots}</${Tag}>`
|
||||||
|
)}`;
|
||||||
|
|
||||||
html = '';
|
html = '';
|
||||||
for await (const chunk of iterable) {
|
const destination: RenderDestination = {
|
||||||
html += chunk;
|
write(chunk) {
|
||||||
}
|
if (chunk instanceof Response) return;
|
||||||
|
html += chunkToString(result, chunk);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await renderTemplateResult.render(destination);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hydration) {
|
if (!hydration) {
|
||||||
return (async function* () {
|
return {
|
||||||
if (slotInstructions) {
|
render(destination) {
|
||||||
yield* slotInstructions;
|
// If no hydration is needed, start rendering the html and return
|
||||||
}
|
if (slotInstructions) {
|
||||||
|
for (const instruction of slotInstructions) {
|
||||||
if (isPage || renderer?.name === 'astro:jsx') {
|
destination.write(instruction);
|
||||||
yield html;
|
}
|
||||||
} else if (html && html.length > 0) {
|
}
|
||||||
yield markHTMLString(
|
if (isPage || renderer?.name === 'astro:jsx') {
|
||||||
removeStaticAstroSlot(html, renderer?.ssr?.supportsAstroStaticSlot ?? false)
|
destination.write(html);
|
||||||
);
|
} else if (html && html.length > 0) {
|
||||||
} else {
|
destination.write(
|
||||||
yield '';
|
markHTMLString(
|
||||||
}
|
removeStaticAstroSlot(html, renderer?.ssr?.supportsAstroStaticSlot ?? false)
|
||||||
})();
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Include componentExport name, componentUrl, and props in hash to dedupe identical islands
|
// Include componentExport name, componentUrl, and props in hash to dedupe identical islands
|
||||||
|
@ -332,15 +362,18 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
|
||||||
island.props['await-children'] = '';
|
island.props['await-children'] = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function* renderAll() {
|
return {
|
||||||
if (slotInstructions) {
|
render(destination) {
|
||||||
yield* slotInstructions;
|
// Render the html
|
||||||
}
|
if (slotInstructions) {
|
||||||
yield { type: 'directive', hydration, result };
|
for (const instruction of slotInstructions) {
|
||||||
yield markHTMLString(renderElement('astro-island', island, false));
|
destination.write(instruction);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return renderAll();
|
destination.write({ type: 'directive', hydration });
|
||||||
|
destination.write(markHTMLString(renderElement('astro-island', island, false)));
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function sanitizeElementName(tag: string) {
|
function sanitizeElementName(tag: string) {
|
||||||
|
@ -349,12 +382,17 @@ function sanitizeElementName(tag: string) {
|
||||||
return tag.trim().split(unsafe)[0].trim();
|
return tag.trim().split(unsafe)[0].trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderFragmentComponent(result: SSRResult, slots: ComponentSlots = {}) {
|
async function renderFragmentComponent(
|
||||||
|
result: SSRResult,
|
||||||
|
slots: ComponentSlots = {}
|
||||||
|
): Promise<RenderInstance> {
|
||||||
const children = await renderSlotToString(result, slots?.default);
|
const children = await renderSlotToString(result, slots?.default);
|
||||||
if (children == null) {
|
return {
|
||||||
return children;
|
render(destination) {
|
||||||
}
|
if (children == null) return;
|
||||||
return markHTMLString(children);
|
destination.write(children);
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderHTMLComponent(
|
async function renderHTMLComponent(
|
||||||
|
@ -362,54 +400,136 @@ async function renderHTMLComponent(
|
||||||
Component: unknown,
|
Component: unknown,
|
||||||
_props: Record<string | number, any>,
|
_props: Record<string | number, any>,
|
||||||
slots: any = {}
|
slots: any = {}
|
||||||
) {
|
): Promise<RenderInstance> {
|
||||||
const { slotInstructions, children } = await renderSlots(result, slots);
|
const { slotInstructions, children } = await renderSlots(result, slots);
|
||||||
const html = (Component as any)({ slots: children });
|
const html = (Component as any)({ slots: children });
|
||||||
const hydrationHtml = slotInstructions
|
const hydrationHtml = slotInstructions
|
||||||
? slotInstructions.map((instr) => stringifyChunk(result, instr)).join('')
|
? slotInstructions.map((instr) => chunkToString(result, instr)).join('')
|
||||||
: '';
|
: '';
|
||||||
return markHTMLString(hydrationHtml + html);
|
return {
|
||||||
|
render(destination) {
|
||||||
|
destination.write(markHTMLString(hydrationHtml + html));
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderComponent(
|
async function renderAstroComponent(
|
||||||
|
result: SSRResult,
|
||||||
|
displayName: string,
|
||||||
|
Component: AstroComponentFactory,
|
||||||
|
props: Record<string | number, any>,
|
||||||
|
slots: any = {}
|
||||||
|
): Promise<RenderInstance> {
|
||||||
|
const instance = await createAstroComponentInstance(result, displayName, Component, props, slots);
|
||||||
|
|
||||||
|
// Eagerly render the component so they are rendered in parallel
|
||||||
|
const chunks: RenderDestinationChunk[] = [];
|
||||||
|
const temporaryDestination: RenderDestination = {
|
||||||
|
write: (chunk) => chunks.push(chunk),
|
||||||
|
};
|
||||||
|
await instance.render(temporaryDestination);
|
||||||
|
|
||||||
|
return {
|
||||||
|
render(destination) {
|
||||||
|
// The real render function will simply pass on the results from the temporary destination
|
||||||
|
for (const chunk of chunks) {
|
||||||
|
destination.write(chunk);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function renderComponent(
|
||||||
result: SSRResult,
|
result: SSRResult,
|
||||||
displayName: string,
|
displayName: string,
|
||||||
Component: unknown,
|
Component: unknown,
|
||||||
props: Record<string | number, any>,
|
props: Record<string | number, any>,
|
||||||
slots: any = {}
|
slots: any = {}
|
||||||
): Promise<ComponentIterable> | ComponentIterable | AstroComponentInstance {
|
): Promise<RenderInstance> {
|
||||||
if (isPromise(Component)) {
|
if (isPromise(Component)) {
|
||||||
return Promise.resolve(Component).then((Unwrapped) => {
|
Component = await Component;
|
||||||
return renderComponent(result, displayName, Unwrapped, props, slots) as any;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isFragmentComponent(Component)) {
|
if (isFragmentComponent(Component)) {
|
||||||
return renderFragmentComponent(result, slots);
|
return await renderFragmentComponent(result, slots);
|
||||||
}
|
}
|
||||||
|
|
||||||
// .html components
|
// .html components
|
||||||
if (isHTMLComponent(Component)) {
|
if (isHTMLComponent(Component)) {
|
||||||
return renderHTMLComponent(result, Component, props, slots);
|
return await renderHTMLComponent(result, Component, props, slots);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isAstroComponentFactory(Component)) {
|
if (isAstroComponentFactory(Component)) {
|
||||||
return createAstroComponentInstance(result, displayName, Component, props, slots);
|
return await renderAstroComponent(result, displayName, Component, props, slots);
|
||||||
}
|
}
|
||||||
|
|
||||||
return renderFrameworkComponent(result, displayName, Component, props, slots);
|
return await renderFrameworkComponent(result, displayName, Component, props, slots);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderComponentToIterable(
|
export async function renderComponentToString(
|
||||||
result: SSRResult,
|
result: SSRResult,
|
||||||
displayName: string,
|
displayName: string,
|
||||||
Component: unknown,
|
Component: unknown,
|
||||||
props: Record<string | number, any>,
|
props: Record<string | number, any>,
|
||||||
slots: any = {}
|
slots: any = {},
|
||||||
): Promise<ComponentIterable> | ComponentIterable {
|
isPage = false,
|
||||||
const renderResult = renderComponent(result, displayName, Component, props, slots);
|
route?: RouteData
|
||||||
if (isAstroComponentInstance(renderResult)) {
|
): Promise<string> {
|
||||||
return renderResult.render();
|
let str = '';
|
||||||
|
let renderedFirstPageChunk = false;
|
||||||
|
|
||||||
|
// Handle head injection if required. Note that this needs to run early so
|
||||||
|
// we can ensure getting a value for `head`.
|
||||||
|
let head = '';
|
||||||
|
if (nonAstroPageNeedsHeadInjection(Component)) {
|
||||||
|
for (const headChunk of maybeRenderHead()) {
|
||||||
|
head += chunkToString(result, headChunk);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return renderResult;
|
|
||||||
|
try {
|
||||||
|
const destination: RenderDestination = {
|
||||||
|
write(chunk) {
|
||||||
|
// Automatic doctype and head insertion for pages
|
||||||
|
if (isPage && !renderedFirstPageChunk) {
|
||||||
|
renderedFirstPageChunk = true;
|
||||||
|
if (!/<!doctype html/i.test(String(chunk))) {
|
||||||
|
const doctype = result.compressHTML ? '<!DOCTYPE html>' : '<!DOCTYPE html>\n';
|
||||||
|
str += doctype + head;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// `renderToString` doesn't work with emitting responses, so ignore here
|
||||||
|
if (chunk instanceof Response) return;
|
||||||
|
|
||||||
|
str += chunkToString(result, chunk);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderInstance = await renderComponent(result, displayName, Component, props, slots);
|
||||||
|
await renderInstance.render(destination);
|
||||||
|
} catch (e) {
|
||||||
|
// We don't have a lot of information downstream, and upstream we can't catch the error properly
|
||||||
|
// So let's add the location here
|
||||||
|
if (AstroError.is(e) && !e.loc) {
|
||||||
|
e.setLocation({
|
||||||
|
file: route?.component,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NonAstroPageComponent = {
|
||||||
|
name: string;
|
||||||
|
[needsHeadRenderingSymbol]: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function nonAstroPageNeedsHeadInjection(
|
||||||
|
pageComponent: any
|
||||||
|
): pageComponent is NonAstroPageComponent {
|
||||||
|
return !!pageComponent?.[needsHeadRenderingSymbol];
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ export async function renderHTMLElement(
|
||||||
constructor: typeof HTMLElement,
|
constructor: typeof HTMLElement,
|
||||||
props: any,
|
props: any,
|
||||||
slots: any
|
slots: any
|
||||||
) {
|
): Promise<string> {
|
||||||
const name = getHTMLElementName(constructor);
|
const name = getHTMLElementName(constructor);
|
||||||
|
|
||||||
let attrHTML = '';
|
let attrHTML = '';
|
||||||
|
|
|
@ -1,12 +1,7 @@
|
||||||
export type { AstroComponentFactory, AstroComponentInstance } from './astro/index';
|
export type { AstroComponentFactory, AstroComponentInstance } from './astro/index';
|
||||||
export {
|
export { createHeadAndContent, renderTemplate, renderToString } from './astro/index.js';
|
||||||
createHeadAndContent,
|
export { Fragment, Renderer, chunkToString, chunkToByteArray } from './common.js';
|
||||||
renderAstroTemplateResult,
|
export { renderComponent, renderComponentToString } from './component.js';
|
||||||
renderTemplate,
|
|
||||||
renderToString,
|
|
||||||
} from './astro/index.js';
|
|
||||||
export { Fragment, Renderer, stringifyChunk } from './common.js';
|
|
||||||
export { renderComponent, renderComponentToIterable } from './component.js';
|
|
||||||
export { renderHTMLElement } from './dom.js';
|
export { renderHTMLElement } from './dom.js';
|
||||||
export { maybeRenderHead, renderHead } from './head.js';
|
export { maybeRenderHead, renderHead } from './head.js';
|
||||||
export { renderPage } from './page.js';
|
export { renderPage } from './page.js';
|
||||||
|
|
|
@ -1,50 +1,11 @@
|
||||||
import type { RouteData, SSRResult } from '../../../@types/astro';
|
import type { RouteData, SSRResult } from '../../../@types/astro';
|
||||||
import type { ComponentIterable } from './component';
|
import { renderComponentToString, type NonAstroPageComponent } from './component.js';
|
||||||
import type { AstroComponentFactory } from './index';
|
import type { AstroComponentFactory } from './index';
|
||||||
|
|
||||||
import { AstroError } from '../../../core/errors/index.js';
|
|
||||||
import { isHTMLString } from '../escape.js';
|
|
||||||
import { createResponse } from '../response.js';
|
import { createResponse } from '../response.js';
|
||||||
import { isAstroComponentFactory, isAstroComponentInstance } from './astro/index.js';
|
import { isAstroComponentFactory } from './astro/index.js';
|
||||||
import { renderToReadableStream, renderToString } from './astro/render.js';
|
import { renderToReadableStream, renderToString } from './astro/render.js';
|
||||||
import { HTMLParts, encoder } from './common.js';
|
import { encoder } from './common.js';
|
||||||
import { renderComponent } from './component.js';
|
|
||||||
import { maybeRenderHead } from './head.js';
|
|
||||||
|
|
||||||
const needsHeadRenderingSymbol = Symbol.for('astro.needsHeadRendering');
|
|
||||||
|
|
||||||
type NonAstroPageComponent = {
|
|
||||||
name: string;
|
|
||||||
[needsHeadRenderingSymbol]: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
function nonAstroPageNeedsHeadInjection(pageComponent: NonAstroPageComponent): boolean {
|
|
||||||
return needsHeadRenderingSymbol in pageComponent && !!pageComponent[needsHeadRenderingSymbol];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function iterableToHTMLBytes(
|
|
||||||
result: SSRResult,
|
|
||||||
iterable: ComponentIterable,
|
|
||||||
onDocTypeInjection?: (parts: HTMLParts) => Promise<void>
|
|
||||||
): Promise<Uint8Array> {
|
|
||||||
const parts = new HTMLParts();
|
|
||||||
let i = 0;
|
|
||||||
for await (const chunk of iterable) {
|
|
||||||
if (isHTMLString(chunk)) {
|
|
||||||
if (i === 0) {
|
|
||||||
i++;
|
|
||||||
if (!/<!doctype html/i.test(String(chunk))) {
|
|
||||||
parts.append(`${result.compressHTML ? '<!DOCTYPE html>' : '<!DOCTYPE html>\n'}`, result);
|
|
||||||
if (onDocTypeInjection) {
|
|
||||||
await onDocTypeInjection(parts);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
parts.append(chunk, result);
|
|
||||||
}
|
|
||||||
return parts.toArrayBuffer();
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function renderPage(
|
export async function renderPage(
|
||||||
result: SSRResult,
|
result: SSRResult,
|
||||||
|
@ -52,49 +13,25 @@ export async function renderPage(
|
||||||
props: any,
|
props: any,
|
||||||
children: any,
|
children: any,
|
||||||
streaming: boolean,
|
streaming: boolean,
|
||||||
route?: RouteData | undefined
|
route?: RouteData
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
if (!isAstroComponentFactory(componentFactory)) {
|
if (!isAstroComponentFactory(componentFactory)) {
|
||||||
result._metadata.headInTree =
|
result._metadata.headInTree =
|
||||||
result.componentMetadata.get((componentFactory as any).moduleId)?.containsHead ?? false;
|
result.componentMetadata.get((componentFactory as any).moduleId)?.containsHead ?? false;
|
||||||
|
|
||||||
const pageProps: Record<string, any> = { ...(props ?? {}), 'server:root': true };
|
const pageProps: Record<string, any> = { ...(props ?? {}), 'server:root': true };
|
||||||
let output: ComponentIterable;
|
|
||||||
let head = '';
|
|
||||||
try {
|
|
||||||
if (nonAstroPageNeedsHeadInjection(componentFactory)) {
|
|
||||||
const parts = new HTMLParts();
|
|
||||||
for await (const chunk of maybeRenderHead()) {
|
|
||||||
parts.append(chunk, result);
|
|
||||||
}
|
|
||||||
head = parts.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderResult = await renderComponent(
|
const str = await renderComponentToString(
|
||||||
result,
|
result,
|
||||||
componentFactory.name,
|
componentFactory.name,
|
||||||
componentFactory,
|
componentFactory,
|
||||||
pageProps,
|
pageProps,
|
||||||
null
|
null,
|
||||||
);
|
true,
|
||||||
if (isAstroComponentInstance(renderResult)) {
|
route
|
||||||
output = renderResult.render();
|
);
|
||||||
} else {
|
|
||||||
output = renderResult;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
if (AstroError.is(e) && !e.loc) {
|
|
||||||
e.setLocation({
|
|
||||||
file: route?.component,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
throw e;
|
const bytes = encoder.encode(str);
|
||||||
}
|
|
||||||
|
|
||||||
// Accumulate the HTML string and append the head if necessary.
|
|
||||||
const bytes = await iterableToHTMLBytes(result, output, async (parts) => {
|
|
||||||
parts.append(head, result);
|
|
||||||
});
|
|
||||||
|
|
||||||
return new Response(bytes, {
|
return new Response(bytes, {
|
||||||
headers: new Headers([
|
headers: new Headers([
|
||||||
|
@ -103,6 +40,7 @@ export async function renderPage(
|
||||||
]),
|
]),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark if this page component contains a <head> within its tree. If it does
|
// Mark if this page component contains a <head> within its tree. If it does
|
||||||
// We avoid implicit head injection entirely.
|
// We avoid implicit head injection entirely.
|
||||||
result._metadata.headInTree =
|
result._metadata.headInTree =
|
||||||
|
|
|
@ -4,6 +4,7 @@ import type { RenderInstruction } from './types.js';
|
||||||
|
|
||||||
import { HTMLString, markHTMLString } from '../escape.js';
|
import { HTMLString, markHTMLString } from '../escape.js';
|
||||||
import { renderChild } from './any.js';
|
import { renderChild } from './any.js';
|
||||||
|
import { chunkToString, type RenderDestination, type RenderInstance } from './common.js';
|
||||||
|
|
||||||
type RenderTemplateResult = ReturnType<typeof renderTemplate>;
|
type RenderTemplateResult = ReturnType<typeof renderTemplate>;
|
||||||
export type ComponentSlots = Record<string, ComponentSlotValue>;
|
export type ComponentSlots = Record<string, ComponentSlotValue>;
|
||||||
|
@ -27,19 +28,19 @@ export function isSlotString(str: string): str is any {
|
||||||
return !!(str as any)[slotString];
|
return !!(str as any)[slotString];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function* renderSlot(
|
export function renderSlot(
|
||||||
result: SSRResult,
|
result: SSRResult,
|
||||||
slotted: ComponentSlotValue | RenderTemplateResult,
|
slotted: ComponentSlotValue | RenderTemplateResult,
|
||||||
fallback?: ComponentSlotValue | RenderTemplateResult
|
fallback?: ComponentSlotValue | RenderTemplateResult
|
||||||
): AsyncGenerator<any, void, undefined> {
|
): RenderInstance {
|
||||||
if (slotted) {
|
if (!slotted && fallback) {
|
||||||
let iterator = renderChild(typeof slotted === 'function' ? slotted(result) : slotted);
|
return renderSlot(result, fallback);
|
||||||
yield* iterator;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fallback && !slotted) {
|
|
||||||
yield* renderSlot(result, fallback);
|
|
||||||
}
|
}
|
||||||
|
return {
|
||||||
|
async render(destination) {
|
||||||
|
await renderChild(destination, typeof slotted === 'function' ? slotted(result) : slotted);
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function renderSlotToString(
|
export async function renderSlotToString(
|
||||||
|
@ -49,17 +50,21 @@ export async function renderSlotToString(
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
let content = '';
|
let content = '';
|
||||||
let instructions: null | RenderInstruction[] = null;
|
let instructions: null | RenderInstruction[] = null;
|
||||||
let iterator = renderSlot(result, slotted, fallback);
|
const temporaryDestination: RenderDestination = {
|
||||||
for await (const chunk of iterator) {
|
write(chunk) {
|
||||||
if (typeof chunk.type === 'string') {
|
if (chunk instanceof Response) return;
|
||||||
if (instructions === null) {
|
if (typeof chunk === 'object' && 'type' in chunk && typeof chunk.type === 'string') {
|
||||||
instructions = [];
|
if (instructions === null) {
|
||||||
|
instructions = [];
|
||||||
|
}
|
||||||
|
instructions.push(chunk);
|
||||||
|
} else {
|
||||||
|
content += chunkToString(result, chunk);
|
||||||
}
|
}
|
||||||
instructions.push(chunk);
|
},
|
||||||
} else {
|
};
|
||||||
content += chunk;
|
const renderInstance = renderSlot(result, slotted, fallback);
|
||||||
}
|
await renderInstance.render(temporaryDestination);
|
||||||
}
|
|
||||||
return markHTMLString(new SlotString(content, instructions));
|
return markHTMLString(new SlotString(content, instructions));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -145,145 +145,3 @@ export function renderElement(
|
||||||
}
|
}
|
||||||
return `<${name}${internalSpreadAttributes(props, shouldEscape)}>${children}</${name}>`;
|
return `<${name}${internalSpreadAttributes(props, shouldEscape)}>${children}</${name}>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const iteratorQueue: EagerAsyncIterableIterator[][] = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Takes an array of iterators and adds them to a list of iterators to start buffering
|
|
||||||
* as soon as the execution flow is suspended for the first time. We expect a lot
|
|
||||||
* of calls to this function before the first suspension, so to reduce the number
|
|
||||||
* of calls to setTimeout we batch the buffering calls.
|
|
||||||
* @param iterators
|
|
||||||
*/
|
|
||||||
function queueIteratorBuffers(iterators: EagerAsyncIterableIterator[]) {
|
|
||||||
if (iteratorQueue.length === 0) {
|
|
||||||
setTimeout(() => {
|
|
||||||
// buffer all iterators that haven't started yet
|
|
||||||
iteratorQueue.forEach((its) => its.forEach((it) => !it.isStarted() && it.buffer()));
|
|
||||||
iteratorQueue.length = 0; // fastest way to empty an array
|
|
||||||
});
|
|
||||||
}
|
|
||||||
iteratorQueue.push(iterators);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This will take an array of async iterables and start buffering them eagerly.
|
|
||||||
* To avoid useless buffering, it will only start buffering the next tick, so the
|
|
||||||
* first sync iterables won't be buffered.
|
|
||||||
*/
|
|
||||||
export function bufferIterators<T>(iterators: AsyncIterable<T>[]): AsyncIterable<T>[] {
|
|
||||||
// all async iterators start running in non-buffered mode to avoid useless caching
|
|
||||||
const eagerIterators = iterators.map((it) => new EagerAsyncIterableIterator(it));
|
|
||||||
// once the execution of the next for loop is suspended due to an async component,
|
|
||||||
// this timeout triggers and we start buffering the other iterators
|
|
||||||
queueIteratorBuffers(eagerIterators);
|
|
||||||
return eagerIterators;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This wrapper around an AsyncIterable can eagerly consume its values, so that
|
|
||||||
// its values are ready to yield out ASAP. This is used for list-like usage of
|
|
||||||
// Astro components, so that we don't have to wait on earlier components to run
|
|
||||||
// to even start running those down in the list.
|
|
||||||
export class EagerAsyncIterableIterator {
|
|
||||||
#iterable: AsyncIterable<any>;
|
|
||||||
#queue = new Queue<IteratorResult<any, any>>();
|
|
||||||
#error: any = undefined;
|
|
||||||
#next: Promise<IteratorResult<any, any>> | undefined;
|
|
||||||
/**
|
|
||||||
* Whether the proxy is running in buffering or pass-through mode
|
|
||||||
*/
|
|
||||||
#isBuffering = false;
|
|
||||||
#gen: AsyncIterator<any> | undefined = undefined;
|
|
||||||
#isStarted = false;
|
|
||||||
|
|
||||||
constructor(iterable: AsyncIterable<any>) {
|
|
||||||
this.#iterable = iterable;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts to eagerly fetch the inner iterator and cache the results.
|
|
||||||
* Note: This might not be called after next() has been called once, e.g. the iterator is started
|
|
||||||
*/
|
|
||||||
async buffer() {
|
|
||||||
if (this.#gen) {
|
|
||||||
// If this called as part of rendering, please open a bug report.
|
|
||||||
// Any call to buffer() should verify that the iterator isn't running
|
|
||||||
throw new Error('Cannot not switch from non-buffer to buffer mode');
|
|
||||||
}
|
|
||||||
this.#isBuffering = true;
|
|
||||||
this.#isStarted = true;
|
|
||||||
this.#gen = this.#iterable[Symbol.asyncIterator]();
|
|
||||||
let value: IteratorResult<any, any> | undefined = undefined;
|
|
||||||
do {
|
|
||||||
this.#next = this.#gen.next();
|
|
||||||
try {
|
|
||||||
value = await this.#next;
|
|
||||||
this.#queue.push(value);
|
|
||||||
} catch (e) {
|
|
||||||
this.#error = e;
|
|
||||||
}
|
|
||||||
} while (value && !value.done);
|
|
||||||
}
|
|
||||||
|
|
||||||
async next() {
|
|
||||||
if (this.#error) {
|
|
||||||
throw this.#error;
|
|
||||||
}
|
|
||||||
// for non-buffered mode, just pass through the next result
|
|
||||||
if (!this.#isBuffering) {
|
|
||||||
if (!this.#gen) {
|
|
||||||
this.#isStarted = true;
|
|
||||||
this.#gen = this.#iterable[Symbol.asyncIterator]();
|
|
||||||
}
|
|
||||||
return await this.#gen.next();
|
|
||||||
}
|
|
||||||
if (!this.#queue.isEmpty()) {
|
|
||||||
return this.#queue.shift()!;
|
|
||||||
}
|
|
||||||
await this.#next;
|
|
||||||
// the previous statement will either put an element in the queue or throw,
|
|
||||||
// so we can safely assume we have something now
|
|
||||||
return this.#queue.shift()!;
|
|
||||||
}
|
|
||||||
|
|
||||||
isStarted() {
|
|
||||||
return this.#isStarted;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Symbol.asyncIterator]() {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface QueueItem<T> {
|
|
||||||
item: T;
|
|
||||||
next?: QueueItem<T>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Basis Queue implementation with a linked list
|
|
||||||
*/
|
|
||||||
class Queue<T> {
|
|
||||||
head: QueueItem<T> | undefined = undefined;
|
|
||||||
tail: QueueItem<T> | undefined = undefined;
|
|
||||||
|
|
||||||
push(item: T) {
|
|
||||||
if (this.head === undefined) {
|
|
||||||
this.head = { item };
|
|
||||||
this.tail = this.head;
|
|
||||||
} else {
|
|
||||||
this.tail!.next = { item };
|
|
||||||
this.tail = this.tail!.next;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isEmpty() {
|
|
||||||
return this.head === undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
shift(): T | undefined {
|
|
||||||
const val = this.head?.item;
|
|
||||||
this.head = this.head?.next;
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -48,7 +48,7 @@ describe('Streaming', () => {
|
||||||
let chunk = decoder.decode(bytes);
|
let chunk = decoder.decode(bytes);
|
||||||
chunks.push(chunk);
|
chunks.push(chunk);
|
||||||
}
|
}
|
||||||
expect(chunks.length).to.equal(3);
|
expect(chunks.length).to.equal(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue