UNPKG

astro

Version:

Astro is a modern site builder with web best practices, performance, and DX front-of-mind.

239 lines (238 loc) • 7.42 kB
import { isVNode } from "../../../jsx-runtime/index.js"; import { escapeHTML, HTMLString, isHTMLString, markHTMLString } from "../escape.js"; import { spreadAttributes } from "../index.js"; import { isPromise } from "../util.js"; import { renderJSX } from "../jsx.js"; import { renderChild } from "./any.js"; import { Fragment } from "./common.js"; import { createBufferedRenderer, voidElementNames } from "./util.js"; import { isAstroComponentFactory } from "./astro/factory.js"; import { createAstroComponentInstance } from "./astro/instance.js"; import { isRenderTemplateResult } from "./astro/render-template.js"; import { containsServerDirective, ServerIslandComponent } from "./server-islands.js"; const ClientOnlyPlaceholder = "astro-client-only"; class TemplateFrame { /** The RenderTemplateResult this frame walks. */ templateResult; /** Resume position: the next `htmlParts`/`expressions` index to process. */ cursor; constructor(templateResult) { this.templateResult = templateResult; this.cursor = 0; } storeCursor(index) { this.cursor = index; } } async function renderStreaming(root, result, destination) { const stack = [root]; const openTagCache = /* @__PURE__ */ new Map(); const closeTagCache = /* @__PURE__ */ new Map(); const closeTagFor = (type) => { let tag = closeTagCache.get(type); if (tag === void 0) { tag = new HTMLString(`</${type}>`); closeTagCache.set(type, tag); } return tag; }; let batch = ""; let buffered = false; let firstAsync = null; const tail = []; let tailStatic = ""; const emitStatic = (s) => { if (!s) return; if (buffered) tailStatic += s; else batch += s; }; const flushTailStatic = () => { if (tailStatic) { tail.push(tailStatic); tailStatic = ""; } }; const renderDynamic = (node) => (d) => { if (isVNode(node)) { return renderJSX(result, node).then((out) => renderChild(d, out)); } return renderChild(d, node); }; const handleVNode = (vnode) => { const type = vnode.type; if (!type) { throw new Error( `Unable to render ${result.pathname} because it contains an undefined Component! Did you forget to import the component or is it possible there is a typo?` ); } if (type === Fragment) { stack.push(vnode.props?.children); return; } if (isAstroComponentFactory(type)) { const props = {}; const slots = {}; for (const [key, value] of Object.entries(vnode.props ?? {})) { if (key === "children" || value && typeof value === "object" && value["$$slot"]) { slots[key === "children" ? "default" : key] = () => renderJSX(result, value); } else { props[key] = value; } } const displayName = type.name || "Anonymous"; if (containsServerDirective(props)) { const island = new ServerIslandComponent(result, props, slots, displayName); result._metadata.propagators.add(island); stack.push(island); return; } stack.push(createAstroComponentInstance(result, displayName, type, props, slots)); return; } if (typeof type === "string" && type !== ClientOnlyPlaceholder) { const props = vnode.props; let hasAttrs = false; if (props) { for (const key in props) { if (key !== "children") { hasAttrs = true; break; } } } const children = props?.children; const isVoid = (children == null || children === "") && voidElementNames.test(type); if (!hasAttrs) { const key = isVoid ? `${type}/` : type; let openTag = openTagCache.get(key); if (openTag === void 0) { openTag = isVoid ? `<${type}/>` : `<${type}>`; openTagCache.set(key, openTag); } emitStatic(openTag); if (!isVoid) { stack.push(closeTagFor(type)); } } else { const { children: _children, ...attrsProps } = props ?? {}; const attrs = spreadAttributes(attrsProps); if (isVoid) { emitStatic(`<${type}${attrs}/>`); return; } emitStatic(`<${type}${attrs}>`); stack.push(markHTMLString(`</${type}>`)); } if (!isVoid && children != null && children !== "") { if (typeof children === "string" && (type === "style" || type === "script")) { stack.push(markHTMLString(children)); } else { stack.push(children); } } return; } if (typeof type === "function" && vnode.props?.["server:root"]) { stack.push(type(vnode.props ?? {})); return; } stack.push(renderJSX(result, vnode)); }; while (stack.length > 0) { const node = stack.pop(); if (node == null || node === false) continue; if (node instanceof TemplateFrame) { const htmlParts = node.templateResult.htmlParts; const expressions = node.templateResult.expressions; let i = node.cursor; while (i < htmlParts.length) { if (htmlParts[i]) { emitStatic(htmlParts[i]); } if (i >= expressions.length) { break; } const expression = expressions[i]; i++; if (expression == null || expression === false) continue; const expressionType = typeof expression; if (expressionType === "string") { emitStatic(escapeHTML(expression)); continue; } if (expressionType === "number" || expressionType === "bigint" || expressionType === "boolean") { emitStatic(String(expression)); continue; } if (expression instanceof HTMLString || isHTMLString(expression)) { emitStatic(expression.toString()); continue; } node.storeCursor(i); stack.push(node); stack.push(expression); break; } continue; } const nodeType = typeof node; if (nodeType === "string") { emitStatic(escapeHTML(node)); continue; } if (nodeType === "number" || nodeType === "bigint" || nodeType === "boolean") { emitStatic(String(node)); continue; } if (node instanceof HTMLString || isHTMLString(node)) { emitStatic(node.toString()); continue; } if (Array.isArray(node)) { for (let i = node.length - 1; i >= 0; i--) stack.push(node[i]); continue; } if (isRenderTemplateResult(node)) { stack.push(new TemplateFrame(node)); continue; } if (isVNode(node)) { handleVNode(node); continue; } if (!buffered) { if (batch) { destination.write(markHTMLString(batch)); batch = ""; } const rendered = renderDynamic(node)(destination); if (isPromise(rendered)) { buffered = true; firstAsync = rendered; } } else { flushTailStatic(); tail.push(createBufferedRenderer(destination, renderDynamic(node))); } } if (!buffered) { if (batch) { destination.write(markHTMLString(batch)); } return; } await firstAsync; flushTailStatic(); for (const seg of tail) { if (typeof seg === "string") { destination.write(markHTMLString(seg)); } else { const r = seg.flush(); if (isPromise(r)) await r; } } } export { renderStreaming };