UNPKG

astro

Version:

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

148 lines (147 loc) • 5.34 kB
import { markHTMLString } from "../escape.js"; import { determineIfNeedsHydrationScript, determinesIfNeedsDirectiveScript, getPrescripts } from "../scripts.js"; import { getInstructionRenderState, shouldRenderInstruction } from "./head-propagation/runtime.js"; import { renderAllHeadContent } from "./head.js"; import { isRenderInstruction } from "./instruction.js"; import { renderServerIslandRuntime } from "./server-islands.js"; import { isSlotString } from "./slot.js"; export const Fragment = Symbol.for("astro:fragment"); export const Renderer = Symbol.for("astro:renderer"); export const encoder = new TextEncoder(); 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. function stringifyChunk(result, chunk) { if (typeof chunk === "string") return chunk; if (isRenderInstruction(chunk)) { const instruction = chunk; switch (instruction.type) { case "directive": { const { hydration } = instruction; const needsHydrationScript = hydration && determineIfNeedsHydrationScript(result); const needsDirectiveScript = hydration && determinesIfNeedsDirectiveScript(result, hydration.directive); if (needsHydrationScript) { const prescripts = getPrescripts(result, "both", hydration.directive); return markHTMLString(prescripts); } else if (needsDirectiveScript) { const prescripts = getPrescripts(result, "directive", hydration.directive); return markHTMLString(prescripts); } else { return ""; } } case "head": { if (!shouldRenderInstruction("head", getInstructionRenderState(result))) { return ""; } return renderAllHeadContent(result); } case "maybe-head": { if (!shouldRenderInstruction("maybe-head", getInstructionRenderState(result))) { return ""; } return renderAllHeadContent(result); } case "renderer-hydration-script": { const { rendererSpecificHydrationScripts } = result._metadata; const { rendererName } = instruction; if (result._metadata.templateDepth > 0) { return instruction.render(); } if (!rendererSpecificHydrationScripts.has(rendererName)) { rendererSpecificHydrationScripts.add(rendererName); return instruction.render(); } return ""; } case "server-island-runtime": { if (result._metadata.templateDepth > 0) { return renderServerIslandRuntime(); } if (result._metadata.hasRenderedServerIslandRuntime) { return ""; } result._metadata.hasRenderedServerIslandRuntime = true; return renderServerIslandRuntime(); } case "script": { const { id, content } = instruction; // If we're inside a <template> element, still render the script but // don't mark it as deduplicated. Template content is inert, scripts // inside don't execute, so the script must also appear outside the // template for non-template instances to work if (result._metadata.templateDepth > 0) { return content; } if (result._metadata.renderedScripts.has(id)) { return ""; } result._metadata.renderedScripts.add(id); return content; } case "template-enter": { result._metadata.templateDepth++; return ""; } case "template-exit": { if (result._metadata.templateDepth <= 0) { throw new Error("Unexpected template-exit instruction without a matching template-enter. " + "This may indicate that the compiler emitted unbalanced template boundaries, " + "or that a component manually injected a template-exit render instruction."); } result._metadata.templateDepth--; return ""; } default: { throw new Error(`Unknown chunk type: ${chunk.type}`); } } } else if (chunk instanceof Response) { return ""; } else if (isSlotString(chunk)) { let out = ""; const c = chunk; // Position-independent instructions (head, hydration, etc.) are emitted first. if (c.instructions) { for (const instr of c.instructions) { out += stringifyChunk(result, instr); } } // Walk the ordered content stream so scripts render at their original // position (and are deduplicated) instead of being hoisted to the front. if (c.chunks.length) { for (const part of c.chunks) { out += typeof part === "string" ? part : stringifyChunk(result, part); } } else { out += chunk.toString(); } return out; } return chunk.toString(); } export function chunkToString(result, chunk) { if (ArrayBuffer.isView(chunk)) { return decoder.decode(chunk); } else { return stringifyChunk(result, chunk); } } export function chunkToByteArray(result, chunk) { if (ArrayBuffer.isView(chunk)) { return chunk; } else { // `stringifyChunk` might return a HTMLString, call `.toString()` to really ensure it's a string const stringified = stringifyChunk(result, chunk); return encoder.encode(stringified.toString()); } } export function chunkToByteArrayOrString(result, chunk) { if (ArrayBuffer.isView(chunk)) { return chunk; } else { return stringifyChunk(result, chunk).toString(); } } export function isRenderInstance(obj) { return typeof obj === "object" && obj !== null && typeof obj.render === "function"; }