UNPKG

@mui/internal-docs-infra

Version:

MUI Infra - internal documentation creation tools.

183 lines (166 loc) 7.92 kB
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; }