UNPKG

@mintlify/common

Version:

Commonly shared code within Mintlify

230 lines (229 loc) 10.1 kB
function isMdxJsxElement(node) { return node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement'; } function getVisibilityFor(node) { for (const attr of node.attributes) { if (attr.type !== 'mdxJsxAttribute' || attr.name !== 'for') continue; if (typeof attr.value === 'string') { if (attr.value === 'humans' || attr.value === 'agents') return attr.value; continue; } if (attr.value && typeof attr.value === 'object' && 'value' in attr.value && typeof attr.value.value === 'string') { const raw = attr.value.value.trim(); const quoted = raw.match(/^(["'])(humans|agents)\1$/); if (quoted) return quoted[2]; } } return undefined; } // `export * from` is intentionally ignored: it exposes no statically-knowable // names and creates no page-local JSX bindings in MDX, so there is nothing to // collect. function collectEsmIdentifiers(node) { var _a, _b, _c; const names = new Set(); if (node.type !== 'mdxjsEsm') return names; for (const statement of (_c = (_b = (_a = node.data) === null || _a === void 0 ? void 0 : _a.estree) === null || _b === void 0 ? void 0 : _b.body) !== null && _c !== void 0 ? _c : []) { if (statement.type === 'ImportDeclaration') { for (const specifier of statement.specifiers) { names.add(specifier.local.name); } } if (statement.type === 'ExportNamedDeclaration') { if (statement.declaration) { if (statement.declaration.type === 'VariableDeclaration') { for (const declarator of statement.declaration.declarations) { if (declarator.id.type === 'Identifier') { names.add(declarator.id.name); } } } else if (statement.declaration.id) { names.add(statement.declaration.id.name); } } // Re-exports: `export { Panel } from '...'` keep names in specifiers for (const specifier of statement.specifiers) { if (specifier.local.type === 'Identifier') { names.add(specifier.local.name); } if (specifier.exported.type === 'Identifier') { names.add(specifier.exported.name); } } } if (statement.type === 'ExportDefaultDeclaration') { const declaration = statement.declaration; if ((declaration.type === 'FunctionDeclaration' || declaration.type === 'ClassDeclaration') && declaration.id) { names.add(declaration.id.name); } } } return names; } function isRecord(value) { return typeof value === 'object' && value !== null; } function getJsxReferenceBaseName(name) { let current = name; while (isRecord(current) && current.type === 'JSXMemberExpression') { current = current.object; } if (isRecord(current) && current.type === 'JSXIdentifier' && typeof current.name === 'string') { return current.name; } return undefined; } // Walks an estree for JSX component references, e.g. `<Logo />` within // `export const Header = () => <Logo />` or `{<Demo />}` expressions. function collectEstreeJsxNames(estree, names) { const queue = [estree]; const seen = new Set(); while (queue.length > 0) { const current = queue.pop(); if (Array.isArray(current)) { queue.push(...current); continue; } if (!isRecord(current) || seen.has(current)) continue; seen.add(current); if (current.type === 'JSXOpeningElement') { const baseName = getJsxReferenceBaseName(current.name); // Lowercase JSX names are intrinsic elements, never component bindings. if (baseName && /^[A-Z]/.test(baseName)) names.add(baseName); } queue.push(...Object.values(current)); } } // JSX rendered inside an ESM node's own declarations, so stripping `Header` // can also pull in helpers that only exist to serve it. function collectJsxReferences(node) { var _a; const names = new Set(); if (node.type === 'mdxjsEsm') { collectEstreeJsxNames((_a = node.data) === null || _a === void 0 ? void 0 : _a.estree, names); } return names; } // Classifies every JSX component usage on the page: names used only inside // `<Visibility for="humans">` land in `hiddenNames`, everything else in // `visibleNames`. Usage inside an agents block counts as visible since that // content survives into the markdown output. function collectJsxUsage(nodes, insideHumans, hiddenNames, visibleNames) { var _a, _b; for (const node of nodes) { let childInsideHumans = insideHumans; if (isMdxJsxElement(node)) { if (node.name === 'Visibility') { if (getVisibilityFor(node) === 'humans') childInsideHumans = true; } else { const baseName = (_a = node.name) === null || _a === void 0 ? void 0 : _a.split('.')[0]; // Lowercase JSX names are intrinsic elements, never component bindings. if (baseName && /^[A-Z]/.test(baseName)) { (insideHumans ? hiddenNames : visibleNames).add(baseName); } } } // Expression-embedded usages like `{<Demo />}` live in estree, not mdast. if (node.type === 'mdxFlowExpression' || node.type === 'mdxTextExpression') { collectEstreeJsxNames((_b = node.data) === null || _b === void 0 ? void 0 : _b.estree, insideHumans ? hiddenNames : visibleNames); } if ('children' in node && Array.isArray(node.children)) { collectJsxUsage(node.children, childInsideHumans, hiddenNames, visibleNames); } } } function transformChildren(children, esmToStrip) { return children.flatMap((child) => transformNode(child, esmToStrip)); } function transformNode(node, esmToStrip) { // Snippet definitions are only stripped when every usage of the components // they declare sits inside a `<Visibility for="humans">` block; JSX that the // author left visible keeps its definition in the markdown output. if (node.type === 'mdxjsEsm') { return esmToStrip.has(node) ? [] : [node]; } if (isMdxJsxElement(node) && node.name === 'Visibility') { const audience = getVisibilityFor(node); if (audience === 'humans') { return []; } if (audience === 'agents') { return transformChildren(node.children, esmToStrip); } } if ('children' in node && Array.isArray(node.children)) { const next = transformChildren(node.children, esmToStrip); node.children = next; } return [node]; } export const remarkVisibilityForMarkdown = () => (tree) => { var _a, _b, _c, _d; const hiddenNames = new Set(); const visibleNames = new Set(); collectJsxUsage(tree.children, false, hiddenNames, visibleNames); const esmNodes = tree.children.filter((child) => child.type === 'mdxjsEsm'); const declaredNames = new Map(esmNodes.map((node) => [node, [...collectEsmIdentifiers(node)]])); const referencedNames = new Map(esmNodes.map((node) => [node, [...collectJsxReferences(node)]])); const esmToStrip = new Set(); // Keep-alive fixed point: a node with any visibly-used name keeps every // name it declares or renders visible, so a visible `<PanelAlias />` keeps // the `export const Panel = ...` node it aliases, and a kept `Footer` keeps // the `Logo` helper it renders, even when both live in separate ESM nodes. let visibleChanged = true; while (visibleChanged) { visibleChanged = false; for (const node of esmNodes) { const declared = (_a = declaredNames.get(node)) !== null && _a !== void 0 ? _a : []; if (!declared.some((name) => visibleNames.has(name))) continue; for (const name of [...declared, ...((_b = referencedNames.get(node)) !== null && _b !== void 0 ? _b : [])]) { if (!visibleNames.has(name)) { visibleNames.add(name); visibleChanged = true; } } } } // Fixed point: stripping a node marks everything it declares or renders as // hidden, so `export { Panel as PanelAlias }` pulls in the `export const // Panel = ...` it aliases, and a stripped `Header` pulls in the `Logo` // helper rendered only inside its body, instead of leaking their source. let changed = true; while (changed) { changed = false; for (const node of esmNodes) { if (esmToStrip.has(node)) continue; const names = (_c = declaredNames.get(node)) !== null && _c !== void 0 ? _c : []; // Declared-but-never-rendered names don't block stripping: when a node // mixes a hidden component with unused names, hiding wins over leaking. const hiddenOnly = names.some((name) => hiddenNames.has(name)) && !names.some((name) => visibleNames.has(name)); if (!hiddenOnly) continue; esmToStrip.add(node); // Visible names stay out of hiddenNames so the two sets stay disjoint. for (const name of [...names, ...((_d = referencedNames.get(node)) !== null && _d !== void 0 ? _d : [])]) { if (!hiddenNames.has(name) && !visibleNames.has(name)) { hiddenNames.add(name); changed = true; } } } } tree.children = transformChildren(tree.children, esmToStrip); };