@mintlify/common
Version:
Commonly shared code within Mintlify
230 lines (229 loc) • 10.1 kB
JavaScript
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);
};