@mui/internal-docs-infra
Version:
MUI Infra - internal documentation creation tools.
771 lines (708 loc) • 35.6 kB
JavaScript
import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkStringify from 'remark-stringify';
import remarkGfm from 'remark-gfm';
import * as md from "../syncPageIndex/createMarkdownNodes.mjs";
import { prettyFormat, prettyFormatMarkdown } from "../loadServerTypesMeta/format.mjs";
/**
* Strip trailing `| undefined` from a type string.
* Used for cleaner markdown display of optional props/params.
*/
function stripTrailingUndefined(typeText) {
return typeText.endsWith(' | undefined') ? typeText.slice(0, -' | undefined'.length) : typeText;
}
/**
* Parse a markdown string into an AST
* @param {string} markdown - Markdown string to parse
* @returns {Object} The root content node of the parsed AST
*/
function parseMarkdown(markdown) {
// Parse markdown into an AST
const processor = unified().use(remarkParse);
const result = processor.parse(markdown);
return result.children;
}
// Phrasing content types that are allowed in table cells
const PHRASING_TYPES = new Set(['text', 'inlineCode', 'emphasis', 'strong', 'link', 'break', 'delete' // strikethrough
]);
/**
* Recursively extract phrasing content from any node.
* Block-level nodes are flattened to their inline content.
* Adds line breaks between block-level siblings.
*/
function extractPhrasingContent(node, result, isTopLevel = false) {
if (PHRASING_TYPES.has(node.type)) {
result.push(node);
} else if ('children' in node && Array.isArray(node.children)) {
// Add line breaks between top-level block elements (paragraphs, etc.)
if (isTopLevel && result.length > 0) {
result.push(md.hardBreak());
result.push(md.hardBreak());
}
for (const child of node.children) {
extractPhrasingContent(child, result, false);
}
}
}
/**
* Parse a markdown string and extract only the inline content (for table cells).
* Block-level elements are flattened to their inline content.
* @param markdown - Markdown string to parse
* @returns Array of phrasing content nodes
*/
function parseInlineMarkdown(markdown) {
const nodes = parseMarkdown(markdown);
const result = [];
for (const node of nodes) {
extractPhrasingContent(node, result, true);
}
return result.length > 0 ? result : [md.text(markdown)];
}
/**
* Stringify AST nodes to markdown using remark.
* Note: remark-stringify will escape underscores in text nodes.
*/
function stringifyToMarkdown(nodes) {
const root = {
type: 'root',
children: nodes
};
return unified().use(remarkGfm).use(remarkStringify, {
bullet: '-',
emphasis: '*',
strong: '*',
fence: '`',
fences: true,
listItemIndent: 'one',
rule: '-',
quote: "'"
}).stringify(root);
}
/**
* A chunk of markdown content.
* - `needsPrettier: true` - needs to be formatted with prettier's markdown parser
* - `needsPrettier: false` - already formatted (e.g., code blocks that went through prettyFormat)
*/
/** Create a markdown chunk from AST nodes (needs prettier formatting) */
function markdownChunk(nodes) {
const content = stringifyToMarkdown(nodes);
return {
content: `${content.trimEnd()}\n`,
needsPrettier: true
};
}
/** Create a code block chunk (already formatted, skip prettier) */
function codeBlockChunk(code, language) {
// Use quad backticks when the code contains triple backticks (e.g., JSDoc @example blocks)
const fence = code.includes('```') ? '````' : '```';
return {
content: `${fence}${language}\n${code}\n${fence}\n`,
needsPrettier: false
};
}
/**
* Create a heading chunk as a raw string (bypasses remark-gfm escaping).
* remark-gfm escapes `w.` patterns (e.g., "Arrow.Props" → "Arrow\.Props")
* because they look like www. URLs. Headings don't need GFM features,
* so we build them directly as strings.
*/
function headingChunk(depth, text) {
const prefix = '#'.repeat(depth);
return {
content: `${prefix} ${text}\n`,
needsPrettier: false
};
}
/**
* Options for generating types markdown.
*/
export async function generateTypesMarkdown(options) {
const {
name,
organized,
typeNameMap = {},
externalTypes = {},
path: filePath
} = options;
// Build the autogenerated comment with an optional validate command hint
const normalizedPath = typeof filePath === 'string' ? filePath.replace(/\\/g, '/') : undefined;
const trimmedPath = normalizedPath?.replace(/^(src\/app\/|app\/)/, '').replace(/\/types\.md$/, '');
const quotedPath = trimmedPath && /[()]/.test(trimmedPath) ? `"${trimmedPath}"` : trimmedPath;
const commentText = quotedPath ? `<-- Autogenerated By (do not edit the following markdown directly), run: pnpm docs:validate ${quotedPath}` : '<-- Autogenerated By (do not edit the following markdown directly)';
// Header chunk
const headerChunk = markdownChunk([md.heading(1, name), md.comment(commentText, 'types.ts'), md.heading(2, 'API Reference')]);
// Get the organized exports and additional types (already sorted by organizeTypesByExport)
const {
exports: organizedExports
} = organized;
// Merge additionalTypes with variantOnlyAdditionalTypes for the markdown output.
// The types.md is a flat reference document that should show all types,
// while the separation only matters for React component rendering.
const additionalTypes = [...organized.additionalTypes, ...Object.values(organized.variantOnlyAdditionalTypes).flat()];
// Collect all types for common prefix detection
const allTypes = [...Object.values(organizedExports).map(exportEntry => exportEntry.type), ...Object.values(organizedExports).flatMap(exportEntry => exportEntry.additionalTypes)];
// For blocks pattern: determine if we should strip the common prefix from component names
// E.g., if we have "Component.Root", "Component.Part" but NO standalone "Component" component,
// strip the "Component." prefix so they display as just "Root", "Part"
// However, if we have multiple namespaces like "Button.Root" and "Checkbox.Root",
// keep the prefixes to distinguish them
// Find all dotted component names
const dottedComponents = allTypes.filter(t => t.type === 'component' || t.type === 'hook').map(t => t.name).filter(componentName => componentName.includes('.'));
let commonPrefix = null;
if (dottedComponents.length > 0) {
// Extract the prefix before the first dot from each name
const prefixes = dottedComponents.map(componentName => componentName.split('.')[0]);
// Get unique prefixes
const uniquePrefixes = Array.from(new Set(prefixes));
// Only strip the prefix if ALL components share the SAME prefix
if (uniquePrefixes.length === 1) {
const singlePrefix = uniquePrefixes[0];
// Check if there's a standalone component with this exact name
const hasStandaloneComponent = allTypes.some(t => {
if (t.type !== 'component' && t.type !== 'hook') {
return false;
}
if (t.name !== singlePrefix) {
return false;
}
// For components: check if it has actual content
if (t.type === 'component') {
const hasProps = t.data.props && Object.keys(t.data.props).length > 0;
const hasDataAttrs = t.data.dataAttributes && Object.keys(t.data.dataAttributes).length > 0;
const hasCssVars = t.data.cssVariables && Object.keys(t.data.cssVariables).length > 0;
return hasProps || hasDataAttrs || hasCssVars || !!t.data.description;
}
return true; // Hooks always have content
});
if (!hasStandaloneComponent) {
commonPrefix = singlePrefix;
}
}
// If there are multiple unique prefixes, don't strip anything
}
// Helper function to generate markdown chunks for a single type
// When useFullName is true, the type name is not stripped of the common prefix
async function generateSingleTypeMarkdown(typeMeta, useFullName = false) {
const chunks = [];
const nodes = [];
// Helper to flush pending nodes to a chunk
const flush = () => {
if (nodes.length > 0) {
chunks.push(markdownChunk([...nodes]));
nodes.length = 0;
}
};
// Helper to add a code block (flushes nodes first)
const addCodeBlock = (code, language) => {
flush();
chunks.push(codeBlockChunk(code, language));
};
// Helper to add a heading (flushes nodes first, bypasses remark-gfm escaping)
const addHeading = (depth, text) => {
flush();
chunks.push(headingChunk(depth, text));
};
// Helper to get display name - either full name or stripped prefix
const getDisplayName = part => {
if (useFullName) {
return part;
}
return commonPrefix && part.startsWith(`${commonPrefix}.`) ? part.slice(commonPrefix.length + 1) : part;
};
if (typeMeta.type === 'component') {
// Use transformed name (e.g., "Component.Part" instead of "ComponentPart")
const part = typeMeta.name;
const data = typeMeta.data; // This is now properly typed as ComponentTypeMeta
// Strip common prefix from component heading if applicable
const displayName = getDisplayName(part);
addHeading(3, displayName);
if (data.descriptionText) {
nodes.push(...parseMarkdown(data.descriptionText));
}
// Props table
if (Object.keys(data.props || {}).length > 0) {
nodes.push(md.paragraph([md.strong(`${displayName} Props:`)]));
const propsRows = Object.entries(data.props).map(([propName, propDef]) => {
// Use * to indicate required props
const propDisplayName = propDef.required ? `${propName}*` : propName;
// Strip `| undefined` from optional props for cleaner markdown display
const displayType = propDef.required ? propDef.typeText : stripTrailingUndefined(propDef.typeText);
return [propDisplayName, displayType ? md.inlineCode(displayType) : '-', propDef.defaultText ? md.inlineCode(propDef.defaultText) : '-', propDef.descriptionText ? parseInlineMarkdown(propDef.descriptionText) : '-'];
});
nodes.push(md.table(['Prop', 'Type', 'Default', 'Description'], propsRows, ['left', 'left', 'left', 'left']));
// Prop examples (after the props table)
const propsWithExamples = Object.entries(data.props).filter(([, propDef]) => propDef.exampleText).map(([propName, propDef]) => {
// Parse the example markdown to extract code block content and language
const codeBlockMatch = propDef.exampleText.match(/```(\w*)\n([\s\S]*?)\n```/);
if (codeBlockMatch) {
return {
propName,
language: codeBlockMatch[1] || 'tsx',
code: codeBlockMatch[2]
};
}
return null;
}).filter(Boolean);
// Format all examples in parallel
const formattedExamples = await Promise.all(propsWithExamples.map(async ({
propName,
language,
code
}) => ({
propName,
language,
formattedCode: await prettyFormat(code, null)
})));
// Add formatted examples to the output
for (const {
propName,
language,
formattedCode
} of formattedExamples) {
nodes.push(md.paragraph([md.strong([md.inlineCode(propName), md.text(' Prop Example:')])]));
addCodeBlock(formattedCode, language);
}
// Prop references (after the prop examples)
const propsWithRefs = Object.entries(data.props).filter(([, propDef]) => propDef.seeText);
for (const [propName, propDef] of propsWithRefs) {
nodes.push(md.paragraph([md.strong([md.inlineCode(propName), md.text(' Prop References:')])]));
nodes.push(...parseMarkdown(propDef.seeText));
}
}
// Data attributes table
if (Object.keys(data.dataAttributes || {}).length > 0) {
nodes.push(md.paragraph([md.strong(`${displayName} Data Attributes:`)]));
const attrRows = Object.entries(data.dataAttributes).map(([attrName, attrDef]) => [attrName, attrDef.type ? md.inlineCode(attrDef.type) : '-', attrDef.descriptionText ? parseInlineMarkdown(attrDef.descriptionText) : '-']);
nodes.push(md.table(['Attribute', 'Type', 'Description'], attrRows, ['left', 'left', 'left']));
}
// CSS variables table
if (Object.keys(data.cssVariables || {}).length > 0) {
nodes.push(md.paragraph([md.strong(`${displayName} CSS Variables:`)]));
const cssRows = Object.entries(data.cssVariables).map(([variableName, variableDef]) => [md.inlineCode(variableName), md.inlineCode(variableDef.type || ''), variableDef.descriptionText ? parseInlineMarkdown(variableDef.descriptionText) : '-']);
nodes.push(md.table(['Variable', 'Type', 'Description'], cssRows, ['left', 'left', 'left']));
}
} else if (typeMeta.type === 'hook') {
// Use transformed name for hooks as well
const part = typeMeta.name;
const data = typeMeta.data; // This is now properly typed as HookTypeMeta
const hookDisplayName = getDisplayName(part);
addHeading(3, hookDisplayName);
if (data.descriptionText) {
nodes.push(...parseMarkdown(data.descriptionText));
}
// Parameters table or Properties table (when single object param was expanded)
const paramsOrProps = data.expandedProperties ?? data.parameters ?? [];
const entries = Array.isArray(paramsOrProps) ? paramsOrProps.map(p => [p.name, p]) : Object.entries(paramsOrProps);
if (entries.length > 0) {
const isProperties = Boolean(data.expandedProperties);
const sectionLabel = isProperties ? 'Properties' : 'Parameters';
const columnLabel = isProperties ? 'Property' : 'Parameter';
nodes.push(md.paragraph([md.strong(`${hookDisplayName} ${sectionLabel}:`)]));
const paramRows = entries.map(([paramName, paramDef]) => {
// Use * to indicate required parameters
const displayName = paramDef.required ? `${paramName}*` : paramName;
// Strip `| undefined` from optional params for cleaner markdown display
const displayType = paramDef.required ? paramDef.typeText : stripTrailingUndefined(paramDef.typeText);
return [displayName, displayType ? md.inlineCode(displayType) : '-', paramDef.defaultText ? md.inlineCode(paramDef.defaultText) : '-', paramDef.descriptionText ? parseInlineMarkdown(paramDef.descriptionText) : '-'];
});
nodes.push(md.table([columnLabel, 'Type', 'Default', 'Description'], paramRows, ['left', 'left', 'left', 'left']));
// Parameter examples (after the parameters table)
const paramsWithExamples = entries.filter(([, paramDef]) => paramDef.exampleText).map(([paramName, paramDef]) => {
const codeBlockMatch = paramDef.exampleText.match(/```(\w*)\n([\s\S]*?)\n```/);
if (codeBlockMatch) {
return {
paramName,
language: codeBlockMatch[1] || 'tsx',
code: codeBlockMatch[2]
};
}
return null;
}).filter(Boolean);
const formattedParamExamples = await Promise.all(paramsWithExamples.map(async ({
paramName,
language,
code
}) => ({
paramName,
language,
formattedCode: await prettyFormat(code, null)
})));
for (const {
paramName,
language,
formattedCode
} of formattedParamExamples) {
nodes.push(md.paragraph([md.strong([md.inlineCode(paramName), md.text(' Parameter Example:')])]));
addCodeBlock(formattedCode, language);
}
// Parameter references (after the parameter examples)
const paramsWithRefs = entries.filter(([, paramDef]) => paramDef.seeText);
for (const [paramName, paramDef] of paramsWithRefs) {
nodes.push(md.paragraph([md.strong([md.inlineCode(paramName), md.text(' Parameter References:')])]));
nodes.push(...parseMarkdown(paramDef.seeText));
}
}
// Return Value
if (data.returnValue) {
nodes.push(md.paragraph([md.strong(`${hookDisplayName} Return Value:`)]));
if (data.returnValueDescriptionText) {
nodes.push(...parseMarkdown(data.returnValueDescriptionText));
}
if (typeof data.returnValue === 'string') {
const typeText = data.returnValueText || data.returnValue;
const formattedReturnType = await prettyFormat(typeText, 'ReturnValue');
addCodeBlock(formattedReturnType, 'tsx');
} else if (typeof data.returnValue === 'object' && Object.keys(data.returnValue).length > 0) {
const returnRows = Object.entries(data.returnValue).map(([returnName, returnDef]) => [returnName, returnDef.typeText ? md.inlineCode(returnDef.typeText) : '-', returnDef.descriptionText ? parseInlineMarkdown(returnDef.descriptionText) : '-']);
nodes.push(md.table(['Property', 'Type', 'Description'], returnRows, ['left', 'left', 'left']));
}
}
} else if (typeMeta.type === 'function') {
const part = typeMeta.data.name;
const data = typeMeta.data;
const displayName = getDisplayName(part);
addHeading(3, displayName);
if (data.descriptionText) {
nodes.push(...parseMarkdown(data.descriptionText));
}
// Parameters or Properties table
const paramsOrProps = data.expandedProperties ?? data.parameters ?? [];
const entries = Array.isArray(paramsOrProps) ? paramsOrProps.map(p => [p.name, p]) : Object.entries(paramsOrProps);
if (entries.length > 0) {
const isProperties = Boolean(data.expandedProperties);
const sectionLabel = isProperties ? 'Properties' : 'Parameters';
const columnLabel = isProperties ? 'Property' : 'Parameter';
if (isProperties) {
// Properties table (expanded from single anonymous object parameter)
nodes.push(md.paragraph([md.strong(`${displayName} ${sectionLabel}:`)]));
const propRows = entries.map(([propName, propDef]) => {
// Use * to indicate required properties
const propDisplayName = propDef.required ? `${propName}*` : propName;
// Strip `| undefined` from optional props for cleaner markdown display
const displayType = propDef.required ? propDef.typeText : stripTrailingUndefined(propDef.typeText);
return [propDisplayName, displayType ? md.inlineCode(displayType) : '-', propDef.defaultText ? md.inlineCode(propDef.defaultText) : '-', propDef.descriptionText ? parseInlineMarkdown(propDef.descriptionText) : '-'];
});
nodes.push(md.table(['Property', 'Type', 'Default', 'Description'], propRows, ['left', 'left', 'left', 'left']));
} else {
// Standard parameters table
nodes.push(md.paragraph([md.strong(`${sectionLabel}:`)]));
const paramRows = entries.map(([paramName, paramDef]) => {
// Use ? to indicate optional parameters (TypeScript convention)
const paramDisplayName = paramDef.optional ? `${paramName}?` : paramName;
// Strip `| undefined` from optional params for cleaner markdown display
const displayType = paramDef.optional ? stripTrailingUndefined(paramDef.typeText) : paramDef.typeText;
return [paramDisplayName, displayType ? md.inlineCode(displayType) : '-', paramDef.defaultText ? md.inlineCode(paramDef.defaultText) : '-', paramDef.descriptionText ? parseInlineMarkdown(paramDef.descriptionText) : '-'];
});
nodes.push(md.table([columnLabel, 'Type', 'Default', 'Description'], paramRows, ['left', 'left', 'left', 'left']));
}
// Parameter/Property examples (after the table)
const paramsWithExamples = entries.filter(([, paramDef]) => paramDef.exampleText).map(([paramName, paramDef]) => {
const codeBlockMatch = paramDef.exampleText.match(/```(\w*)\n([\s\S]*?)\n```/);
if (codeBlockMatch) {
return {
paramName,
language: codeBlockMatch[1] || 'tsx',
code: codeBlockMatch[2]
};
}
return null;
}).filter(Boolean);
const formattedParamExamples = await Promise.all(paramsWithExamples.map(async ({
paramName,
language,
code
}) => ({
paramName,
language,
formattedCode: await prettyFormat(code, null)
})));
const exampleLabel = isProperties ? 'Property' : 'Parameter';
for (const {
paramName,
language,
formattedCode
} of formattedParamExamples) {
nodes.push(md.paragraph([md.strong([md.inlineCode(paramName), md.text(` ${exampleLabel} Example:`)])]));
addCodeBlock(formattedCode, language);
}
// Parameter/Property references (after the examples)
const funcParamsWithRefs = entries.filter(([, paramDef]) => paramDef.seeText);
const refLabel = isProperties ? 'Property' : 'Parameter';
for (const [paramName, paramDef] of funcParamsWithRefs) {
nodes.push(md.paragraph([md.strong([md.inlineCode(paramName), md.text(` ${refLabel} References:`)])]));
nodes.push(...parseMarkdown(paramDef.seeText));
}
}
// Return Value
if (data.returnValue) {
nodes.push(md.paragraph([md.strong('Return Value:')]));
if (data.returnValueDescriptionText) {
nodes.push(...parseMarkdown(data.returnValueDescriptionText));
}
if (typeof data.returnValue === 'string') {
const formattedReturnType = await prettyFormat(data.returnValue, 'ReturnValue');
addCodeBlock(formattedReturnType, 'tsx');
} else {
// Object return value - generate a table like hooks (no Default column)
const returnProps = data.returnValue;
const returnRows = Object.entries(returnProps).map(([propName, prop]) => [md.inlineCode(propName), md.inlineCode(prop.typeText.length > 60 ? `${prop.typeText.slice(0, 60)}...` : prop.typeText), prop.descriptionText ? parseInlineMarkdown(prop.descriptionText) : '-']);
nodes.push(md.table(['Property', 'Type', 'Description'], returnRows, ['left', 'left', 'left']));
}
}
} else if (typeMeta.type === 'class') {
// For 'class' types (ClassTypeMeta)
const part = typeMeta.data.name;
const data = typeMeta.data;
const displayName = getDisplayName(part);
addHeading(3, displayName);
if (data.descriptionText) {
nodes.push(...parseMarkdown(data.descriptionText));
}
// Static Methods (before constructor, as they're often factory methods)
const staticMethods = Object.entries(data.methods || {}).filter(([, methodDef]) => methodDef.isStatic);
if (staticMethods.length > 0) {
nodes.push(md.paragraph([md.strong('Static Methods:')]));
// Format all method signatures in parallel
const formattedStaticMethods = await Promise.all(staticMethods.map(async ([methodName, methodDef]) => {
const paramSignature = methodDef.parameters.map(pDef => {
const optional = pDef.optional ? '?' : '';
return `${pDef.name}${optional}: ${pDef.typeText}`;
}).join(', ');
const signature = `function ${methodName}(${paramSignature}): ${methodDef.returnValue}`;
const formattedSignature = await prettyFormat(signature, null);
return {
methodName,
formattedSignature,
descriptionText: methodDef.descriptionText
};
}));
for (const {
formattedSignature,
descriptionText
} of formattedStaticMethods) {
addCodeBlock(formattedSignature, 'typescript');
if (descriptionText) {
nodes.push(...parseMarkdown(descriptionText));
}
}
}
// Constructor parameters table
if ((data.constructorParameters || []).length > 0) {
nodes.push(md.paragraph([md.strong('Constructor Parameters:')]));
const paramRows = data.constructorParameters.map(paramDef => {
const paramDisplayName = paramDef.optional ? `${paramDef.name}?` : paramDef.name;
const displayType = paramDef.optional ? stripTrailingUndefined(paramDef.typeText) : paramDef.typeText;
return [paramDisplayName, displayType ? md.inlineCode(displayType) : '-', paramDef.defaultText ? md.inlineCode(paramDef.defaultText) : '-', paramDef.descriptionText ? parseInlineMarkdown(paramDef.descriptionText) : '-'];
});
nodes.push(md.table(['Parameter', 'Type', 'Default', 'Description'], paramRows, ['left', 'left', 'left', 'left']));
}
// Properties table
if (Object.keys(data.properties || {}).length > 0) {
nodes.push(md.paragraph([md.strong('Properties:')]));
const propRows = Object.entries(data.properties).map(([propName, propDef]) => {
const propDisplayName = propDef.optional ? `${propName}?` : propName;
const displayType = propDef.optional ? stripTrailingUndefined(propDef.typeText) : propDef.typeText;
const modifiers = [];
if (propDef.isStatic) {
modifiers.push('static');
}
if (propDef.readonly) {
modifiers.push('readonly');
}
const modifiersText = modifiers.join(', ') || '-';
const descriptionCell = propDef.descriptionText ? parseInlineMarkdown(propDef.descriptionText) : '-';
return [propDisplayName, displayType ? md.inlineCode(displayType) : '-', modifiersText, descriptionCell];
});
nodes.push(md.table(['Property', 'Type', 'Modifiers', 'Description'], propRows, ['left', 'left', 'left', 'left']));
}
// Methods (instance methods)
const instanceMethods = Object.entries(data.methods || {}).filter(([, methodDef]) => !methodDef.isStatic);
if (instanceMethods.length > 0) {
nodes.push(md.paragraph([md.strong('Methods:')]));
// Format all method signatures in parallel
const formattedInstanceMethods = await Promise.all(instanceMethods.map(async ([methodName, methodDef]) => {
const paramSignature = methodDef.parameters.map(pDef => {
const optional = pDef.optional ? '?' : '';
return `${pDef.name}${optional}: ${pDef.typeText}`;
}).join(', ');
const signature = `function ${methodName}(${paramSignature}): ${methodDef.returnValue}`;
const formattedSignature = await prettyFormat(signature, null);
return {
methodName,
formattedSignature,
descriptionText: methodDef.descriptionText
};
}));
for (const {
formattedSignature,
descriptionText
} of formattedInstanceMethods) {
addCodeBlock(formattedSignature, 'typescript');
if (descriptionText) {
nodes.push(...parseMarkdown(descriptionText));
}
}
}
} else {
// For 'raw' types (RawTypeMeta)
// The formatting is already done in formatRaw.ts, we just need to output it
const part = typeMeta.name;
const data = typeMeta.data;
const displayName = getDisplayName(part);
addHeading(3, displayName);
if (data.reExportOf) {
nodes.push(md.paragraph([md.text('Re-export of '), md.link(data.reExportOf.slug, data.reExportOf.name), md.text(` ${data.reExportOf.suffix}.`)]));
} else if (data.dataAttributesOf) {
const componentName = data.dataAttributesOf;
const anchorId = componentName.toLowerCase().replace(/\./g, '');
nodes.push(md.paragraph([md.text('Data attributes for '), md.link(`#${anchorId}`, componentName), md.text(' component.')]));
} else if (data.cssVarsOf) {
const componentName = data.cssVarsOf;
const anchorId = componentName.toLowerCase().replace(/\./g, '');
nodes.push(md.paragraph([md.text('CSS variables for '), md.link(`#${anchorId}`, componentName), md.text(' component.')]));
} else if (data.enumMembers && data.enumMembers.length > 0) {
// Render enum as a table
if (data.descriptionText) {
nodes.push(...parseMarkdown(data.descriptionText));
}
const enumRows = data.enumMembers.map(member => [member.name, member.value !== undefined ? md.inlineCode(String(member.value)) : '-', member.descriptionText ? parseInlineMarkdown(member.descriptionText) : '-']);
nodes.push(md.table(['Member', 'Value', 'Description'], enumRows, ['left', 'left', 'left']));
} else {
// Regular raw type - output description and pre-formatted code
if (data.descriptionText) {
nodes.push(...parseMarkdown(data.descriptionText));
}
// The formattedCode is already formatted by prettyFormat in formatRaw.ts
addCodeBlock(data.formattedCode, 'typescript');
}
}
flush();
return chunks;
}
// Process all exports in parallel, each export generating chunks for its main type and additionalTypes
const exportChunksArrays = await Promise.all(Object.values(organizedExports).map(async exportData => {
// Generate chunks for the main type
const mainTypeChunks = await generateSingleTypeMarkdown(exportData.type);
// Generate chunks for additional types (Props, State, etc.)
const additionalTypeChunks = await Promise.all(exportData.additionalTypes.map(additionalType => generateSingleTypeMarkdown(additionalType)));
return [...mainTypeChunks, ...additionalTypeChunks.flat()];
}));
// Process additional types (non-namespaced types like InputType) if any
let additionalTypesChunks = [];
if (additionalTypes.length > 0) {
// Add the "## Additional Types" heading
additionalTypesChunks.push(headingChunk(2, 'Additional Types'));
// Process each additional type using the same helper, but with full names
const additionalTypeChunksArrays = await Promise.all(additionalTypes.map(typeMeta => generateSingleTypeMarkdown(typeMeta, true)));
additionalTypesChunks = additionalTypesChunks.concat(additionalTypeChunksArrays.flat());
}
// Process external types if any
const externalTypesChunks = [];
const externalTypeEntries = Object.entries(externalTypes);
if (externalTypeEntries.length > 0) {
// Add the "## External Types" heading
externalTypesChunks.push(headingChunk(2, 'External Types'));
// Process each external type
for (const [typeName, definition] of externalTypeEntries) {
// Add type heading
externalTypesChunks.push(headingChunk(3, typeName));
// Add type definition as code block (definition is the full declaration from prettyFormat)
externalTypesChunks.push(codeBlockChunk(definition, 'typescript'));
}
}
// Build human-readable metadata sections (Export Groups and Canonical Types)
const metadataChunks = [];
// Determine if we have multiple variants (not just "Default")
const variantNames = Object.keys(organized.variantTypeNames);
const hasMultipleVariants = variantNames.length > 1 || variantNames.length === 1 && variantNames[0] !== 'Default';
if (hasMultipleVariants) {
// Build Export Groups section: variant name -> array of type names
// Format: - `VariantName`: `Type1`, `Type2` (or just `- VariantName` if key equals single value)
const exportGroupsItems = Object.entries(organized.variantTypeNames).map(([variantName, typeNames]) => {
if (typeNames.length === 1 && typeNames[0] === variantName) {
// Key equals single value, omit the value
return md.listItem([md.inlineCode(variantName)]);
}
// Build: `VariantName`: `Type1`, `Type2`
const children = [md.inlineCode(variantName), md.text(': ')];
typeNames.forEach((typeName, i) => {
if (i > 0) {
children.push(md.text(', '));
}
children.push(md.inlineCode(typeName));
});
return md.listItem(children);
});
metadataChunks.push(headingChunk(2, 'Export Groups'));
metadataChunks.push(markdownChunk([md.list(exportGroupsItems)]));
}
// Build Canonical Types section: invert typeNameMap to group keys by canonical name
// Also track which variant each type belongs to via variantTypeNameMapKeys
if (Object.keys(typeNameMap).length > 0) {
// Invert typeNameMap: canonical name -> array of flat names
const canonicalToFlat = {};
for (const [flatName, canonicalName] of Object.entries(typeNameMap)) {
if (!canonicalToFlat[canonicalName]) {
canonicalToFlat[canonicalName] = [];
}
canonicalToFlat[canonicalName].push(flatName);
}
// Build variantTypeNameMapKeys for determining which variants each key belongs to
const keyToVariants = {};
if (hasMultipleVariants) {
for (const [variantName, perVariantMap] of Object.entries(organized.variantTypeNameMaps)) {
for (const key of Object.keys(perVariantMap)) {
if (!keyToVariants[key]) {
keyToVariants[key] = [];
}
keyToVariants[key].push(variantName);
}
}
}
// Build the Canonical Types list items
const canonicalTypesItems = Object.entries(canonicalToFlat).map(([canonicalName, flatNames]) => {
// Determine the variant annotation for this canonical type
// We use the variants of the first flat name (they should all be the same)
const variants = keyToVariants[flatNames[0]] || [];
// Build the list item content
const children = [md.inlineCode(canonicalName)];
// Only show variant annotation if:
// 1. There are multiple variants
// 2. This canonical type is NOT available in ALL export groups
if (variants.length > 0 && hasMultipleVariants) {
const isInAllVariants = variantNames.every(v => variants.includes(v));
if (!isInAllVariants) {
children.push(md.text(' ('));
variants.forEach((variant, i) => {
if (i > 0) {
children.push(md.text(', '));
}
children.push(md.inlineCode(variant));
});
children.push(md.text(')'));
}
}
children.push(md.text(': '));
flatNames.forEach((flatName, i) => {
if (i > 0) {
children.push(md.text(', '));
}
children.push(md.inlineCode(flatName));
});
return md.listItem(children);
});
metadataChunks.push(headingChunk(2, 'Canonical Types'));
metadataChunks.push(markdownChunk([md.paragraph([md.text('Maps '), md.inlineCode('Canonical'), md.text(': '), md.inlineCode('Alias'), md.text(' — Use Canonical when its namespace is already imported; otherwise use Alias.')])]));
metadataChunks.push(markdownChunk([md.list(canonicalTypesItems)]));
}
// Flatten all chunks and format with prettier where needed
const allChunks = [headerChunk, ...exportChunksArrays.flat(), ...additionalTypesChunks, ...externalTypesChunks, ...metadataChunks];
const formattedChunks = await Promise.all(allChunks.map(async chunk => {
if (chunk.needsPrettier) {
return prettyFormatMarkdown(chunk.content);
}
return chunk.content.trimEnd();
}));
// Join all chunks with double newlines to ensure proper spacing in markdown
return `${formattedChunks.join('\n\n')}\n`;
}