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
JavaScript
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";
}