242 lines
8.3 KiB
JavaScript
242 lines
8.3 KiB
JavaScript
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) || hasServerDeferDirective(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 if (hasClientDirective(node)) {
|
|
metadata.hydratedComponents.push({
|
|
exportName: "*",
|
|
localName: "",
|
|
specifier: tagName,
|
|
resolvedPath
|
|
});
|
|
addClientMetadata(node, matchedImport, resolvedPath);
|
|
} else if (hasServerDeferDirective(node)) {
|
|
metadata.serverComponents.push({
|
|
exportName: matchedImport.name,
|
|
localName: tagName,
|
|
specifier: matchedImport.path,
|
|
resolvedPath
|
|
});
|
|
addServerDeferMetadata(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 hasServerDeferDirective(node) {
|
|
return node.attributes.some(
|
|
(attr) => attr.type === "mdxJsxAttribute" && attr.name === "server:defer"
|
|
);
|
|
}
|
|
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) {
|
|
// Namespace import: "<buttons.Foo.Bar />" => name: "Foo.Bar"
|
|
case "*": {
|
|
const accessPath = tagName.slice(tagSpecifier.length + 1);
|
|
return { name: accessPath, path: source };
|
|
}
|
|
// Default import: "<buttons.Foo.Bar />" => name: "default.Foo.Bar"
|
|
case "default": {
|
|
const accessPath = tagName.slice(tagSpecifier.length + 1);
|
|
return { name: `default.${accessPath}`, path: source };
|
|
}
|
|
// Named import: "<buttons.Foo.Bar />" => name: "buttons.Foo.Bar"
|
|
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;
|
|
}
|
|
function addServerDeferMetadata(node, meta, resolvedPath) {
|
|
const attributeNames = node.attributes.map((attr) => attr.type === "mdxJsxAttribute" ? attr.name : null).filter(Boolean);
|
|
if (!attributeNames.includes("server:component-directive")) {
|
|
node.attributes.push({
|
|
type: "mdxJsxAttribute",
|
|
name: "server:component-directive",
|
|
value: "server:defer"
|
|
});
|
|
}
|
|
if (!attributeNames.includes("server:component-path")) {
|
|
node.attributes.push({
|
|
type: "mdxJsxAttribute",
|
|
name: "server:component-path",
|
|
value: resolvedPath
|
|
});
|
|
}
|
|
if (!attributeNames.includes("server:component-export")) {
|
|
if (meta.name === "*") {
|
|
meta.name = node.name.split(".").slice(1).join(".");
|
|
}
|
|
node.attributes.push({
|
|
type: "mdxJsxAttribute",
|
|
name: "server:component-export",
|
|
value: meta.name
|
|
});
|
|
}
|
|
}
|
|
export {
|
|
getAstroMetadata,
|
|
rehypeAnalyzeAstroMetadata
|
|
};
|