summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.changeset/lemon-snakes-invite.md5
-rw-r--r--packages/astro/src/core/render/result.ts12
-rw-r--r--packages/astro/src/runtime/server/index.ts3
-rw-r--r--packages/astro/src/runtime/server/jsx.ts20
-rw-r--r--packages/astro/src/runtime/server/render/any.ts40
-rw-r--r--packages/astro/src/runtime/server/render/astro/index.ts6
-rw-r--r--packages/astro/src/runtime/server/render/astro/instance.ts16
-rw-r--r--packages/astro/src/runtime/server/render/astro/render-template.ts45
-rw-r--r--packages/astro/src/runtime/server/render/astro/render.ts34
-rw-r--r--packages/astro/src/runtime/server/render/common.ts58
-rw-r--r--packages/astro/src/runtime/server/render/component.ts250
-rw-r--r--packages/astro/src/runtime/server/render/dom.ts2
-rw-r--r--packages/astro/src/runtime/server/render/index.ts11
-rw-r--r--packages/astro/src/runtime/server/render/page.ts94
-rw-r--r--packages/astro/src/runtime/server/render/slot.ts43
-rw-r--r--packages/astro/src/runtime/server/render/util.ts142
-rw-r--r--packages/astro/test/streaming.test.js2
17 files changed, 322 insertions, 461 deletions
diff --git a/.changeset/lemon-snakes-invite.md b/.changeset/lemon-snakes-invite.md
new file mode 100644
index 000000000..49bb98510
--- /dev/null
+++ b/.changeset/lemon-snakes-invite.md
@@ -0,0 +1,5 @@
+---
+"astro": patch
+---
+
+Refactor Astro rendering to write results directly. This improves the rendering performance for all Astro files.
diff --git a/packages/astro/src/core/render/result.ts b/packages/astro/src/core/render/result.ts
index d86cce348..46bae1128 100644
--- a/packages/astro/src/core/render/result.ts
+++ b/packages/astro/src/core/render/result.ts
@@ -7,16 +7,12 @@ import type {
SSRLoadedRenderer,
SSRResult,
} from '../../@types/astro';
-import { isHTMLString } from '../../runtime/server/escape.js';
-import {
- renderSlotToString,
- stringifyChunk,
- type ComponentSlots,
-} from '../../runtime/server/index.js';
+import { renderSlotToString, type ComponentSlots } from '../../runtime/server/index.js';
import { renderJSX } from '../../runtime/server/jsx.js';
import { AstroCookies } from '../cookies/index.js';
import { AstroError, AstroErrorData } from '../errors/index.js';
import { warn, type LogOptions } from '../logger/core.js';
+import { chunkToString } from '../../runtime/server/render/index.js';
const clientAddressSymbol = Symbol.for('astro.clientAddress');
const responseSentSymbol = Symbol.for('astro.responseSent');
@@ -112,7 +108,7 @@ class Slots {
const expression = getFunctionExpression(component);
if (expression) {
const slot = async () =>
- isHTMLString(await expression) ? expression : expression(...args);
+ typeof expression === 'function' ? expression(...args) : expression;
return await renderSlotToString(result, slot).then((res) => {
return res != null ? String(res) : res;
});
@@ -126,7 +122,7 @@ class Slots {
}
const content = await renderSlotToString(result, this.#slots[name]);
- const outHTML = stringifyChunk(result, content);
+ const outHTML = chunkToString(result, content);
return outHTML;
}
diff --git a/packages/astro/src/runtime/server/index.ts b/packages/astro/src/runtime/server/index.ts
index 1a03a507b..aca260d00 100644
--- a/packages/astro/src/runtime/server/index.ts
+++ b/packages/astro/src/runtime/server/index.ts
@@ -17,9 +17,7 @@ export {
Fragment,
maybeRenderHead,
renderTemplate as render,
- renderAstroTemplateResult as renderAstroComponent,
renderComponent,
- renderComponentToIterable,
Renderer as Renderer,
renderHead,
renderHTMLElement,
@@ -30,7 +28,6 @@ export {
renderTemplate,
renderToString,
renderUniqueStylesheet,
- stringifyChunk,
voidElementNames,
} from './render/index.js';
export type {
diff --git a/packages/astro/src/runtime/server/jsx.ts b/packages/astro/src/runtime/server/jsx.ts
index 48f879b10..d2cb87a61 100644
--- a/packages/astro/src/runtime/server/jsx.ts
+++ b/packages/astro/src/runtime/server/jsx.ts
@@ -5,13 +5,11 @@ import {
HTMLString,
escapeHTML,
markHTMLString,
- renderComponentToIterable,
renderToString,
spreadAttributes,
voidElementNames,
} from './index.js';
-import { HTMLParts } from './render/common.js';
-import type { ComponentIterable } from './render/component';
+import { renderComponentToString } from './render/component.js';
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);
props[Skip.symbol] = skip;
- let output: ComponentIterable;
+ let output: string;
if (vnode.type === ClientOnlyPlaceholder && vnode.props['client:only']) {
- output = await renderComponentToIterable(
+ output = await renderComponentToString(
result,
vnode.props['client:display-name'] ?? '',
null,
@@ -187,7 +185,7 @@ Did you forget to import the component or is it possible there is a typo?`);
slots
);
} else {
- output = await renderComponentToIterable(
+ output = await renderComponentToString(
result,
typeof vnode.type === 'function' ? vnode.type.name : vnode.type,
vnode.type,
@@ -195,15 +193,7 @@ Did you forget to import the component or is it possible there is a typo?`);
slots
);
}
- if (typeof output !== 'string' && Symbol.asyncIterator in output) {
- let parts = new HTMLParts();
- for await (const chunk of output) {
- parts.append(chunk, result);
- }
- return markHTMLString(parts.toString());
- } else {
- return markHTMLString(output);
- }
+ return markHTMLString(output);
}
}
// numbers, plain objects, etc
diff --git a/packages/astro/src/runtime/server/render/any.ts b/packages/astro/src/runtime/server/render/any.ts
index 4ee947ee6..7c181fecb 100644
--- a/packages/astro/src/runtime/server/render/any.ts
+++ b/packages/astro/src/runtime/server/render/any.ts
@@ -1,47 +1,43 @@
import { escapeHTML, isHTMLString, markHTMLString } from '../escape.js';
-import {
- isAstroComponentInstance,
- isRenderTemplateResult,
- renderAstroTemplateResult,
-} from './astro/index.js';
+import { isAstroComponentInstance, isRenderTemplateResult } from './astro/index.js';
+import { isRenderInstance, type RenderDestination } from './common.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;
if (child instanceof SlotString) {
- if (child.instructions) {
- yield* child.instructions;
- }
- yield child;
+ destination.write(child);
} else if (isHTMLString(child)) {
- yield child;
+ destination.write(child);
} else if (Array.isArray(child)) {
- const bufferedIterators = bufferIterators(child.map((c) => renderChild(c)));
- for (const value of bufferedIterators) {
- yield markHTMLString(await value);
+ for (const c of child) {
+ await renderChild(destination, c);
}
} else if (typeof child === 'function') {
// Special: If a child is a function, call it automatically.
// This lets you do {() => ...} without the extra boilerplate
// of wrapping it in a function and calling it.
- yield* renderChild(child());
+ await renderChild(destination, child());
} else if (typeof child === 'string') {
- yield markHTMLString(escapeHTML(child));
+ destination.write(markHTMLString(escapeHTML(child)));
} else if (!child && child !== 0) {
// do nothing, safe to ignore falsey values.
+ } else if (isRenderInstance(child)) {
+ await child.render(destination);
} else if (isRenderTemplateResult(child)) {
- yield* renderAstroTemplateResult(child);
+ await child.render(destination);
} else if (isAstroComponentInstance(child)) {
- yield* child.render();
+ await child.render(destination);
} else if (ArrayBuffer.isView(child)) {
- yield child;
+ destination.write(child);
} else if (
typeof child === 'object' &&
(Symbol.asyncIterator in child || Symbol.iterator in child)
) {
- yield* child;
+ for await (const value of child) {
+ await renderChild(destination, value);
+ }
} else {
- yield child;
+ destination.write(child);
}
}
diff --git a/packages/astro/src/runtime/server/render/astro/index.ts b/packages/astro/src/runtime/server/render/astro/index.ts
index f7d9923ee..d9283b9f9 100644
--- a/packages/astro/src/runtime/server/render/astro/index.ts
+++ b/packages/astro/src/runtime/server/render/astro/index.ts
@@ -3,9 +3,5 @@ export { isAstroComponentFactory } from './factory.js';
export { createHeadAndContent, isHeadAndContent } from './head-and-content.js';
export type { AstroComponentInstance } from './instance';
export { createAstroComponentInstance, isAstroComponentInstance } from './instance.js';
-export {
- isRenderTemplateResult,
- renderAstroTemplateResult,
- renderTemplate,
-} from './render-template.js';
+export { isRenderTemplateResult, renderTemplate } from './render-template.js';
export { renderToReadableStream, renderToString } from './render.js';
diff --git a/packages/astro/src/runtime/server/render/astro/instance.ts b/packages/astro/src/runtime/server/render/astro/instance.ts
index 527d4a8c6..e4df186c6 100644
--- a/packages/astro/src/runtime/server/render/astro/instance.ts
+++ b/packages/astro/src/runtime/server/render/astro/instance.ts
@@ -6,6 +6,7 @@ import { isPromise } from '../../util.js';
import { renderChild } from '../any.js';
import { isAPropagatingComponent } from './factory.js';
import { isHeadAndContent } from './head-and-content.js';
+import type { RenderDestination } from '../common.js';
type ComponentProps = Record<string | number, any>;
@@ -40,7 +41,7 @@ export class AstroComponentInstance {
return this.returnValue;
}
- async *render() {
+ async render(destination: RenderDestination) {
if (this.returnValue === undefined) {
await this.init(this.result);
}
@@ -50,9 +51,9 @@ export class AstroComponentInstance {
value = await value;
}
if (isHeadAndContent(value)) {
- yield* value.content;
+ await value.content.render(destination);
} 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,
displayName: string,
factory: AstroComponentFactory,
@@ -80,9 +81,16 @@ export function createAstroComponentInstance(
) {
validateComponentProps(props, displayName);
const instance = new AstroComponentInstance(result, props, slots, factory);
+
if (isAPropagatingComponent(result, factory) && !result._metadata.propagators.has(factory)) {
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;
}
diff --git a/packages/astro/src/runtime/server/render/astro/render-template.ts b/packages/astro/src/runtime/server/render/astro/render-template.ts
index b0dbabdc1..1d5af33fc 100644
--- a/packages/astro/src/runtime/server/render/astro/render-template.ts
+++ b/packages/astro/src/runtime/server/render/astro/render-template.ts
@@ -1,9 +1,7 @@
-import type { RenderInstruction } from '../types';
-
-import { HTMLBytes, markHTMLString } from '../../escape.js';
+import { markHTMLString } from '../../escape.js';
import { isPromise } from '../../util.js';
import { renderChild } from '../any.js';
-import { bufferIterators } from '../util.js';
+import type { RenderDestination } from '../common.js';
const renderTemplateResultSym = Symbol.for('astro.renderTemplateResult');
@@ -33,17 +31,15 @@ export class RenderTemplateResult {
});
}
- async *[Symbol.asyncIterator]() {
- const { htmlParts, expressions } = this;
-
- let iterables = bufferIterators(expressions.map((e) => renderChild(e)));
- for (let i = 0; i < htmlParts.length; i++) {
- const html = htmlParts[i];
- const iterable = iterables[i];
+ async render(destination: RenderDestination) {
+ for (let i = 0; i < this.htmlParts.length; i++) {
+ const html = this.htmlParts[i];
+ const exp = this.expressions[i];
- yield markHTMLString(html);
- if (iterable) {
- yield* iterable;
+ destination.write(markHTMLString(html));
+ // Skip render if falsy, except the number 0
+ if (exp || exp === 0) {
+ await renderChild(destination, exp);
}
}
}
@@ -54,27 +50,6 @@ export function isRenderTemplateResult(obj: unknown): obj is RenderTemplateResul
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[]) {
return new RenderTemplateResult(htmlParts, expressions);
}
diff --git a/packages/astro/src/runtime/server/render/astro/render.ts b/packages/astro/src/runtime/server/render/astro/render.ts
index 81b4375be..89dc28b75 100644
--- a/packages/astro/src/runtime/server/render/astro/render.ts
+++ b/packages/astro/src/runtime/server/render/astro/render.ts
@@ -3,7 +3,7 @@ import { AstroError, AstroErrorData } from '../../../../core/errors/index.js';
import { chunkToByteArray, chunkToString, encoder, type RenderDestination } from '../common.js';
import type { AstroComponentFactory } from './factory.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
export async function renderToString(
@@ -46,9 +46,7 @@ export async function renderToString(
},
};
- for await (const chunk of renderAstroTemplateResult(templateResult)) {
- destination.write(chunk);
- }
+ await templateResult.render(destination);
return str;
}
@@ -73,10 +71,6 @@ export async function renderToReadableStream(
// If the Astro component returns a Response on init, return that response
if (templateResult instanceof Response) return templateResult;
- if (isPage) {
- await bufferHeadContent(result);
- }
-
let renderedFirstPageChunk = false;
return new ReadableStream({
@@ -108,9 +102,7 @@ export async function renderToReadableStream(
(async () => {
try {
- for await (const chunk of renderAstroTemplateResult(templateResult)) {
- destination.write(chunk);
- }
+ await templateResult.render(destination);
controller.close();
} catch (e) {
// 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,
});
}
- 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;
}
-
-// 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);
- }
- }
-}
diff --git a/packages/astro/src/runtime/server/render/common.ts b/packages/astro/src/runtime/server/render/common.ts
index 206f138cc..48d8143df 100644
--- a/packages/astro/src/runtime/server/render/common.ts
+++ b/packages/astro/src/runtime/server/render/common.ts
@@ -1,7 +1,7 @@
import type { SSRResult } from '../../../@types/astro';
import type { RenderInstruction } from './types.js';
-import { HTMLBytes, markHTMLString } from '../escape.js';
+import { HTMLBytes, HTMLString, markHTMLString } from '../escape.js';
import {
determineIfNeedsHydrationScript,
determinesIfNeedsDirectiveScript,
@@ -11,12 +11,32 @@ import {
import { renderAllHeadContent } from './head.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 {
/**
* 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');
@@ -28,9 +48,9 @@ export const decoder = new TextDecoder();
// Rendering produces either marked strings of HTML or instructions for hydration.
// 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.
-export function stringifyChunk(
+function stringifyChunk(
result: SSRResult,
- chunk: string | SlotString | RenderInstruction
+ chunk: string | HTMLString | SlotString | RenderInstruction
): string {
if (typeof (chunk as any).type === 'string') {
const instruction = chunk as RenderInstruction;
@@ -89,27 +109,7 @@ export function stringifyChunk(
}
}
-export class HTMLParts {
- 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) {
+export function chunkToString(result: SSRResult, chunk: Exclude<RenderDestinationChunk, Response>) {
if (ArrayBuffer.isView(chunk)) {
return decoder.decode(chunk);
} else {
@@ -119,7 +119,7 @@ export function chunkToString(result: SSRResult, chunk: string | HTMLBytes | Ren
export function chunkToByteArray(
result: SSRResult,
- chunk: string | HTMLBytes | RenderInstruction
+ chunk: Exclude<RenderDestinationChunk, Response>
): Uint8Array {
if (ArrayBuffer.isView(chunk)) {
return chunk as Uint8Array;
@@ -129,3 +129,7 @@ export function chunkToByteArray(
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';
+}
diff --git a/packages/astro/src/runtime/server/render/component.ts b/packages/astro/src/runtime/server/render/component.ts
index 4eacafe80..de36b0ac9 100644
--- a/packages/astro/src/runtime/server/render/component.ts
+++ b/packages/astro/src/runtime/server/render/component.ts
@@ -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 { AstroError, AstroErrorData } from '../../../core/errors/index.js';
@@ -10,16 +15,23 @@ import { isPromise } from '../util.js';
import {
createAstroComponentInstance,
isAstroComponentFactory,
- isAstroComponentInstance,
- renderAstroTemplateResult,
renderTemplate,
- type AstroComponentInstance,
+ type AstroComponentFactory,
} 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 { renderSlotToString, renderSlots, type ComponentSlots } from './slot.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']]);
function guessRenderers(componentUrl?: string): string[] {
@@ -67,7 +79,7 @@ async function renderFrameworkComponent(
Component: unknown,
_props: Record<string | number, any>,
slots: any = {}
-): Promise<ComponentIterable> {
+): Promise<RenderInstance> {
if (!Component && !_props['client:only']) {
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?`
@@ -134,9 +146,17 @@ async function renderFrameworkComponent(
}
if (!renderer && typeof HTMLElement === 'function' && componentIsHTMLElement(Component)) {
- const output = renderHTMLElement(result, Component as typeof HTMLElement, _props, slots);
-
- return output;
+ const output = await renderHTMLElement(
+ result,
+ Component as typeof HTMLElement,
+ _props,
+ slots
+ );
+ return {
+ render(destination) {
+ destination.write(output);
+ },
+ };
}
} else {
// 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 🙄
const Tag = sanitizeElementName(Component);
const childSlots = Object.values(children).join('');
- const iterable = renderAstroTemplateResult(
- await renderTemplate`<${Tag}${internalSpreadAttributes(props)}${markHTMLString(
- childSlots === '' && voidElementNames.test(Tag) ? `/>` : `>${childSlots}</${Tag}>`
- )}`
- );
+
+ const renderTemplateResult = renderTemplate`<${Tag}${internalSpreadAttributes(
+ props
+ )}${markHTMLString(
+ childSlots === '' && voidElementNames.test(Tag) ? `/>` : `>${childSlots}</${Tag}>`
+ )}`;
+
html = '';
- for await (const chunk of iterable) {
- html += chunk;
- }
+ const destination: RenderDestination = {
+ write(chunk) {
+ if (chunk instanceof Response) return;
+ html += chunkToString(result, chunk);
+ },
+ };
+ await renderTemplateResult.render(destination);
}
if (!hydration) {
- return (async function* () {
- if (slotInstructions) {
- yield* slotInstructions;
- }
-
- if (isPage || renderer?.name === 'astro:jsx') {
- yield html;
- } else if (html && html.length > 0) {
- yield markHTMLString(
- removeStaticAstroSlot(html, renderer?.ssr?.supportsAstroStaticSlot ?? false)
- );
- } else {
- yield '';
- }
- })();
+ return {
+ render(destination) {
+ // If no hydration is needed, start rendering the html and return
+ if (slotInstructions) {
+ for (const instruction of slotInstructions) {
+ destination.write(instruction);
+ }
+ }
+ if (isPage || renderer?.name === 'astro:jsx') {
+ destination.write(html);
+ } else if (html && html.length > 0) {
+ destination.write(
+ markHTMLString(
+ removeStaticAstroSlot(html, renderer?.ssr?.supportsAstroStaticSlot ?? false)
+ )
+ );
+ }
+ },
+ };
}
// 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'] = '';
}
- async function* renderAll() {
- if (slotInstructions) {
- yield* slotInstructions;
- }
- yield { type: 'directive', hydration, result };
- yield markHTMLString(renderElement('astro-island', island, false));
- }
-
- return renderAll();
+ return {
+ render(destination) {
+ // Render the html
+ if (slotInstructions) {
+ for (const instruction of slotInstructions) {
+ destination.write(instruction);
+ }
+ }
+ destination.write({ type: 'directive', hydration });
+ destination.write(markHTMLString(renderElement('astro-island', island, false)));
+ },
+ };
}
function sanitizeElementName(tag: string) {
@@ -349,12 +382,17 @@ function sanitizeElementName(tag: string) {
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);
- if (children == null) {
- return children;
- }
- return markHTMLString(children);
+ return {
+ render(destination) {
+ if (children == null) return;
+ destination.write(children);
+ },
+ };
}
async function renderHTMLComponent(
@@ -362,54 +400,136 @@ async function renderHTMLComponent(
Component: unknown,
_props: Record<string | number, any>,
slots: any = {}
-) {
+): Promise<RenderInstance> {
const { slotInstructions, children } = await renderSlots(result, slots);
const html = (Component as any)({ slots: children });
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,
displayName: string,
Component: unknown,
props: Record<string | number, any>,
slots: any = {}
-): Promise<ComponentIterable> | ComponentIterable | AstroComponentInstance {
+): Promise<RenderInstance> {
if (isPromise(Component)) {
- return Promise.resolve(Component).then((Unwrapped) => {
- return renderComponent(result, displayName, Unwrapped, props, slots) as any;
- });
+ Component = await Component;
}
if (isFragmentComponent(Component)) {
- return renderFragmentComponent(result, slots);
+ return await renderFragmentComponent(result, slots);
}
// .html components
if (isHTMLComponent(Component)) {
- return renderHTMLComponent(result, Component, props, slots);
+ return await renderHTMLComponent(result, Component, props, slots);
}
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,
displayName: string,
Component: unknown,
props: Record<string | number, any>,
- slots: any = {}
-): Promise<ComponentIterable> | ComponentIterable {
- const renderResult = renderComponent(result, displayName, Component, props, slots);
- if (isAstroComponentInstance(renderResult)) {
- return renderResult.render();
+ slots: any = {},
+ isPage = false,
+ route?: RouteData
+): Promise<string> {
+ 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];
}
diff --git a/packages/astro/src/runtime/server/render/dom.ts b/packages/astro/src/runtime/server/render/dom.ts
index 803f29995..1d0ea192f 100644
--- a/packages/astro/src/runtime/server/render/dom.ts
+++ b/packages/astro/src/runtime/server/render/dom.ts
@@ -13,7 +13,7 @@ export async function renderHTMLElement(
constructor: typeof HTMLElement,
props: any,
slots: any
-) {
+): Promise<string> {
const name = getHTMLElementName(constructor);
let attrHTML = '';
diff --git a/packages/astro/src/runtime/server/render/index.ts b/packages/astro/src/runtime/server/render/index.ts
index d34bdd6c7..8a5376797 100644
--- a/packages/astro/src/runtime/server/render/index.ts
+++ b/packages/astro/src/runtime/server/render/index.ts
@@ -1,12 +1,7 @@
export type { AstroComponentFactory, AstroComponentInstance } from './astro/index';
-export {
- createHeadAndContent,
- renderAstroTemplateResult,
- renderTemplate,
- renderToString,
-} from './astro/index.js';
-export { Fragment, Renderer, stringifyChunk } from './common.js';
-export { renderComponent, renderComponentToIterable } from './component.js';
+export { createHeadAndContent, renderTemplate, renderToString } from './astro/index.js';
+export { Fragment, Renderer, chunkToString, chunkToByteArray } from './common.js';
+export { renderComponent, renderComponentToString } from './component.js';
export { renderHTMLElement } from './dom.js';
export { maybeRenderHead, renderHead } from './head.js';
export { renderPage } from './page.js';
diff --git a/packages/astro/src/runtime/server/render/page.ts b/packages/astro/src/runtime/server/render/page.ts
index 025b0b9e3..cabbe8dae 100644
--- a/packages/astro/src/runtime/server/render/page.ts
+++ b/packages/astro/src/runtime/server/render/page.ts
@@ -1,50 +1,11 @@
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 { AstroError } from '../../../core/errors/index.js';
-import { isHTMLString } from '../escape.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 { HTMLParts, 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();
-}
+import { encoder } from './common.js';
export async function renderPage(
result: SSRResult,
@@ -52,49 +13,25 @@ export async function renderPage(
props: any,
children: any,
streaming: boolean,
- route?: RouteData | undefined
+ route?: RouteData
): Promise<Response> {
if (!isAstroComponentFactory(componentFactory)) {
result._metadata.headInTree =
result.componentMetadata.get((componentFactory as any).moduleId)?.containsHead ?? false;
- 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(
- result,
- componentFactory.name,
- componentFactory,
- pageProps,
- null
- );
- if (isAstroComponentInstance(renderResult)) {
- output = renderResult.render();
- } else {
- output = renderResult;
- }
- } catch (e) {
- if (AstroError.is(e) && !e.loc) {
- e.setLocation({
- file: route?.component,
- });
- }
+ const pageProps: Record<string, any> = { ...(props ?? {}), 'server:root': true };
- throw e;
- }
+ const str = await renderComponentToString(
+ result,
+ componentFactory.name,
+ componentFactory,
+ pageProps,
+ null,
+ true,
+ route
+ );
- // Accumulate the HTML string and append the head if necessary.
- const bytes = await iterableToHTMLBytes(result, output, async (parts) => {
- parts.append(head, result);
- });
+ const bytes = encoder.encode(str);
return new Response(bytes, {
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
// We avoid implicit head injection entirely.
result._metadata.headInTree =
diff --git a/packages/astro/src/runtime/server/render/slot.ts b/packages/astro/src/runtime/server/render/slot.ts
index 152230ba9..daae87a80 100644
--- a/packages/astro/src/runtime/server/render/slot.ts
+++ b/packages/astro/src/runtime/server/render/slot.ts
@@ -4,6 +4,7 @@ import type { RenderInstruction } from './types.js';
import { HTMLString, markHTMLString } from '../escape.js';
import { renderChild } from './any.js';
+import { chunkToString, type RenderDestination, type RenderInstance } from './common.js';
type RenderTemplateResult = ReturnType<typeof renderTemplate>;
export type ComponentSlots = Record<string, ComponentSlotValue>;
@@ -27,19 +28,19 @@ export function isSlotString(str: string): str is any {
return !!(str as any)[slotString];
}
-export async function* renderSlot(
+export function renderSlot(
result: SSRResult,
slotted: ComponentSlotValue | RenderTemplateResult,
fallback?: ComponentSlotValue | RenderTemplateResult
-): AsyncGenerator<any, void, undefined> {
- if (slotted) {
- let iterator = renderChild(typeof slotted === 'function' ? slotted(result) : slotted);
- yield* iterator;
- }
-
- if (fallback && !slotted) {
- yield* renderSlot(result, fallback);
+): RenderInstance {
+ if (!slotted && fallback) {
+ return renderSlot(result, fallback);
}
+ return {
+ async render(destination) {
+ await renderChild(destination, typeof slotted === 'function' ? slotted(result) : slotted);
+ },
+ };
}
export async function renderSlotToString(
@@ -49,17 +50,21 @@ export async function renderSlotToString(
): Promise<string> {
let content = '';
let instructions: null | RenderInstruction[] = null;
- let iterator = renderSlot(result, slotted, fallback);
- for await (const chunk of iterator) {
- if (typeof chunk.type === 'string') {
- if (instructions === null) {
- instructions = [];
+ const temporaryDestination: RenderDestination = {
+ write(chunk) {
+ if (chunk instanceof Response) return;
+ if (typeof chunk === 'object' && 'type' in chunk && typeof chunk.type === 'string') {
+ 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));
}
diff --git a/packages/astro/src/runtime/server/render/util.ts b/packages/astro/src/runtime/server/render/util.ts
index f422f66d5..e007fe6f1 100644
--- a/packages/astro/src/runtime/server/render/util.ts
+++ b/packages/astro/src/runtime/server/render/util.ts
@@ -145,145 +145,3 @@ export function renderElement(
}
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;
- }
-}
diff --git a/packages/astro/test/streaming.test.js b/packages/astro/test/streaming.test.js
index c7b835de1..e3627d7ba 100644
--- a/packages/astro/test/streaming.test.js
+++ b/packages/astro/test/streaming.test.js
@@ -48,7 +48,7 @@ describe('Streaming', () => {
let chunk = decoder.decode(bytes);
chunks.push(chunk);
}
- expect(chunks.length).to.equal(3);
+ expect(chunks.length).to.equal(2);
});
});