@mintlify/common
Version:
Commonly shared code within Mintlify
136 lines (135 loc) • 5.69 kB
JavaScript
import { visit } from 'unist-util-visit';
import { isExportNode, isMdxJsEsm } from '../../utils.js';
// Underscore-prefixed so it cannot collide with a user-authored MDX component name. The client
// (MdxContent) maps this to an error boundary; see apps/client/src/ui/MdxContent.tsx.
export const MDX_COMPONENT_BOUNDARY_NAME = '_MdxComponentBoundary';
/**
* Collects the names of function-valued exports declared in the MDX itself — `export function X`,
* `export const X = () => …` / `function(){}`, and re-exported local functions (`export { X }`).
* These are the user-authored components we treat as renderable (and, in remarkMdxRemoveUnknownJsx,
* as "known"), as opposed to exported plain values which are not components.
*/
export function getExportedFunctionNames(tree) {
const names = new Set();
visit(tree, 'mdxjsEsm', (node) => {
var _a, _b, _c, _d, _e, _f;
if (!isMdxJsEsm(node) || !((_b = (_a = node.data) === null || _a === void 0 ? void 0 : _a.estree) === null || _b === void 0 ? void 0 : _b.body))
return;
const localFunctionNames = new Set();
for (const bodyChild of node.data.estree.body) {
if (bodyChild.type === 'FunctionDeclaration' && ((_c = bodyChild.id) === null || _c === void 0 ? void 0 : _c.name)) {
localFunctionNames.add(bodyChild.id.name);
}
if (bodyChild.type === 'VariableDeclaration') {
addExportedVariableFunctionNames(bodyChild, localFunctionNames);
}
}
for (const bodyChild of node.data.estree.body) {
if (!isExportNode(bodyChild) || bodyChild.type === 'ExportAllDeclaration')
continue;
if (((_d = bodyChild.declaration) === null || _d === void 0 ? void 0 : _d.type) === 'FunctionDeclaration') {
if ((_e = bodyChild.declaration.id) === null || _e === void 0 ? void 0 : _e.name)
names.add(bodyChild.declaration.id.name);
continue;
}
if (((_f = bodyChild.declaration) === null || _f === void 0 ? void 0 : _f.type) === 'VariableDeclaration') {
addExportedVariableFunctionNames(bodyChild.declaration, names);
continue;
}
if (bodyChild.type === 'ExportNamedDeclaration') {
addExportedSpecifierNames(bodyChild, names, localFunctionNames);
}
}
});
return names;
}
function addExportedVariableFunctionNames(declaration, names) {
var _a, _b;
for (const declarator of declaration.declarations) {
if (declarator.id.type !== 'Identifier')
continue;
if (((_a = declarator.init) === null || _a === void 0 ? void 0 : _a.type) === 'ArrowFunctionExpression' ||
((_b = declarator.init) === null || _b === void 0 ? void 0 : _b.type) === 'FunctionExpression') {
names.add(declarator.id.name);
}
}
}
function addExportedSpecifierNames(node, names, localFunctionNames) {
for (const specifier of node.specifiers) {
if (localFunctionNames.has(specifier.local.name)) {
names.add(specifier.exported.name);
}
}
}
function shouldWrapComponent(name, componentNames) {
if (!name)
return false;
if (name === MDX_COMPONENT_BOUNDARY_NAME)
return false;
return componentNames.has(name);
}
function wrapFlowNode(node) {
var _a;
return {
type: 'mdxJsxFlowElement',
name: MDX_COMPONENT_BOUNDARY_NAME,
attributes: [
{
type: 'mdxJsxAttribute',
name: 'name',
value: (_a = node.name) !== null && _a !== void 0 ? _a : 'MDX component',
},
],
children: [node],
};
}
function wrapTextNode(node) {
var _a;
return {
type: 'mdxJsxTextElement',
name: MDX_COMPONENT_BOUNDARY_NAME,
attributes: [
{
type: 'mdxJsxAttribute',
name: 'name',
value: (_a = node.name) !== null && _a !== void 0 ? _a : 'MDX component',
},
],
children: [node],
};
}
/**
* Wraps each user-authored component usage in a `_MdxComponentBoundary` element so the client can
* isolate it behind an error boundary — a single component that throws at render time then renders
* nothing instead of crashing the whole page.
*/
export const remarkMdxClientComponentBoundaries = ({ allowlist = [] } = {}) => (tree) => {
const componentNames = getExportedFunctionNames(tree);
for (const name of allowlist) {
componentNames.add(name);
}
if (componentNames.size === 0)
return;
const flowReplacements = [];
const textReplacements = [];
visit(tree, 'mdxJsxFlowElement', (node, index, parent) => {
if (index === undefined || !(parent === null || parent === void 0 ? void 0 : parent.children))
return;
if (!shouldWrapComponent(node.name, componentNames))
return;
flowReplacements.push({ parent, index, node });
});
visit(tree, 'mdxJsxTextElement', (node, index, parent) => {
if (index === undefined || !(parent === null || parent === void 0 ? void 0 : parent.children))
return;
if (!shouldWrapComponent(node.name, componentNames))
return;
textReplacements.push({ parent, index, node });
});
for (const { parent, index, node } of flowReplacements) {
parent.children[index] = wrapFlowNode(node);
}
for (const { parent, index, node } of textReplacements) {
parent.children[index] = wrapTextNode(node);
}
};