import { createRenderInstruction } from "./instruction.js"; import { clsx } from "clsx"; import { AstroError, AstroErrorData } from "../../../core/errors/index.js"; import { markHTMLString } from "../escape.js"; import { extractDirectives, generateHydrateScript } from "../hydration.js"; import { serializeProps } from "../serialize.js"; import { shorthash } from "../shorthash.js"; import { isPromise } from "../util.js"; import { isAstroComponentFactory } from "./astro/factory.js"; import { renderTemplate } from "./astro/index.js"; import { createAstroComponentInstance } from "./astro/instance.js"; import { Fragment, Renderer, chunkToString } from "./common.js"; import { componentIsHTMLElement, renderHTMLElement } from "./dom.js"; import { maybeRenderHead } from "./head.js"; import { renderSlotToString, renderSlots } from "./slot.js"; import { formatList, internalSpreadAttributes, renderElement, voidElementNames } from "./util.js"; const needsHeadRenderingSymbol = Symbol.for("astro.needsHeadRendering"); const rendererAliases = /* @__PURE__ */ new Map([["solid", "solid-js"]]); function guessRenderers(componentUrl) { const extname = componentUrl?.split(".").pop(); switch (extname) { case "svelte": return ["@astrojs/svelte"]; case "vue": return ["@astrojs/vue"]; case "jsx": case "tsx": return ["@astrojs/react", "@astrojs/preact", "@astrojs/solid-js", "@astrojs/vue (jsx)"]; default: return [ "@astrojs/react", "@astrojs/preact", "@astrojs/solid-js", "@astrojs/vue", "@astrojs/svelte", "@astrojs/lit" ]; } } function isFragmentComponent(Component) { return Component === Fragment; } function isHTMLComponent(Component) { return Component && Component["astro:html"] === true; } const ASTRO_SLOT_EXP = /\<\/?astro-slot\b[^>]*>/g; const ASTRO_STATIC_SLOT_EXP = /\<\/?astro-static-slot\b[^>]*>/g; function removeStaticAstroSlot(html, supportsAstroStaticSlot) { const exp = supportsAstroStaticSlot ? ASTRO_STATIC_SLOT_EXP : ASTRO_SLOT_EXP; return html.replace(exp, ""); } async function renderFrameworkComponent(result, displayName, Component, _props, slots = {}) { if (!Component && !_props["client:only"]) { throw new Error( `Unable to render ${displayName} because it is ${Component}! Did you forget to import the component or is it possible there is a typo?` ); } const { renderers, clientDirectives } = result; const metadata = { astroStaticSlot: true, displayName }; const { hydration, isPage, props, propsWithoutTransitionAttributes } = extractDirectives( _props, clientDirectives ); let html = ""; let attrs = void 0; if (hydration) { metadata.hydrate = hydration.directive; metadata.hydrateArgs = hydration.value; metadata.componentExport = hydration.componentExport; metadata.componentUrl = hydration.componentUrl; } const probableRendererNames = guessRenderers(metadata.componentUrl); const validRenderers = renderers.filter((r) => r.name !== "astro:jsx"); const { children, slotInstructions } = await renderSlots(result, slots); let renderer; if (metadata.hydrate !== "only") { let isTagged = false; try { isTagged = Component && Component[Renderer]; } catch { } if (isTagged) { const rendererName = Component[Renderer]; renderer = renderers.find(({ name }) => name === rendererName); } if (!renderer) { let error; for (const r of renderers) { try { if (await r.ssr.check.call({ result }, Component, props, children)) { renderer = r; break; } } catch (e) { error ??= e; } } if (!renderer && error) { throw error; } } if (!renderer && typeof HTMLElement === "function" && componentIsHTMLElement(Component)) { const output = await renderHTMLElement( result, Component, _props, slots ); return { render(destination) { destination.write(output); } }; } } else { if (metadata.hydrateArgs) { const passedName = metadata.hydrateArgs; const rendererName = rendererAliases.has(passedName) ? rendererAliases.get(passedName) : passedName; renderer = renderers.find( ({ name }) => name === `@astrojs/${rendererName}` || name === rendererName ); } if (!renderer && validRenderers.length === 1) { renderer = validRenderers[0]; } if (!renderer) { const extname = metadata.componentUrl?.split(".").pop(); renderer = renderers.filter( ({ name }) => name === `@astrojs/${extname}` || name === extname )[0]; } } if (!renderer) { if (metadata.hydrate === "only") { throw new AstroError({ ...AstroErrorData.NoClientOnlyHint, message: AstroErrorData.NoClientOnlyHint.message(metadata.displayName), hint: AstroErrorData.NoClientOnlyHint.hint( probableRendererNames.map((r) => r.replace("@astrojs/", "")).join("|") ) }); } else if (typeof Component !== "string") { const matchingRenderers = validRenderers.filter( (r) => probableRendererNames.includes(r.name) ); const plural = validRenderers.length > 1; if (matchingRenderers.length === 0) { throw new AstroError({ ...AstroErrorData.NoMatchingRenderer, message: AstroErrorData.NoMatchingRenderer.message( metadata.displayName, metadata?.componentUrl?.split(".").pop(), plural, validRenderers.length ), hint: AstroErrorData.NoMatchingRenderer.hint( formatList(probableRendererNames.map((r) => "`" + r + "`")) ) }); } else if (matchingRenderers.length === 1) { renderer = matchingRenderers[0]; ({ html, attrs } = await renderer.ssr.renderToStaticMarkup.call( { result }, Component, propsWithoutTransitionAttributes, children, metadata )); } else { throw new Error(`Unable to render ${metadata.displayName}! This component likely uses ${formatList(probableRendererNames)}, but Astro encountered an error during server-side rendering. Please ensure that ${metadata.displayName}: 1. Does not unconditionally access browser-specific globals like \`window\` or \`document\`. If this is unavoidable, use the \`client:only\` hydration directive. 2. Does not conditionally return \`null\` or \`undefined\` when rendered on the server. If you're still stuck, please open an issue on GitHub or join us at https://astro.build/chat.`); } } } else { if (metadata.hydrate === "only") { html = await renderSlotToString(result, slots?.fallback); } else { ({ html, attrs } = await renderer.ssr.renderToStaticMarkup.call( { result }, Component, propsWithoutTransitionAttributes, children, metadata )); } } if (renderer && !renderer.clientEntrypoint && renderer.name !== "@astrojs/lit" && metadata.hydrate) { throw new AstroError({ ...AstroErrorData.NoClientEntrypoint, message: AstroErrorData.NoClientEntrypoint.message( displayName, metadata.hydrate, renderer.name ) }); } if (!html && typeof Component === "string") { const Tag = sanitizeElementName(Component); const childSlots = Object.values(children).join(""); const renderTemplateResult = renderTemplate`<${Tag}${internalSpreadAttributes( props )}${markHTMLString( childSlots === "" && voidElementNames.test(Tag) ? `/>` : `>${childSlots}` )}`; html = ""; const destination = { write(chunk) { if (chunk instanceof Response) return; html += chunkToString(result, chunk); } }; await renderTemplateResult.render(destination); } if (!hydration) { return { render(destination) { if (slotInstructions) { for (const instruction of slotInstructions) { destination.write(instruction); } } if (isPage || renderer?.name === "astro:jsx") { destination.write(html); } else if (html && html.length > 0) { destination.write( markHTMLString( removeStaticAstroSlot(html, renderer?.ssr?.supportsAstroStaticSlot ?? false) ) ); } } }; } const astroId = shorthash( ` ${html} ${serializeProps( props, metadata )}` ); const island = await generateHydrateScript( { renderer, result, astroId, props, attrs }, metadata ); let unrenderedSlots = []; if (html) { if (Object.keys(children).length > 0) { for (const key of Object.keys(children)) { let tagName = renderer?.ssr?.supportsAstroStaticSlot ? !!metadata.hydrate ? "astro-slot" : "astro-static-slot" : "astro-slot"; let expectedHTML = key === "default" ? `<${tagName}>` : `<${tagName} name="${key}">`; if (!html.includes(expectedHTML)) { unrenderedSlots.push(key); } } } } else { unrenderedSlots = Object.keys(children); } const template = unrenderedSlots.length > 0 ? unrenderedSlots.map( (key) => `` ).join("") : ""; island.children = `${html ?? ""}${template}`; if (island.children) { island.props["await-children"] = ""; island.children += ``; } return { render(destination) { if (slotInstructions) { for (const instruction of slotInstructions) { destination.write(instruction); } } destination.write(createRenderInstruction({ type: "directive", hydration })); if (hydration.directive !== "only" && renderer?.ssr.renderHydrationScript) { destination.write( createRenderInstruction({ type: "renderer-hydration-script", rendererName: renderer.name, render: renderer.ssr.renderHydrationScript }) ); } destination.write(markHTMLString(renderElement("astro-island", island, false))); } }; } function sanitizeElementName(tag) { const unsafe = /[&<>'"\s]+/g; if (!unsafe.test(tag)) return tag; return tag.trim().split(unsafe)[0].trim(); } async function renderFragmentComponent(result, slots = {}) { const children = await renderSlotToString(result, slots?.default); return { render(destination) { if (children == null) return; destination.write(children); } }; } async function renderHTMLComponent(result, Component, _props, slots = {}) { const { slotInstructions, children } = await renderSlots(result, slots); const html = Component({ slots: children }); const hydrationHtml = slotInstructions ? slotInstructions.map((instr) => chunkToString(result, instr)).join("") : ""; return { render(destination) { destination.write(markHTMLString(hydrationHtml + html)); } }; } function renderAstroComponent(result, displayName, Component, props, slots = {}) { const instance = createAstroComponentInstance(result, displayName, Component, props, slots); return { async render(destination) { await instance.render(destination); } }; } async function renderComponent(result, displayName, Component, props, slots = {}) { if (isPromise(Component)) { Component = await Component; } if (isFragmentComponent(Component)) { return await renderFragmentComponent(result, slots); } props = normalizeProps(props); if (isHTMLComponent(Component)) { return await renderHTMLComponent(result, Component, props, slots); } if (isAstroComponentFactory(Component)) { return renderAstroComponent(result, displayName, Component, props, slots); } return await renderFrameworkComponent(result, displayName, Component, props, slots); } function normalizeProps(props) { if (props["class:list"] !== void 0) { const value = props["class:list"]; delete props["class:list"]; props["class"] = clsx(props["class"], value); if (props["class"] === "") { delete props["class"]; } } return props; } async function renderComponentToString(result, displayName, Component, props, slots = {}, isPage = false, route) { let str = ""; let renderedFirstPageChunk = false; let head = ""; if (nonAstroPageNeedsHeadInjection(Component)) { for (const headChunk of maybeRenderHead()) { head += chunkToString(result, headChunk); } } try { const destination = { write(chunk) { if (isPage && !renderedFirstPageChunk) { renderedFirstPageChunk = true; if (!result.partial && !/" : "\n"; str += doctype + head; } } if (chunk instanceof Response) return; str += chunkToString(result, chunk); } }; const renderInstance = await renderComponent(result, displayName, Component, props, slots); await renderInstance.render(destination); } catch (e) { if (AstroError.is(e) && !e.loc) { e.setLocation({ file: route?.component }); } throw e; } return str; } function nonAstroPageNeedsHeadInjection(pageComponent) { return !!pageComponent?.[needsHeadRenderingSymbol]; } export { renderComponent, renderComponentToString };