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