@mui/internal-docs-infra
Version:
MUI Infra - internal documentation creation tools.
306 lines (288 loc) • 11.8 kB
JavaScript
// Can use node: imports here since this is server-only code
import path from 'node:path';
import { writeFile, readFile } from 'node:fs/promises';
import { extractNameAndSlugFromUrl } from "../loaderUtils/index.mjs";
import { nameMark, performanceMeasure } from "../loadPrecomputedCodeHighlighter/performanceLogger.mjs";
import { loadServerTypesMeta } from "../loadServerTypesMeta/index.mjs";
import { namespaceParts as defaultNamespacePartsOrder } from "../loadServerTypesText/order.mjs";
import { generateTypesMarkdown } from "./generateTypesMarkdown.mjs";
import { syncPageIndex } from "../syncPageIndex/index.mjs";
const functionName = 'Sync Types';
/**
* Helper to process a single TypesMeta and add it to parts, exports, or types.
*/
function processTypeMeta(typeMeta, parts, pageExports, pageTypes) {
if (typeMeta.type === 'component') {
const componentName = typeMeta.name;
const componentData = typeMeta.data;
const metadata = {
props: Object.keys(componentData.props || {}).sort(),
dataAttributes: Object.keys(componentData.dataAttributes || {}).sort(),
cssVariables: Object.keys(componentData.cssVariables || {}).sort()
};
// Check if this is a namespaced component (e.g., "Accordion.Root")
if (componentName.includes('.')) {
// Extract the part name (everything after the last dot)
const partName = componentName.split('.').pop() || componentName;
parts[partName] = metadata;
} else {
// Non-namespaced component goes into exports
pageExports[componentName] = metadata;
}
} else if (typeMeta.type === 'hook' || typeMeta.type === 'function') {
const name = typeMeta.name;
const data = typeMeta.data;
const hasExpandedProperties = 'expandedProperties' in data && data.expandedProperties;
const paramsOrProps = hasExpandedProperties ? data.expandedProperties : data.parameters ?? [];
const paramKeys = hasExpandedProperties ? Object.keys(paramsOrProps).sort() : paramsOrProps.map(p => p.name);
// When the function takes a single anonymous object parameter (expanded into properties),
// wrap in a nested array so the serializer renders { }.
const parameters = hasExpandedProperties && paramKeys.length > 0 ? [paramKeys] : paramKeys;
// Namespaced functions (e.g., "Popover.createHandle") become parts,
// stripping the namespace prefix since it's redundant.
if (name.includes('.')) {
const partName = name.split('.').pop() || name;
parts[partName] = {
parameters
};
} else {
pageExports[name] = {
parameters
};
}
} else if (typeMeta.type === 'class') {
const name = typeMeta.name;
const data = typeMeta.data;
const paramKeys = (data.constructorParameters || []).map(p => p.name);
// Namespaced classes (e.g., "Popover.Handle") become parts,
// stripping the namespace prefix since it's redundant.
if (name.includes('.')) {
const partName = name.split('.').pop() || name;
parts[partName] = {
parameters: paramKeys
};
} else {
pageExports[name] = {
parameters: paramKeys
};
}
} else if (typeMeta.type === 'raw') {
// Raw types (state objects, enums, type aliases) are tracked separately
pageTypes.push(typeMeta.name);
}
}
/**
* Builds page metadata from the loaded types for the parent index.
* Extracts props, dataAttributes, and cssVariables from component types.
*
* Component names with dots (e.g., "Accordion.Root") are converted to the parts format,
* where the part after the dot becomes the part name (e.g., { parts: { Root: {...} } }).
* This matches the serialized format "Accordion - Root" in the parent index.
*/
function buildPageMetadataFromTypes(typesMarkdownPath, organized, ordering) {
// Extract slug and title from the types file path
// The types file is typically at /path/to/component/types.ts or types.md
// We want the parent directory name as the slug
const parentDir = path.dirname(typesMarkdownPath);
const {
name: title,
slug
} = extractNameAndSlugFromUrl(parentDir);
// Build parts metadata for component types with dots in names (e.g., Accordion.Root)
// Build exports metadata for other types (hooks, functions, components without dots)
const parts = {};
const pageExports = {};
const pageTypes = [];
// Process main export types
for (const {
type: typeMeta
} of Object.values(organized.exports)) {
processTypeMeta(typeMeta, parts, pageExports, pageTypes);
}
// Process additional types (additionalTypes within exports and top-level)
for (const {
additionalTypes
} of Object.values(organized.exports)) {
for (const typeMeta of additionalTypes) {
processTypeMeta(typeMeta, parts, pageExports, pageTypes);
}
}
for (const typeMeta of organized.additionalTypes) {
processTypeMeta(typeMeta, parts, pageExports, pageTypes);
}
// If no types were found, return null
if (Object.keys(parts).length === 0 && Object.keys(pageExports).length === 0 && pageTypes.length === 0) {
return null;
}
// Sort parts using the namespaceParts order
const namespacePartsOrder = ordering?.namespaceParts ?? defaultNamespacePartsOrder;
const sortedParts = {};
const partKeys = Object.keys(parts);
partKeys.sort((a, b) => {
const aIndex = namespacePartsOrder.indexOf(a);
const bIndex = namespacePartsOrder.indexOf(b);
const everythingElseIndex = namespacePartsOrder.indexOf('__EVERYTHING_ELSE__');
// If both are in the order list, sort by their position
if (aIndex !== -1 && bIndex !== -1) {
return aIndex - bIndex;
}
// If only a is in the list, it comes first (unless after __EVERYTHING_ELSE__)
if (aIndex !== -1) {
return aIndex < everythingElseIndex ? -1 : 1;
}
// If only b is in the list, it comes first (unless after __EVERYTHING_ELSE__)
if (bIndex !== -1) {
return bIndex < everythingElseIndex ? 1 : -1;
}
// Neither is in the list, sort alphabetically
return a.localeCompare(b);
});
for (const key of partKeys) {
sortedParts[key] = parts[key];
}
return {
title,
slug,
path: `./${slug}/page.mdx`,
parts: Object.keys(sortedParts).length > 0 ? sortedParts : undefined,
exports: Object.keys(pageExports).length > 0 ? pageExports : undefined,
types: pageTypes.length > 0 ? pageTypes.sort() : undefined
};
}
/**
* Syncs types for a component/hook/function.
* - Loads and formats types via loadServerTypesMeta
* - Generates markdown documentation
* - Writes markdown to disk
* - Updates parent index page (if configured)
*
* This is separated from the webpack loader to allow reuse in other contexts.
*/
export async function syncTypes(options) {
const {
typesMarkdownPath,
rootContext,
updateParentIndex
} = options;
// Derive relative path for logging
const relativePath = path.relative(rootContext, typesMarkdownPath);
let currentMark = nameMark(functionName, 'Start Loading', [relativePath]);
performance.mark(currentMark);
// Load and format types using loadServerTypesMeta
const typesMetaResult = await loadServerTypesMeta({
typesMarkdownPath: options.typesMarkdownPath,
rootContext: options.rootContext,
variants: options.variants,
watchSourceDirectly: options.watchSourceDirectly,
formattingOptions: options.formattingOptions,
socketDir: options.socketDir,
externalTypesPattern: options.externalTypesPattern,
ordering: options.ordering,
descriptionReplacements: options.descriptionReplacements
});
const {
allDependencies,
typeNameMap,
externalTypes,
resourceName,
exports: organizedExports,
additionalTypes: organizedAdditionalTypes,
variantOnlyAdditionalTypes: organizedVariantOnlyAdditionalTypes,
variantTypeNames,
variantTypeNameMaps
} = typesMetaResult;
currentMark = performanceMeasure(currentMark, {
mark: 'types meta loaded',
measure: 'types meta loading'
}, [functionName, relativePath]);
// Generate and write markdown
const markdownStart = performance.now();
const markdown = await generateTypesMarkdown({
name: resourceName,
organized: {
exports: organizedExports,
additionalTypes: organizedAdditionalTypes,
variantOnlyAdditionalTypes: organizedVariantOnlyAdditionalTypes,
variantTypeNames,
variantTypeNameMaps
},
typeNameMap,
externalTypes,
path: relativePath
});
const markdownEnd = performance.now();
const markdownCompleteMark = nameMark(functionName, 'markdown generated', [relativePath]);
performance.mark(markdownCompleteMark);
performance.measure(nameMark(functionName, 'markdown generation', [relativePath]), {
start: markdownStart,
end: markdownEnd
});
// Check if markdown has changed before writing
const writeStart = performance.now();
let updated = false;
const existingMarkdown = await readFile(typesMarkdownPath, 'utf-8').catch(() => null);
if (existingMarkdown !== markdown) {
await writeFile(typesMarkdownPath, markdown, 'utf-8');
updated = true;
}
// Track allDependencies locally so we can add typesMarkdownPath in production
const dependencies = [...allDependencies];
if (process.env.NODE_ENV === 'production') {
// during development, if this markdown file is included as a dependency,
// it causes a second rebuild when this file is written
// during production builds, we should already have the file in place
// so this is not an issue and we should ensure changing this file triggers a rebuild
dependencies.push(typesMarkdownPath);
}
const writeEnd = performance.now();
const writeCompleteMark = nameMark(functionName, 'markdown written', [relativePath]);
performance.mark(writeCompleteMark);
performance.measure(nameMark(functionName, 'markdown write', [relativePath]), {
start: writeStart,
end: writeEnd
});
currentMark = performanceMeasure(currentMark, {
mark: 'markdown generated',
measure: 'markdown generation'
}, [functionName, relativePath], true);
// Update the parent index page with component metadata if configured
if (updateParentIndex) {
const pageMetadata = buildPageMetadataFromTypes(typesMarkdownPath, {
exports: organizedExports,
additionalTypes: organizedAdditionalTypes
}, options.ordering);
if (pageMetadata) {
// Derive the component's page.mdx path from the types.md file
// types.md is at /path/to/components/checkbox/types.md
// page.mdx is at /path/to/components/checkbox/page.mdx
// syncPageIndex will update the parent index at /path/to/components/page.mdx
const pagePath = path.join(path.dirname(typesMarkdownPath), 'page.mdx');
await syncPageIndex({
pagePath,
metadata: pageMetadata,
baseDir: updateParentIndex.baseDir,
indexFileName: updateParentIndex.indexFileName,
markerDir: updateParentIndex.markerDir,
onlyUpdateIndexes: updateParentIndex.onlyUpdateIndexes ?? false,
errorIfOutOfDate: updateParentIndex.errorIfOutOfDate,
// Auto-generated title/slug from types should not override user-set values
preserveExistingTitleAndSlug: true
});
performanceMeasure(currentMark, {
mark: 'parent index updated',
measure: 'parent index update'
}, [functionName, relativePath]);
}
}
return {
exports: organizedExports,
additionalTypes: organizedAdditionalTypes,
variantOnlyAdditionalTypes: organizedVariantOnlyAdditionalTypes,
externalTypes,
allDependencies: dependencies,
typeNameMap: typeNameMap ?? {},
variantTypeNames,
variantTypeNameMaps,
updated
};
}