UNPKG

@mui/internal-docs-infra

Version:

MUI Infra - internal documentation creation tools.

242 lines (218 loc) 10.4 kB
import { prettyFormat, parseMarkdownToHast, applyDescriptionReplacements, formatProperties, extractTypeParameters } from "./format.mjs"; import { formatType } from "./formatType.mjs"; import { isEnumType, isObjectType } from "./typeGuards.mjs"; import { rewriteTypeStringsDeep } from "./rewriteTypes.mjs"; /** * Information about a re-exported type. */ /** * Formatted raw type metadata with the type declaration as a formatted code string. * * Used for types that don't fit into component/hook/function categories, * such as type aliases, interfaces, and enums. * * Type highlighting (formattedCode → HAST) is deferred to the loadServerTypes * stage via highlightTypesMeta() after highlightTypes(). */ /** * Enum member metadata for raw type enum rendering. */ /** * Formats a raw type export into a structured metadata object with formatted code. * * @param exportNode - The export node from typescript-api-extractor * @param displayName - The display name (e.g., "Component.Root.State") * @param typeNameMap - Map for transforming type names * @param rewriteContext - Context for type string rewriting * @param options - Formatting options * @returns Formatted raw type metadata with the type declaration as formatted code */ export async function formatRawData(exportNode, displayName, typeNameMap, rewriteContext, _options = {}) { const { descriptionReplacements } = _options; const descriptionText = exportNode.documentation?.description ? applyDescriptionReplacements(exportNode.documentation.description, descriptionReplacements) : undefined; const description = descriptionText ? await parseMarkdownToHast(descriptionText) : undefined; // Handle enum types specially - they get a table of members if (isEnumType(exportNode.type) && exportNode.type.members && exportNode.type.members.length > 0) { const enumMembers = await Promise.all(exportNode.type.members.map(async member => { const memberDescriptionText = member.documentation?.description ? applyDescriptionReplacements(member.documentation.description, descriptionReplacements) : undefined; return { name: member.name, value: member.value, description: memberDescriptionText ? await parseMarkdownToHast(memberDescriptionText) : undefined, descriptionText: memberDescriptionText }; })); // For enums, still generate the code block but also include members const formattedCode = await generateFormattedCode(exportNode, displayName, typeNameMap); // Rewrite type names in descriptions (but NOT in formattedCode which is valid TypeScript syntax) const rewrittenDescriptionText = descriptionText ? rewriteTypeStringsDeep(descriptionText, rewriteContext) : undefined; const raw = { name: displayName, description, descriptionText: rewrittenDescriptionText, formattedCode, enumMembers }; return raw; } // Handle DataAttributes types if (displayName.endsWith('.DataAttributes')) { const componentName = displayName.replace('.DataAttributes', ''); const formattedCode = await generateFormattedCode(exportNode, displayName, typeNameMap); const rewrittenDescriptionText = descriptionText ? rewriteTypeStringsDeep(descriptionText, rewriteContext) : undefined; const raw = { name: displayName, description, descriptionText: rewrittenDescriptionText, formattedCode, dataAttributesOf: componentName }; return raw; } // Handle CssVars types if (displayName.endsWith('.CssVars')) { const componentName = displayName.replace('.CssVars', ''); const formattedCode = await generateFormattedCode(exportNode, displayName, typeNameMap); const rewrittenDescriptionText = descriptionText ? rewriteTypeStringsDeep(descriptionText, rewriteContext) : undefined; const raw = { name: displayName, description, descriptionText: rewrittenDescriptionText, formattedCode, cssVarsOf: componentName }; return raw; } // Generate formatted code for regular types const formattedCode = await generateFormattedCode(exportNode, displayName, typeNameMap, _options.externalTypes); const rewrittenDescriptionText = descriptionText ? rewriteTypeStringsDeep(descriptionText, rewriteContext) : undefined; const raw = { name: displayName, description, descriptionText: rewrittenDescriptionText, formattedCode }; // For object types with properties, extract structured property data. // This allows the enhancement stage to convert named return type references // (e.g., `AutocompleteFilter`) into property tables. if (isObjectType(exportNode.type) && exportNode.type.properties && exportNode.type.properties.length > 0) { const { exportNames } = rewriteContext; raw.properties = rewriteTypeStringsDeep(await formatProperties(exportNode.type.properties, { exportNames, typeNameMap, externalTypes: _options.externalTypes, descriptionReplacements }), rewriteContext); } return raw; } /** * Generate the formatted code string for a type declaration. */ async function generateFormattedCode(exportNode, displayName, typeNameMap, externalTypesCollector) { const typeAsAny = exportNode.type; // Compute the original flat type name for the declaration. // The displayName uses dots (e.g., "Toolbar.Root.State") but the actual // TypeScript declaration needs the original name without dots (e.g., "ToolbarRootState"). // // We prefer displayName with dots removed because: // - exportNode.name for namespaced exports is just the short name ("State") // - displayName.replace(/\./g, '') gives us the full concatenated name ("ToolbarRootState") const originalTypeName = displayName.replace(/\./g, ''); // Handle typeAlias types if (typeAsAny.kind === 'typeAlias' && typeof typeAsAny.typeText === 'string') { // Prefer typeText over expandedTypeText to preserve type alias references // (e.g., show `ToastManagerEvent` instead of fully expanding it) let sourceTypeText = typeAsAny.typeText; // Detect circular references: when typeText references the type we're defining // (e.g., `type ToastActionState = Toast.Action.State` where both resolve to the same type) // In this case, use expandedTypeText to show the actual structure instead of a self-reference if (typeAsAny.expandedTypeText) { // Normalize both names for comparison (remove dots to handle namespaced names) // "Toast.Action.State" -> "ToastActionState" const typeTextNormalized = sourceTypeText.replace(/\./g, ''); const displayNameNormalized = displayName.replace(/\./g, ''); // It's a circular reference if the normalized names match const isCircularReference = typeTextNormalized === displayNameNormalized; // Also use expandedTypeText when typeText is a `typeof` expression // (e.g., `typeof DEFAULT_COORDS` should expand to `{ x: number; y: number }`) const isTypeofExpression = sourceTypeText.startsWith('typeof '); if (isCircularReference || isTypeofExpression) { sourceTypeText = typeAsAny.expandedTypeText; } } // Sanitize extremely complex iterator types if (sourceTypeText.includes('@iterator') && sourceTypeText.length > 500) { sourceTypeText = 'any[]'; } // Transform type names using typeNameMap let transformedTypeText = sourceTypeText; if (typeNameMap) { const namespaceMatch = displayName.match(/^([^.]+)\./); const currentNamespace = namespaceMatch ? namespaceMatch[1] : null; if (currentNamespace) { for (const [, dottedName] of Object.entries(typeNameMap)) { const nameParts = dottedName.split('.'); if (nameParts.length >= 2 && nameParts[0] === currentNamespace) { const memberName = nameParts.slice(1).join('.'); const memberPattern = `\\w+\\.${memberName.replace(/\./g, '\\.')}`; const regex = new RegExp(memberPattern, 'g'); transformedTypeText = transformedTypeText.replace(regex, dottedName); } } } } const typeParams = typeAsAny.typeParameters || extractTypeParameters(exportNode.type, typeNameMap); const fullTypeName = `${originalTypeName}${typeParams}`; return prettyFormat(transformedTypeText, fullTypeName); } // For non-typeAlias types (interfaces, etc.), use formatType const typeParams = extractTypeParameters(exportNode.type, typeNameMap); const fullTypeName = `${originalTypeName}${typeParams}`; return prettyFormat(formatType(exportNode.type, { removeUndefined: true, expandObjects: true, exportNames: [], typeNameMap, externalTypesCollector, selfName: originalTypeName, withPropertyComments: true, preserveTypeParameters: typeParams.length > 0 }), fullTypeName); } /** * Type guard to check if an export node represents a "raw" type that should be * formatted as a code block (i.e., not a component, hook, or function). * * @param exportNode - The export node to check * @param isComponent - Whether the node has been identified as a component * @param isHook - Whether the node has been identified as a hook * @param isFunction - Whether the node has been identified as a function * @returns true if the export should be formatted as a raw type */ export function isRawType(exportNode, isComponent, isHook, isFunction) { return !isComponent && !isHook && !isFunction; } /** * Formats re-export information for a type that re-exports another component's props. */ export async function formatReExportData(exportNode, displayName, reExportOf, typeNameMap, rewriteContext) { const descriptionText = exportNode.documentation?.description; const description = descriptionText ? await parseMarkdownToHast(descriptionText) : undefined; // Still generate the code for reference const formattedCode = await generateFormattedCode(exportNode, displayName, typeNameMap); // Rewrite type names in descriptions (but NOT in formattedCode which is valid TypeScript syntax) const rewrittenDescriptionText = descriptionText ? rewriteTypeStringsDeep(descriptionText, rewriteContext) : undefined; const raw = { name: displayName, description, descriptionText: rewrittenDescriptionText, formattedCode, reExportOf }; return raw; }