@mui/internal-docs-infra
Version:
MUI Infra - internal documentation creation tools.
242 lines (218 loc) • 10.4 kB
JavaScript
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;
}