UNPKG

astro

Version:

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

198 lines (197 loc) • 6.83 kB
import { visit } from "unist-util-visit"; import { AstroError } from "../core/errors/errors.js"; import { AstroErrorData } from "../core/errors/index.js"; import { resolvePath } from "../core/viteUtils.js"; import { createDefaultAstroMetadata } from "../vite-plugin-astro/metadata.js"; const ClientOnlyPlaceholder = "astro-client-only"; const rehypeAnalyzeAstroMetadata = () => { return (tree, file) => { const metadata = createDefaultAstroMetadata(); const imports = parseImports(tree.children); visit(tree, (node) => { if (node.type !== "mdxJsxFlowElement" && node.type !== "mdxJsxTextElement") return; const tagName = node.name; if (!tagName || !isComponent(tagName) || !hasClientDirective(node)) return; const matchedImport = findMatchingImport(tagName, imports); if (!matchedImport) { throw new AstroError({ ...AstroErrorData.NoMatchingImport, message: AstroErrorData.NoMatchingImport.message(node.name) }); } if (matchedImport.path.endsWith(".astro")) { const clientAttribute = node.attributes.find( (attr) => attr.type === "mdxJsxAttribute" && attr.name.startsWith("client:") ); if (clientAttribute) { console.warn( `You are attempting to render <${node.name} ${clientAttribute.name} />, but ${node.name} is an Astro component. Astro components do not render in the client and should not have a hydration directive. Please use a framework component for client rendering.` ); } } const resolvedPath = resolvePath(matchedImport.path, file.path); if (hasClientOnlyDirective(node)) { metadata.clientOnlyComponents.push({ exportName: matchedImport.name, localName: "", specifier: tagName, resolvedPath }); addClientOnlyMetadata(node, matchedImport, resolvedPath); } else { metadata.hydratedComponents.push({ exportName: "*", localName: "", specifier: tagName, resolvedPath }); addClientMetadata(node, matchedImport, resolvedPath); } }); file.data.__astroMetadata = metadata; }; }; function getAstroMetadata(file) { return file.data.__astroMetadata; } function parseImports(children) { const imports = /* @__PURE__ */ new Map(); for (const child of children) { if (child.type !== "mdxjsEsm") continue; const body = child.data?.estree?.body; if (!body) continue; for (const ast of body) { if (ast.type !== "ImportDeclaration") continue; const source = ast.source.value; const specs = ast.specifiers.map((spec) => { switch (spec.type) { case "ImportDefaultSpecifier": return { local: spec.local.name, imported: "default" }; case "ImportNamespaceSpecifier": return { local: spec.local.name, imported: "*" }; case "ImportSpecifier": { return { local: spec.local.name, imported: spec.imported.type === "Identifier" ? spec.imported.name : String(spec.imported.value) }; } default: throw new Error("Unknown import declaration specifier: " + spec); } }); let specSet = imports.get(source); if (!specSet) { specSet = /* @__PURE__ */ new Set(); imports.set(source, specSet); } for (const spec of specs) { specSet.add(spec); } } } return imports; } function isComponent(tagName) { return tagName[0] && tagName[0].toLowerCase() !== tagName[0] || tagName.includes(".") || /[^a-zA-Z]/.test(tagName[0]); } function hasClientDirective(node) { return node.attributes.some( (attr) => attr.type === "mdxJsxAttribute" && attr.name.startsWith("client:") ); } function hasClientOnlyDirective(node) { return node.attributes.some( (attr) => attr.type === "mdxJsxAttribute" && attr.name === "client:only" ); } function findMatchingImport(tagName, imports) { const tagSpecifier = tagName.split(".")[0]; for (const [source, specs] of imports) { for (const { imported, local } of specs) { if (local === tagSpecifier) { if (tagSpecifier !== tagName) { switch (imported) { case "*": { const accessPath = tagName.slice(tagSpecifier.length + 1); return { name: accessPath, path: source }; } case "default": { const accessPath = tagName.slice(tagSpecifier.length + 1); return { name: `default.${accessPath}`, path: source }; } default: { return { name: tagName, path: source }; } } } return { name: imported, path: source }; } } } } function addClientMetadata(node, meta, resolvedPath) { const attributeNames = node.attributes.map((attr) => attr.type === "mdxJsxAttribute" ? attr.name : null).filter(Boolean); if (!attributeNames.includes("client:component-path")) { node.attributes.push({ type: "mdxJsxAttribute", name: "client:component-path", value: resolvedPath }); } if (!attributeNames.includes("client:component-export")) { if (meta.name === "*") { meta.name = node.name.split(".").slice(1).join("."); } node.attributes.push({ type: "mdxJsxAttribute", name: "client:component-export", value: meta.name }); } if (!attributeNames.includes("client:component-hydration")) { node.attributes.push({ type: "mdxJsxAttribute", name: "client:component-hydration", value: null }); } } function addClientOnlyMetadata(node, meta, resolvedPath) { const attributeNames = node.attributes.map((attr) => attr.type === "mdxJsxAttribute" ? attr.name : null).filter(Boolean); if (!attributeNames.includes("client:display-name")) { node.attributes.push({ type: "mdxJsxAttribute", name: "client:display-name", value: node.name }); } if (!attributeNames.includes("client:component-hydpathation")) { node.attributes.push({ type: "mdxJsxAttribute", name: "client:component-path", value: resolvedPath }); } if (!attributeNames.includes("client:component-export")) { if (meta.name === "*") { meta.name = node.name.split(".").slice(1).join("."); } node.attributes.push({ type: "mdxJsxAttribute", name: "client:component-export", value: meta.name }); } if (!attributeNames.includes("client:component-hydration")) { node.attributes.push({ type: "mdxJsxAttribute", name: "client:component-hydration", value: null }); } node.name = ClientOnlyPlaceholder; } export { getAstroMetadata, rehypeAnalyzeAstroMetadata };