@mui/internal-docs-infra
Version:
MUI Infra - internal documentation creation tools.
183 lines (166 loc) • 7.92 kB
JavaScript
import { formatProperties, formatEnum, parseMarkdownToHast, applyDescriptionReplacements } from "./format.mjs";
import { isComponentType } from "./typeGuards.mjs";
import { rewriteTypeStringsDeep } from "./rewriteTypes.mjs";
import * as memberOrder from "../loadServerTypesText/order.mjs";
/**
* Complete component type metadata for documentation.
*/
/**
* Options for customizing component data formatting.
*/
/**
* Formats a TypeScript component export into structured documentation metadata.
*
* This function extracts and formats all relevant component information including
* props, data attributes, and CSS variables. It also applies post-processing to
* normalize type names across re-exports and hide internal implementation details.
*
* The component must be validated with `isPublicComponent()` before calling this function.
*/
export async function formatComponentData(component, allExports, typeNameMap, rewriteContext, options = {}) {
const {
dataAttributesSuffix = 'DataAttributes',
cssVariablesSuffix = 'CssVars',
descriptionReplacements,
formatting,
externalTypes
} = options;
const {
exportNames
} = rewriteContext;
const descriptionText = component.documentation?.description ? applyDescriptionReplacements(component.documentation.description, descriptionReplacements) : undefined;
const description = descriptionText ? await parseMarkdownToHast(descriptionText) : undefined;
// Find data attributes and CSS variables in a single loop
let dataAttributes;
let cssVariables;
// For DataAttributes/CssVars lookup, use the originalName (before transformations).
// Example for re-exported component:
// - Component: ContextMenu.Backdrop (transformed from MenuBackdrop)
// - originalName: MenuBackdrop
// - We look for: MenuBackdropDataAttributes (using originalName + suffix)
// - That export's originalName will also be MenuBackdropDataAttributes
const originalName = component.originalName;
const componentNameForLookup = originalName || component.name.replace(/\./g, '');
const dataAttributesName = `${componentNameForLookup}${dataAttributesSuffix}`;
const cssVariablesName = `${componentNameForLookup}${cssVariablesSuffix}`;
// Get the component's short name (e.g., "Trigger" from "AlertDialog.Trigger")
const componentShortName = component.name.split('.').pop() || component.name;
const dataAttributesSuffixWithShortName = `${componentShortName}${dataAttributesSuffix}`;
const cssVariablesSuffixWithShortName = `${componentShortName}${cssVariablesSuffix}`;
// Look for DataAttributes/CssVars by checking originalName on each export
// First pass: exact match
for (const node of allExports) {
const nodeOriginalName = node.originalName;
const nodeName = nodeOriginalName || node.name;
if (nodeName === dataAttributesName) {
dataAttributes = node;
} else if (nodeName === cssVariablesName) {
cssVariables = node;
}
// Early exit if we found both
if (dataAttributes && cssVariables) {
break;
}
}
// Fallback: For re-exported components (like AlertDialog.Trigger which re-exports DialogTrigger),
// the DataAttributes file uses the original component name (DialogTriggerDataAttributes).
// If we didn't find an exact match, look for any DataAttributes ending with the component's
// short name (e.g., "TriggerDataAttributes").
// Priority: prefer DataAttributes whose prefix is contained in the component's namespace
// (e.g., for AlertDialog.Trigger, prefer DialogTriggerDataAttributes over MenuTriggerDataAttributes)
if (!dataAttributes || !cssVariables) {
// Get the component's namespace (e.g., "AlertDialog" from "AlertDialog.Trigger")
const componentNamespace = component.name.includes('.') ? component.name.substring(0, component.name.lastIndexOf('.')) : '';
// Collect all matching candidates
const dataAttributesCandidates = [];
const cssVariablesCandidates = [];
for (const node of allExports) {
const nodeOriginalName = node.originalName;
const nodeName = nodeOriginalName || node.name;
// Check if this export ends with the component's short name + suffix
if (!dataAttributes && nodeName.endsWith(dataAttributesSuffixWithShortName)) {
// Extract the prefix (e.g., "Dialog" from "DialogTriggerDataAttributes")
const prefix = nodeName.slice(0, -dataAttributesSuffixWithShortName.length);
// Priority: 2 if prefix is contained in namespace (related component), 1 otherwise
const priority = componentNamespace.includes(prefix) ? 2 : 1;
dataAttributesCandidates.push({
node,
priority
});
}
if (!cssVariables && nodeName.endsWith(cssVariablesSuffixWithShortName)) {
const prefix = nodeName.slice(0, -cssVariablesSuffixWithShortName.length);
const priority = componentNamespace.includes(prefix) ? 2 : 1;
cssVariablesCandidates.push({
node,
priority
});
}
}
// Select the highest priority candidate
if (!dataAttributes && dataAttributesCandidates.length > 0) {
dataAttributesCandidates.sort((a, b) => b.priority - a.priority);
dataAttributes = dataAttributesCandidates[0].node;
}
if (!cssVariables && cssVariablesCandidates.length > 0) {
cssVariablesCandidates.sort((a, b) => b.priority - a.priority);
cssVariables = cssVariablesCandidates[0].node;
}
}
const raw = {
name: component.name,
description,
descriptionText,
props: sortObjectByKeys(await formatProperties(component.type.props, {
exportNames,
typeNameMap,
isComponentContext: true,
formatting,
externalTypes,
descriptionReplacements
}), options.ordering?.props ?? memberOrder.props),
dataAttributes: dataAttributes && dataAttributes.type.kind === 'enum' ? sortObjectByKeys(await formatEnum(dataAttributes.type, descriptionReplacements), options.ordering?.dataAttributes ?? memberOrder.dataAttributes) : {},
cssVariables: cssVariables && cssVariables.type.kind === 'enum' ? sortObjectByKeys(await formatEnum(cssVariables.type, descriptionReplacements), options.ordering?.cssVariables ?? memberOrder.cssVariables) : {}
};
// Post-process type strings to align naming across re-exports and hide internal suffixes.
return rewriteTypeStringsDeep(raw, rewriteContext);
}
/**
* Type guard to check if an export is a public component that should be documented.
*
* A component is considered public if it's a ComponentNode, doesn't have an @ignore tag,
* and is marked as public (not @internal). Use this to filter components before passing
* them to `formatComponentData()`.
*/
export function isPublicComponent(exportNode) {
const isPublic = exportNode.documentation?.visibility !== 'private' && exportNode.documentation?.visibility !== 'internal';
const hasIgnoreTag = exportNode.documentation?.tags?.some(tag => tag.name === 'ignore');
return isComponentType(exportNode.type) && !hasIgnoreTag && isPublic;
}
function sortObjectByKeys(obj, order) {
if (order.length === 0) {
return obj;
}
const sortedObj = {};
const everythingElse = {};
// Gather keys that are not in the order array
Object.keys(obj).forEach(key => {
if (!order.includes(key)) {
everythingElse[key] = obj[key];
}
});
// Sort the keys of everythingElse
const sortedEverythingElseKeys = Object.keys(everythingElse).sort();
// Populate the sorted object according to the order array
order.forEach(key => {
if (key === '__EVERYTHING_ELSE__') {
// Insert all "everything else" keys at this position, sorted
sortedEverythingElseKeys.forEach(sortedKey => {
sortedObj[sortedKey] = everythingElse[sortedKey];
});
} else if (obj.hasOwnProperty(key)) {
sortedObj[key] = obj[key];
}
});
return sortedObj;
}