318 lines
11 KiB
JavaScript
318 lines
11 KiB
JavaScript
![]() |
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 {
|
||
|
createAstroComponentInstance,
|
||
|
isAstroComponentFactory,
|
||
|
isAstroComponentInstance,
|
||
|
renderAstroTemplateResult,
|
||
|
renderTemplate
|
||
|
} from "./astro/index.js";
|
||
|
import { Fragment, Renderer, stringifyChunk } from "./common.js";
|
||
|
import { componentIsHTMLElement, renderHTMLElement } from "./dom.js";
|
||
|
import { renderSlots, renderSlotToString } from "./slot.js";
|
||
|
import { formatList, internalSpreadAttributes, renderElement, voidElementNames } from "./util.js";
|
||
|
const rendererAliases = /* @__PURE__ */ new Map([["solid", "solid-js"]]);
|
||
|
function guessRenderers(componentUrl) {
|
||
|
const extname = componentUrl == null ? void 0 : 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 = {}) {
|
||
|
var _a, _b, _c;
|
||
|
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 } = 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 = renderHTMLElement(result, Component, _props, slots);
|
||
|
return 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 = (_a = metadata.componentUrl) == null ? void 0 : _a.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,
|
||
|
(_b = metadata == null ? void 0 : metadata.componentUrl) == null ? void 0 : _b.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,
|
||
|
props,
|
||
|
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 == null ? void 0 : slots.fallback);
|
||
|
} else {
|
||
|
({ html, attrs } = await renderer.ssr.renderToStaticMarkup.call(
|
||
|
{ result },
|
||
|
Component,
|
||
|
props,
|
||
|
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 iterable = renderAstroTemplateResult(
|
||
|
await renderTemplate`<${Tag}${internalSpreadAttributes(props)}${markHTMLString(
|
||
|
childSlots === "" && voidElementNames.test(Tag) ? `/>` : `>${childSlots}</${Tag}>`
|
||
|
)}`
|
||
|
);
|
||
|
html = "";
|
||
|
for await (const chunk of iterable) {
|
||
|
html += chunk;
|
||
|
}
|
||
|
}
|
||
|
if (!hydration) {
|
||
|
return async function* () {
|
||
|
var _a2;
|
||
|
if (slotInstructions) {
|
||
|
yield* slotInstructions;
|
||
|
}
|
||
|
if (isPage || (renderer == null ? void 0 : renderer.name) === "astro:jsx") {
|
||
|
yield html;
|
||
|
} else if (html && html.length > 0) {
|
||
|
yield markHTMLString(
|
||
|
removeStaticAstroSlot(html, ((_a2 = renderer == null ? void 0 : renderer.ssr) == null ? void 0 : _a2.supportsAstroStaticSlot) ?? false)
|
||
|
);
|
||
|
} else {
|
||
|
yield "";
|
||
|
}
|
||
|
}();
|
||
|
}
|
||
|
const astroId = shorthash(
|
||
|
`<!--${metadata.componentExport.value}:${metadata.componentUrl}-->
|
||
|
${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 = ((_c = renderer == null ? void 0 : renderer.ssr) == null ? void 0 : _c.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) => `<template data-astro-template${key !== "default" ? `="${key}"` : ""}>${children[key]}</template>`
|
||
|
).join("") : "";
|
||
|
island.children = `${html ?? ""}${template}`;
|
||
|
if (island.children) {
|
||
|
island.props["await-children"] = "";
|
||
|
}
|
||
|
async function* renderAll() {
|
||
|
if (slotInstructions) {
|
||
|
yield* slotInstructions;
|
||
|
}
|
||
|
yield { type: "directive", hydration, result };
|
||
|
yield markHTMLString(renderElement("astro-island", island, false));
|
||
|
}
|
||
|
return renderAll();
|
||
|
}
|
||
|
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 == null ? void 0 : slots.default);
|
||
|
if (children == null) {
|
||
|
return children;
|
||
|
}
|
||
|
return markHTMLString(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) => stringifyChunk(result, instr)).join("") : "";
|
||
|
return markHTMLString(hydrationHtml + html);
|
||
|
}
|
||
|
function renderComponent(result, displayName, Component, props, slots = {}) {
|
||
|
if (isPromise(Component)) {
|
||
|
return Promise.resolve(Component).then((Unwrapped) => {
|
||
|
return renderComponent(result, displayName, Unwrapped, props, slots);
|
||
|
});
|
||
|
}
|
||
|
if (isFragmentComponent(Component)) {
|
||
|
return renderFragmentComponent(result, slots);
|
||
|
}
|
||
|
if (isHTMLComponent(Component)) {
|
||
|
return renderHTMLComponent(result, Component, props, slots);
|
||
|
}
|
||
|
if (isAstroComponentFactory(Component)) {
|
||
|
return createAstroComponentInstance(result, displayName, Component, props, slots);
|
||
|
}
|
||
|
return renderFrameworkComponent(result, displayName, Component, props, slots);
|
||
|
}
|
||
|
function renderComponentToIterable(result, displayName, Component, props, slots = {}) {
|
||
|
const renderResult = renderComponent(result, displayName, Component, props, slots);
|
||
|
if (isAstroComponentInstance(renderResult)) {
|
||
|
return renderResult.render();
|
||
|
}
|
||
|
return renderResult;
|
||
|
}
|
||
|
export {
|
||
|
renderComponent,
|
||
|
renderComponentToIterable
|
||
|
};
|