UNPKG

astro

Version:

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

120 lines (119 loc) 4.82 kB
import { encryptString } from "../../../core/encryption.js"; import { markHTMLString } from "../escape.js"; import { renderChild } from "./any.js"; import { createRenderInstruction } from "./instruction.js"; import { renderSlotToString } from "./slot.js"; const internalProps = /* @__PURE__ */ new Set([ "server:component-path", "server:component-export", "server:component-directive", "server:defer" ]); function containsServerDirective(props) { return "server:component-directive" in props; } const SCRIPT_RE = /<\/script/giu; const COMMENT_RE = /<!--/gu; const SCRIPT_REPLACER = "<\\/script"; const COMMENT_REPLACER = "\\u003C!--"; function safeJsonStringify(obj) { return JSON.stringify(obj).replace(SCRIPT_RE, SCRIPT_REPLACER).replace(COMMENT_RE, COMMENT_REPLACER); } function createSearchParams(componentExport, encryptedProps, slots) { const params = new URLSearchParams(); params.set("e", componentExport); params.set("p", encryptedProps); params.set("s", slots); return params; } function isWithinURLLimit(pathname, params) { const url = pathname + "?" + params.toString(); const chars = url.length; return chars < 2048; } function renderServerIsland(result, _displayName, props, slots) { return { async render(destination) { const componentPath = props["server:component-path"]; const componentExport = props["server:component-export"]; const componentId = result.serverIslandNameMap.get(componentPath); if (!componentId) { throw new Error(`Could not find server component name`); } for (const key2 of Object.keys(props)) { if (internalProps.has(key2)) { delete props[key2]; } } destination.write(createRenderInstruction({ type: "server-island-runtime" })); destination.write("<!--[if astro]>server-island-start<![endif]-->"); const renderedSlots = {}; for (const name in slots) { if (name !== "fallback") { const content = await renderSlotToString(result, slots[name]); renderedSlots[name] = content.toString(); } else { await renderChild(destination, slots.fallback(result)); } } const key = await result.key; const propsEncrypted = Object.keys(props).length === 0 ? "" : await encryptString(key, JSON.stringify(props)); const hostId = crypto.randomUUID(); const slash = result.base.endsWith("/") ? "" : "/"; let serverIslandUrl = `${result.base}${slash}_server-islands/${componentId}${result.trailingSlash === "always" ? "/" : ""}`; const potentialSearchParams = createSearchParams( componentExport, propsEncrypted, safeJsonStringify(renderedSlots) ); const useGETRequest = isWithinURLLimit(serverIslandUrl, potentialSearchParams); if (useGETRequest) { serverIslandUrl += "?" + potentialSearchParams.toString(); destination.write( `<link rel="preload" as="fetch" href="${serverIslandUrl}" crossorigin="anonymous">` ); } destination.write(`<script type="module" data-astro-rerun data-island-id="${hostId}">${useGETRequest ? ( // GET request `let response = await fetch('${serverIslandUrl}');` ) : ( // POST request `let data = { componentExport: ${safeJsonStringify(componentExport)}, encryptedProps: ${safeJsonStringify(propsEncrypted)}, slots: ${safeJsonStringify(renderedSlots)}, }; let response = await fetch('${serverIslandUrl}', { method: 'POST', body: JSON.stringify(data), });` )} replaceServerIsland('${hostId}', response);</script>`); } }; } const renderServerIslandRuntime = () => markHTMLString( ` <script> async function replaceServerIsland(id, r) { let s = document.querySelector(\`script[data-island-id="\${id}"]\`); // If there's no matching script, or the request fails then return if (!s || r.status !== 200 || r.headers.get('content-type')?.split(';')[0].trim() !== 'text/html') return; // Load the HTML before modifying the DOM in case of errors let html = await r.text(); // Remove any placeholder content before the island script while (s.previousSibling && s.previousSibling.nodeType !== 8 && s.previousSibling.data !== '[if astro]>server-island-start<![endif]') s.previousSibling.remove(); s.previousSibling?.remove(); // Insert the new HTML s.before(document.createRange().createContextualFragment(html)); // Remove the script. Prior to v5.4.2, this was the trick to force rerun of scripts. Keeping it to minimize change to the existing behavior. s.remove(); } </script>`.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("//")).join(" ") ); export { containsServerDirective, renderServerIsland, renderServerIslandRuntime };