UNPKG

astro

Version:

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

160 lines (159 loc) • 6.98 kB
import { AstroJSX, isVNode } from "../../jsx-runtime/index.js"; import { escapeStyleText } from "./escape.js"; import { escapeHTML, HTMLString, markHTMLString, spreadAttributes, voidElementNames } from "./index.js"; import { isAstroComponentFactory } from "./render/astro/factory.js"; import { renderComponentToString } from "./render/component.js"; import { mergeSlotInstructions, SlotString } from "./render/slot.js"; const ClientOnlyPlaceholder = "astro-client-only"; // If the `vnode.type` is a function, we could render it as JSX or as framework components. // Inside `renderJSXNode`, we first try to render as framework components, and if `renderJSXNode` // is called again while rendering the component, it's likely that the `astro:jsx` is invoking // `renderJSXNode` again (loop). In this case, we try to render as JSX instead. // // This Symbol is assigned to `vnode.props` to track if it had tried to render as framework components. // It mutates `vnode.props` to be able to scope to the current render call. const hasTriedRenderComponentSymbol = Symbol("hasTriedRenderComponent"); export async function renderJSX(result, vnode) { // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check switch (true) { case vnode instanceof HTMLString: if (vnode.toString().trim() === "") { return ""; } return vnode; case typeof vnode === "string": return markHTMLString(escapeHTML(vnode)); case typeof vnode === "function": return vnode; case !vnode && vnode !== 0: return ""; case Array.isArray(vnode): { const renderedItems = await Promise.all(vnode.map((v) => renderJSX(result, v))); // Collect instructions from SlotString items to preserve hydration scripts let instructions = null; let content = ""; for (const item of renderedItems) { if (item instanceof SlotString) { content += item; instructions = mergeSlotInstructions(instructions, item); } else { content += item; } } if (instructions) { return markHTMLString(new SlotString(content, instructions)); } return markHTMLString(content); } } return renderJSXVNode(result, vnode); } async function renderJSXVNode(result, vnode) { if (isVNode(vnode)) { // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check switch (true) { case !vnode.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?`); } case vnode.type === Symbol.for("astro:fragment"): return renderJSX(result, vnode.props.children); case isAstroComponentFactory(vnode.type): { let props = {}; let 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; } } // We don't use renderToString because it doesn't contain the server island script handling const str = await renderComponentToString(result, vnode.type.name, vnode.type, props, slots); const html = markHTMLString(str); return html; } case !vnode.type && vnode.type !== 0: return ""; case typeof vnode.type === "string" && vnode.type !== ClientOnlyPlaceholder && !vnode.type.includes("-"): return markHTMLString(await renderElement(result, vnode.type, vnode.props ?? {})); } if (vnode.type) { if (typeof vnode.type === "function" && vnode.props["server:root"]) { const output = await vnode.type(vnode.props ?? {}); return await renderJSX(result, output); } if (typeof vnode.type === "function") { if (vnode.props[hasTriedRenderComponentSymbol]) { // omitting compiler-internals from user components delete vnode.props[hasTriedRenderComponentSymbol]; const output = await vnode.type(vnode.props ?? {}); if (output?.[AstroJSX] || !output) { return await renderJSXVNode(result, output); } else { return; } } else { vnode.props[hasTriedRenderComponentSymbol] = true; } } const { children = null, ...props } = vnode.props ?? {}; const _slots = { default: [] }; // For custom HTML elements (string type with hyphen), the `slot` attribute on children // is a standard HTML attribute for web component Shadow DOM slot distribution, not an // Astro slot assignment. Skip slot extraction to preserve it in the output. const isCustomElement = typeof vnode.type === "string" && vnode.type.includes("-"); function extractSlots(child) { if (Array.isArray(child)) { return child.map((c) => extractSlots(c)); } if (!isVNode(child)) { _slots.default.push(child); return; } if ("slot" in child.props && !isCustomElement) { _slots[child.props.slot] = [..._slots[child.props.slot] ?? [], child]; delete child.props.slot; return; } _slots.default.push(child); } extractSlots(children); for (const [key, value] of Object.entries(props)) { if (value?.["$$slot"]) { _slots[key] = value; delete props[key]; } } const slotPromises = []; const slots = {}; for (const [key, value] of Object.entries(_slots)) { slotPromises.push(renderJSX(result, value).then((output) => { if (output.toString().trim().length === 0) return; slots[key] = () => output; })); } await Promise.all(slotPromises); let output; if (vnode.type === ClientOnlyPlaceholder && vnode.props["client:only"]) { output = await renderComponentToString(result, vnode.props["client:display-name"] ?? "", null, props, slots); } else { output = await renderComponentToString(result, typeof vnode.type === "function" ? vnode.type.name : vnode.type, vnode.type, props, slots); } return markHTMLString(output); } } // numbers, plain objects, etc return markHTMLString(`${vnode}`); } async function renderElement(result, tag, { children, ...props }) { return markHTMLString(`<${tag}${spreadAttributes(props)}${markHTMLString((children == null || children === "") && voidElementNames.test(tag) ? `/>` : `>${children == null ? "" : await renderJSX(result, prerenderElementChildren(tag, children))}</${tag}>`)}`); } /** * Pre-render the children with the given `tag` information */ function prerenderElementChildren(tag, children) { // Literal `<style>`/`<script>` content (including content injected by remark/rehype // plugins) is collapsed into `set:html` at compile time, so any remaining plain-string // children here are dynamic values that should be escaped like other element content. // `escapeStyleText` only escapes `<`, preserving quotes CSS commonly relies on while // still preventing the value from closing the `<style>` tag early. if (typeof children === "string" && tag === "style") { return markHTMLString(escapeStyleText(children)); } return children; }