UNPKG

astro

Version:

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

83 lines (82 loc) • 3.82 kB
import { markHTMLString } from "../escape.js"; import { renderCspContent } from "./csp.js"; import { createRenderInstruction } from "./instruction.js"; import { renderElement } from "./util.js"; // Deterministic stringification of props that is key-order independent, // so elements with the same props in different insertion order are still deduped. function stablePropsKey(props) { const keys = Object.keys(props).sort(); let result = "{"; for (let i = 0; i < keys.length; i++) { if (i > 0) result += ","; result += JSON.stringify(keys[i]) + ":" + JSON.stringify(props[keys[i]]); } result += "}"; return result; } // Filter out duplicate elements using a Set for O(N) instead of O(N²) function deduplicateElements(elements) { if (elements.length <= 1) return elements; const seen = new Set(); return elements.filter((item) => { const key = stablePropsKey(item.props) + item.children; if (seen.has(key)) return false; seen.add(key); return true; }); } export function renderAllHeadContent(result) { result._metadata.hasRenderedHead = true; let content = ""; if (result.shouldInjectCspMetaTags && result.cspDestination === "meta") { content += renderElement("meta", { props: { "http-equiv": "content-security-policy", content: renderCspContent(result) }, children: "" }, false); } const styles = deduplicateElements(Array.from(result.styles)).map((style) => style.props.rel === "stylesheet" ? renderElement("link", style) : renderElement("style", style)); // Clear result.styles so that any new styles added will be inlined. result.styles.clear(); const scripts = deduplicateElements(Array.from(result.scripts)).map((script) => { if (result.userAssetsBase) { script.props.src = (result.base === "/" ? "" : result.base) + result.userAssetsBase + script.props.src; } return renderElement("script", script, false); }); const links = deduplicateElements(Array.from(result.links)).map((link) => renderElement("link", link, false)); // Order styles -> links -> scripts similar to src/content/runtime.ts // The order is usually fine as the ordering between these groups are mutually exclusive, // except for CSS styles and CSS stylesheet links. However CSS stylesheet links usually // consist of CSS modules which should naturally take precedence over CSS styles, so the // order will still work. In prod, all CSS are stylesheet links. // In the future, it may be better to have only an array of head elements to avoid these assumptions. const sep = result.compressHTML === true || result.compressHTML === "jsx" ? "" : "\n"; content += styles.join(sep) + links.join(sep) + scripts.join(sep); // Inject static speculation rules when CSP + clientPrerender are both enabled. // The content is pre-hashed at build time and included in the CSP script-src directive. if (result.speculationRulesContent) { content += renderElement("script", { props: { type: "speculationrules" }, children: result.speculationRulesContent }, false); } content += result._metadata.extraHead.join(""); return markHTMLString(content); } const HEAD_INSTRUCTION = Object.freeze(createRenderInstruction({ type: "head" })); const MAYBE_HEAD_INSTRUCTION = Object.freeze(createRenderInstruction({ type: "maybe-head" })); export function renderHead() { return HEAD_INSTRUCTION; } // This function is called by Astro components that do not contain a <head> component // This accommodates the fact that using a <head> is optional in Astro, so this // is called before a component's first non-head HTML element. If the head was // already injected it is a noop. export function maybeRenderHead() { // This is an instruction informing the page rendering that head might need rendering. // This allows the page to deduplicate head injections. return MAYBE_HEAD_INSTRUCTION; }