UNPKG

@mui/internal-docs-infra

Version:

MUI Infra - internal documentation creation tools.

306 lines (288 loc) 11.8 kB
// 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 }; }