Files
react-test/packages/api-docs-builder/ApiBuilders/ComponentApiBuilder.ts
how2ice 005cf56baf
Some checks failed
No response / noResponse (push) Has been cancelled
CI / Continuous releases (push) Has been cancelled
CI / test-dev (macos-latest) (push) Has been cancelled
CI / test-dev (ubuntu-latest) (push) Has been cancelled
CI / test-dev (windows-latest) (push) Has been cancelled
Maintenance / main (push) Has been cancelled
Scorecards supply-chain security / Scorecards analysis (push) Has been cancelled
CodeQL / Analyze (push) Has been cancelled
init project
2025-12-12 14:26:25 +09:00

926 lines
31 KiB
TypeScript

import { readFileSync, writeFileSync } from 'fs';
import path from 'path';
import * as astTypes from 'ast-types';
import * as babel from '@babel/core';
import traverse from '@babel/traverse';
import { kebabCase, escapeRegExp } from 'es-toolkit/string';
import { remark } from 'remark';
import { visit as remarkVisit } from 'unist-util-visit';
import type { Link } from 'mdast';
import { defaultHandlers, parse as docgenParse } from 'react-docgen';
import { parse as parseDoctrine, Annotation } from 'doctrine';
import { renderCodeTags, renderMarkdown } from '../buildApi';
import { ProjectSettings, SortingStrategiesType } from '../ProjectSettings';
import { toGitHubPath, writePrettifiedFile } from '../buildApiUtils';
import muiDefaultPropsHandler from '../utils/defaultPropsHandler';
import parseTest from '../utils/parseTest';
import generatePropTypeDescription, { getChained } from '../utils/generatePropTypeDescription';
import createDescribeableProp, {
CreateDescribeablePropSettings,
DescribeablePropDescriptor,
} from '../utils/createDescribeableProp';
import generatePropDescription from '../utils/generatePropDescription';
import { TypeScriptProject } from '../utils/createTypeScriptProject';
import parseSlotsAndClasses from '../utils/parseSlotsAndClasses';
import generateApiTranslations from '../utils/generateApiTranslation';
import { sortAlphabetical } from '../utils/sortObjects';
import {
AdditionalPropsInfo,
ComponentApiContent,
ComponentReactApi,
ParsedProperty,
TypeDescriptions,
} from '../types/ApiBuilder.types';
import { Slot, ComponentInfo, ApiItemDescription } from '../types/utils.types';
import extractInfoFromEnum from '../utils/extractInfoFromEnum';
const cssComponents = new Set(['Box', 'Grid', 'Typography', 'Stack']);
/**
* Produces markdown of the description that can be hosted anywhere.
*
* By default we assume that the markdown is hosted on mui.com which is
* why the source includes relative url. We transform them to absolute urls with
* this method.
*/
export async function computeApiDescription(
api: { description: ComponentReactApi['description'] },
options: { host: string },
): Promise<string> {
const { host } = options;
const file = await remark()
.use(function docsLinksAttacher() {
return function transformer(tree) {
remarkVisit(tree, 'link', (linkNode) => {
const link = linkNode as Link;
if ((link.url as string).startsWith('/')) {
link.url = `${host}${link.url}`;
}
});
};
})
.process(api.description);
return file.toString().trim();
}
/**
* Add demos & API comment block to type definitions, e.g.:
* /**
* * Demos:
* *
* * - [Icons](https://mui.com/components/icons/)
* * - [Material Icons](https://mui.com/components/material-icons/)
* *
* * API:
* *
* * - [Icon API](https://mui.com/api/icon/)
*/
async function annotateComponentDefinition(
api: ComponentReactApi,
componentJsdoc: Annotation,
projectSettings: ProjectSettings,
) {
const HOST = projectSettings.baseApiUrl ?? 'https://mui.com';
const typesFilename = api.filename.replace(/\.js$/, '.d.ts');
const fileName = path.parse(api.filename).name;
const typesSource = readFileSync(typesFilename, { encoding: 'utf8' });
const typesAST = await babel.parseAsync(typesSource, {
configFile: false,
filename: typesFilename,
presets: [require.resolve('@babel/preset-typescript')],
});
if (typesAST === null) {
throw new Error('No AST returned from babel.');
}
let start = 0;
let end = null;
traverse(typesAST, {
ExportDefaultDeclaration(babelPath) {
/**
* export default function Menu() {}
*/
let node: babel.Node = babelPath.node;
if (node.declaration.type === 'Identifier') {
// declare const Menu: {};
// export default Menu;
if (babel.types.isIdentifier(babelPath.node.declaration)) {
const bindingId = babelPath.node.declaration.name;
const binding = babelPath.scope.bindings[bindingId];
// The JSDoc MUST be located at the declaration
if (babel.types.isFunctionDeclaration(binding.path.node)) {
// For function declarations the binding is equal to the declaration
// /**
// */
// function Component() {}
node = binding.path.node;
} else {
// For variable declarations the binding points to the declarator.
// /**
// */
// const Component = () => {}
node = binding.path.parentPath!.node;
}
}
}
const { leadingComments } = node;
const leadingCommentBlocks =
leadingComments != null
? leadingComments.filter(({ type }) => type === 'CommentBlock')
: null;
const jsdocBlock = leadingCommentBlocks != null ? leadingCommentBlocks[0] : null;
if (leadingCommentBlocks != null && leadingCommentBlocks.length > 1) {
throw new Error(
`Should only have a single leading jsdoc block but got ${
leadingCommentBlocks.length
}:\n${leadingCommentBlocks
.map(({ type, value }, index) => `#${index} (${type}): ${value}`)
.join('\n')}`,
);
}
if (jsdocBlock?.start != null && jsdocBlock?.end != null) {
start = jsdocBlock.start;
end = jsdocBlock.end;
} else if (node.start != null) {
start = node.start - 1;
end = start;
}
},
ExportNamedDeclaration(babelPath) {
let node: babel.Node = babelPath.node;
if (node.declaration == null) {
// export { Menu };
node.specifiers.forEach((specifier) => {
if (specifier.type === 'ExportSpecifier' && specifier.local.name === fileName) {
const binding = babelPath.scope.bindings[specifier.local.name];
if (babel.types.isFunctionDeclaration(binding.path.node)) {
// For function declarations the binding is equal to the declaration
// /**
// */
// function Component() {}
node = binding.path.node;
} else {
// For variable declarations the binding points to the declarator.
// /**
// */
// const Component = () => {}
node = binding.path.parentPath!.node;
}
}
});
} else if (babel.types.isFunctionDeclaration(node.declaration)) {
// export function Menu() {}
if (node.declaration.id?.name === fileName) {
node = node.declaration;
}
} else {
return;
}
const { leadingComments } = node;
const leadingCommentBlocks =
leadingComments != null
? leadingComments.filter(({ type }) => type === 'CommentBlock')
: null;
const jsdocBlock = leadingCommentBlocks != null ? leadingCommentBlocks[0] : null;
if (leadingCommentBlocks != null && leadingCommentBlocks.length > 1) {
throw new Error(
`Should only have a single leading jsdoc block but got ${
leadingCommentBlocks.length
}:\n${leadingCommentBlocks
.map(({ type, value }, index) => `#${index} (${type}): ${value}`)
.join('\n')}`,
);
}
if (jsdocBlock?.start != null && jsdocBlock?.end != null) {
start = jsdocBlock.start;
end = jsdocBlock.end;
} else if (node.start != null) {
start = node.start - 1;
end = start;
}
},
});
if (end === null || start === 0) {
throw new TypeError(
`${api.filename}: Don't know where to insert the jsdoc block. Probably no default export or named export matching the file name was found.`,
);
}
let inheritanceAPILink = null;
if (api.inheritance) {
inheritanceAPILink = `[${api.inheritance.name} API](${
api.inheritance.apiPathname.startsWith('http')
? api.inheritance.apiPathname
: `${HOST}${api.inheritance.apiPathname}`
})`;
}
const markdownLines = (await computeApiDescription(api, { host: HOST })).split('\n');
// Ensure a newline between manual and generated description.
if (markdownLines[markdownLines.length - 1] !== '') {
markdownLines.push('');
}
if (api.customAnnotation) {
markdownLines.push(
...api.customAnnotation
.split('\n')
.map((line) => line.trim())
.filter(Boolean),
);
} else {
markdownLines.push(
'Demos:',
'',
...api.demos.map((demo) => {
return `- [${demo.demoPageTitle}](${
demo.demoPathname.startsWith('http') ? demo.demoPathname : `${HOST}${demo.demoPathname}`
})`;
}),
'',
);
markdownLines.push(
'API:',
'',
`- [${api.name} API](${
api.apiPathname.startsWith('http') ? api.apiPathname : `${HOST}${api.apiPathname}`
})`,
);
if (api.inheritance) {
markdownLines.push(`- inherits ${inheritanceAPILink}`);
}
}
if (componentJsdoc.tags.length > 0) {
markdownLines.push('');
}
componentJsdoc.tags.forEach((tag) => {
markdownLines.push(`@${tag.title}${tag.name ? ` ${tag.name} -` : ''} ${tag.description}`);
});
const jsdoc = `/**\n${markdownLines
.map((line) => (line.length > 0 ? ` * ${line}` : ` *`))
.join('\n')}\n */`;
const typesSourceNew = typesSource.slice(0, start) + jsdoc + typesSource.slice(end);
writeFileSync(typesFilename, typesSourceNew, { encoding: 'utf8' });
}
/**
* Substitute CSS class description conditions with placeholder
*/
function extractClassCondition(description: string) {
const stylesRegex =
/((Styles|State class|Class name) applied to )(.*?)(( if | unless | when |, ){1}(.*))?\./;
const conditions = description.match(stylesRegex);
if (conditions && conditions[6]) {
return {
description: renderMarkdown(
description.replace(stylesRegex, '$1{{nodeName}}$5{{conditions}}.'),
),
nodeName: renderMarkdown(conditions[3]),
conditions: renderMarkdown(renderCodeTags(conditions[6])),
};
}
if (conditions && conditions[3] && conditions[3] !== 'the root element') {
return {
description: renderMarkdown(description.replace(stylesRegex, '$1{{nodeName}}$5.')),
nodeName: renderMarkdown(conditions[3]),
};
}
return { description: renderMarkdown(description) };
}
const generateApiPage = async (
apiPagesDirectory: string,
importTranslationPagesDirectory: string,
reactApi: ComponentReactApi,
sortingStrategies?: SortingStrategiesType,
onlyJsonFile: boolean = false,
layoutConfigPath: string = '',
) => {
const normalizedApiPathname = reactApi.apiPathname.replace(/\\/g, '/');
/**
* Gather the metadata needed for the component's API page.
*/
const pageContent: ComponentApiContent = {
// Sorted by required DESC, name ASC
props: Object.fromEntries(
Object.entries(reactApi.propsTable).sort(([aName, aData], [bName, bData]) => {
if ((aData.required && bData.required) || (!aData.required && !bData.required)) {
return aName.localeCompare(bName);
}
if (aData.required) {
return -1;
}
return 1;
}),
),
name: reactApi.name,
imports: reactApi.imports,
...(reactApi.slots?.length > 0 && { slots: reactApi.slots }),
...(Object.keys(reactApi.cssVariables).length > 0 && { cssVariables: reactApi.cssVariables }),
...(Object.keys(reactApi.dataAttributes).length > 0 && {
dataAttributes: reactApi.dataAttributes,
}),
classes: reactApi.classes,
spread: reactApi.spread,
themeDefaultProps: reactApi.themeDefaultProps,
muiName: normalizedApiPathname.startsWith('/joy-ui')
? reactApi.muiName.replace('Mui', 'Joy')
: reactApi.muiName,
forwardsRefTo: reactApi.forwardsRefTo,
filename: toGitHubPath(reactApi.filename),
inheritance: reactApi.inheritance
? {
component: reactApi.inheritance.name,
pathname: reactApi.inheritance.apiPathname,
}
: null,
demos: `<ul>${reactApi.demos
.map((item) => `<li><a href="${item.demoPathname}">${item.demoPageTitle}</a></li>`)
.join('\n')}</ul>`,
cssComponent: cssComponents.has(reactApi.name),
deprecated: reactApi.deprecated,
};
const { classesSort = sortAlphabetical('key'), slotsSort = null } = {
...sortingStrategies,
};
if (classesSort) {
pageContent.classes = [...pageContent.classes].sort(classesSort);
}
if (slotsSort && pageContent.slots) {
pageContent.slots = [...pageContent.slots].sort(slotsSort);
}
await writePrettifiedFile(
path.resolve(apiPagesDirectory, `${kebabCase(reactApi.name)}.json`),
JSON.stringify(pageContent),
);
if (!onlyJsonFile) {
await writePrettifiedFile(
path.resolve(apiPagesDirectory, `${kebabCase(reactApi.name)}.js`),
`import * as React from 'react';
import ApiPage from 'docs/src/modules/components/ApiPage';
import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations';${
layoutConfigPath === ''
? ''
: `
import layoutConfig from '${layoutConfigPath}';`
}
import jsonPageContent from './${kebabCase(reactApi.name)}.json';
export default function Page(props) {
const { descriptions } = props;
return <ApiPage ${layoutConfigPath === '' ? '' : '{...layoutConfig} '}descriptions={descriptions} pageContent={jsonPageContent} />;
}
export async function getStaticProps() {
const req = require.context(
'${importTranslationPagesDirectory}/${kebabCase(reactApi.name)}',
false,
/\\.\\/${kebabCase(reactApi.name)}.*\\.json$/,
);
const descriptions = mapApiPageTranslations(req);
return { props: { descriptions } };
}
`.replace(/\r?\n/g, reactApi.EOL),
);
}
};
const attachTranslations = (
reactApi: ComponentReactApi,
deprecationInfo: string | undefined,
settings?: CreateDescribeablePropSettings,
) => {
const translations: ComponentReactApi['translations'] = {
componentDescription: reactApi.description,
deprecationInfo: deprecationInfo ? renderMarkdown(deprecationInfo) : undefined,
propDescriptions: {},
classDescriptions: {},
};
Object.entries(reactApi.props!).forEach(([propName, propDescriptor]) => {
let prop: DescribeablePropDescriptor | null;
try {
prop = createDescribeableProp(propDescriptor, propName, settings);
} catch (error) {
prop = null;
}
if (prop) {
const { deprecated, seeMore, jsDocText, signatureArgs, signatureReturn, requiresRef } =
generatePropDescription(prop, propName);
// description = renderMarkdownInline(`${description}`);
const typeDescriptions: TypeDescriptions = {};
(signatureArgs || [])
.concat(signatureReturn || [])
.forEach(({ name, description, argType, argTypeDescription }) => {
typeDescriptions[name] = {
name,
description: renderMarkdown(description),
argType,
argTypeDescription: argTypeDescription ? renderMarkdown(argTypeDescription) : undefined,
};
});
translations.propDescriptions[propName] = {
description: renderMarkdown(jsDocText),
requiresRef: requiresRef || undefined,
deprecated: renderMarkdown(deprecated) || undefined,
typeDescriptions: Object.keys(typeDescriptions).length > 0 ? typeDescriptions : undefined,
seeMoreText: seeMore?.description,
};
}
});
/**
* Slot descriptions.
*/
if (reactApi.slots?.length > 0) {
translations.slotDescriptions = {};
[...reactApi.slots]
.sort(sortAlphabetical('name')) // Sort to ensure consistency of object key order
.forEach((slot: Slot) => {
const { name, description } = slot;
translations.slotDescriptions![name] = renderMarkdown(description);
});
}
/**
* CSS class descriptions and deprecations.
*/
[...reactApi.classes]
.sort(sortAlphabetical('key')) // Sort to ensure consistency of object key order
.forEach((classDefinition) => {
translations.classDescriptions[classDefinition.key] = {
...extractClassCondition(classDefinition.description),
deprecationInfo: classDefinition.deprecationInfo,
};
});
reactApi.classes.forEach((classDefinition, index) => {
delete reactApi.classes[index].deprecationInfo; // store deprecation info in translations only
});
/**
* CSS variables descriptions.
*/
if (Object.keys(reactApi.cssVariables).length > 0) {
translations.cssVariablesDescriptions = {};
[...Object.keys(reactApi.cssVariables)]
.sort() // Sort to ensure consistency of object key order
.forEach((cssVariableName: string) => {
const cssVariable = reactApi.cssVariables[cssVariableName];
const { description } = cssVariable;
translations.cssVariablesDescriptions![cssVariableName] = renderMarkdown(description);
});
}
/**
* Data attributes descriptions.
*/
if (Object.keys(reactApi.dataAttributes).length > 0) {
translations.dataAttributesDescriptions = {};
[...Object.keys(reactApi.dataAttributes)]
.sort() // Sort to ensure consistency of object key order
.forEach((dataAttributeName: string) => {
const dataAttribute = reactApi.dataAttributes[dataAttributeName];
const { description } = dataAttribute;
translations.dataAttributesDescriptions![dataAttributeName] = renderMarkdown(description);
});
}
reactApi.translations = translations;
};
const attachPropsTable = (
reactApi: ComponentReactApi,
settings?: CreateDescribeablePropSettings,
) => {
const propErrors: Array<[propName: string, error: Error]> = [];
type Pair = [string, ComponentReactApi['propsTable'][string]];
const componentProps: ComponentReactApi['propsTable'] = Object.fromEntries(
Object.entries(reactApi.props!).map(([propName, propDescriptor]): Pair => {
let prop: DescribeablePropDescriptor | null;
try {
prop = createDescribeableProp(propDescriptor, propName, settings);
} catch (error) {
propErrors.push([`[${reactApi.name}] \`${propName}\``, error as Error]);
prop = null;
}
if (prop === null) {
// have to delete `componentProps.undefined` later
return [] as any;
}
const defaultValue = propDescriptor.jsdocDefaultValue?.value;
const {
signature: signatureType,
signatureArgs,
signatureReturn,
seeMore,
} = generatePropDescription(prop, propName);
const propTypeDescription = generatePropTypeDescription(propDescriptor.type);
const chainedPropType = getChained(prop.type);
const requiredProp =
prop.required ||
prop.type.raw?.includes('.isRequired') ||
(chainedPropType !== false && chainedPropType.required);
const deprecation = (propDescriptor.description || '').match(/@deprecated(\s+(?<info>.*))?/);
const additionalPropsInfo: AdditionalPropsInfo = {};
const normalizedApiPathname = reactApi.apiPathname.replace(/\\/g, '/');
if (propName === 'classes') {
additionalPropsInfo.cssApi = true;
} else if (propName === 'sx') {
additionalPropsInfo.sx = true;
} else if (propName === 'slots' && !normalizedApiPathname.startsWith('/material-ui')) {
additionalPropsInfo.slotsApi = true;
} else if (normalizedApiPathname.startsWith('/joy-ui')) {
switch (propName) {
case 'size':
additionalPropsInfo['joy-size'] = true;
break;
case 'color':
additionalPropsInfo['joy-color'] = true;
break;
case 'variant':
additionalPropsInfo['joy-variant'] = true;
break;
default:
}
}
let signature: ComponentReactApi['propsTable'][string]['signature'];
if (signatureType !== undefined) {
signature = {
type: signatureType,
describedArgs: signatureArgs?.map((arg) => arg.name),
returned: signatureReturn?.name,
};
}
return [
propName,
{
type: {
name: propDescriptor.type.name,
description:
propTypeDescription !== propDescriptor.type.name ? propTypeDescription : undefined,
},
default: defaultValue,
// undefined values are not serialized => saving some bytes
required: requiredProp || undefined,
deprecated: !!deprecation || undefined,
deprecationInfo: renderMarkdown(deprecation?.groups?.info || '').trim() || undefined,
signature,
additionalInfo:
Object.keys(additionalPropsInfo).length === 0 ? undefined : additionalPropsInfo,
seeMoreLink: seeMore?.link,
},
];
}),
);
if (propErrors.length > 0) {
throw new Error(
`There were errors creating prop descriptions:\n${propErrors
.map(([propName, error]) => {
return ` - ${propName}: ${error}`;
})
.join('\n')}`,
);
}
// created by returning the `[]` entry
delete componentProps.undefined;
reactApi.propsTable = componentProps;
};
/**
* Helper to get the import options
* @param name The name of the component
* @param filename The filename where its defined (to infer the package)
* @returns an array of import command
*/
const defaultGetComponentImports = (name: string, filename: string) => {
const githubPath = toGitHubPath(filename);
const rootImportPath = githubPath.replace(
/\/packages\/mui(?:-(.+?))?\/src\/.*/,
(match, pkg) => `@mui/${pkg}`,
);
const subdirectoryImportPath = githubPath.replace(
/\/packages\/mui(?:-(.+?))?\/src\/([^\\/]+)\/.*/,
(match, pkg, directory) => `@mui/${pkg}/${directory}`,
);
let namedImportName = name;
const defaultImportName = name;
if (githubPath.includes('Unstable_')) {
namedImportName = `Unstable_${name} as ${name}`;
}
const useNamedImports = rootImportPath === '@mui/base';
const subpathImport = useNamedImports
? `import { ${namedImportName} } from '${subdirectoryImportPath}';`
: `import ${defaultImportName} from '${subdirectoryImportPath}';`;
const rootImport = `import { ${namedImportName} } from '${rootImportPath}';`;
return [subpathImport, rootImport];
};
const attachTable = (
reactApi: ComponentReactApi,
params: ParsedProperty[],
attribute: 'cssVariables' | 'dataAttributes',
defaultType?: string,
) => {
const errors: Array<[propName: string, error: Error]> = [];
const data: { [key: string]: ApiItemDescription } = params
.map((p) => {
const { name: propName, ...propDescriptor } = p;
let prop: Omit<ParsedProperty, 'name'> | null;
try {
prop = propDescriptor;
} catch (error) {
errors.push([propName, error as Error]);
prop = null;
}
if (prop === null) {
// have to delete `componentProps.undefined` later
return [] as any;
}
const deprecationTag = propDescriptor.tags?.deprecated;
const deprecation = deprecationTag?.text?.[0]?.text;
const typeTag = propDescriptor.tags?.type;
let type = typeTag?.text?.[0]?.text ?? defaultType;
if (typeof type === 'string') {
type = type.replace(/{|}/g, '');
}
return {
name: propName,
description: propDescriptor.description,
type,
deprecated: !!deprecation || undefined,
deprecationInfo: renderMarkdown(deprecation || '').trim() || undefined,
};
})
.reduce((acc, cssVarDefinition) => {
const { name, ...other } = cssVarDefinition;
return {
...acc,
[name]: other,
};
}, {});
if (errors.length > 0) {
throw new Error(
`There were errors creating ${attribute.replace(/([A-Z])/g, ' $1')} descriptions:\n${errors
.map(([item, error]) => {
return ` - ${item}: ${error}`;
})
.join('\n')}`,
);
}
reactApi[attribute] = data;
};
/**
* - Build react component (specified filename) api by lookup at its definition (.d.ts or ts)
* and then generate the API page + json data
* - Generate the translations
* - Add the comment in the component filename with its demo & API urls (including the inherited component).
* this process is done by sourcing markdown files and filter matched `components` in the frontmatter
*/
export default async function generateComponentApi(
componentInfo: ComponentInfo,
project: TypeScriptProject,
projectSettings: ProjectSettings,
) {
const { shouldSkip, spread, EOL, src } = componentInfo.readFile();
if (shouldSkip) {
return null;
}
const filename = componentInfo.filename;
let reactApi: ComponentReactApi;
try {
reactApi = docgenParse(src, null, defaultHandlers.concat(muiDefaultPropsHandler), {
filename,
});
} catch (error) {
// fallback to default logic if there is no `create*` definition.
if ((error as Error).message === 'No suitable component definition found.') {
reactApi = docgenParse(
src,
(ast) => {
let node;
// TODO migrate to react-docgen v6, using Babel AST now
astTypes.visit(ast, {
visitFunctionDeclaration: (functionPath) => {
// @ts-ignore
if (functionPath.node.params[0].name === 'props') {
node = functionPath;
}
return false;
},
visitVariableDeclaration: (variablePath) => {
const definitions: any[] = [];
if (variablePath.node.declarations) {
variablePath
.get('declarations')
.each((declarator: any) => definitions.push(declarator.get('init')));
}
definitions.forEach((definition) => {
// definition.value.expression is defined when the source is in TypeScript.
const expression = definition.value?.expression
? definition.get('expression')
: definition;
if (expression.value?.callee) {
const definitionName = expression.value.callee.name;
if (definitionName === `create${componentInfo.name}`) {
node = expression;
}
}
});
return false;
},
});
return node;
},
defaultHandlers.concat(muiDefaultPropsHandler),
{
filename,
},
);
} else {
throw error;
}
}
if (!reactApi.props) {
reactApi.props = {};
}
const { getComponentImports = defaultGetComponentImports } = projectSettings;
const componentJsdoc = parseDoctrine(reactApi.description);
// We override `reactApi.description` with `componentJsdoc.description` because
// the former can include JSDoc tags that we don't want to render in the docs.
reactApi.description = componentJsdoc.description;
// Ignore what we might have generated in `annotateComponentDefinition`
let annotationBoundary: RegExp = /(Demos|API):\r?\n\r?\n/;
if (componentInfo.customAnnotation) {
annotationBoundary = new RegExp(
escapeRegExp(componentInfo.customAnnotation.trim().split('\n')[0].trim()),
);
}
const annotatedDescriptionMatch = reactApi.description.match(new RegExp(annotationBoundary));
if (annotatedDescriptionMatch !== null) {
reactApi.description = reactApi.description.slice(0, annotatedDescriptionMatch.index).trim();
}
reactApi.filename = filename;
reactApi.name = componentInfo.name;
reactApi.imports = getComponentImports(componentInfo.name, filename);
reactApi.muiName = componentInfo.muiName;
reactApi.apiPathname = componentInfo.apiPathname;
reactApi.EOL = EOL;
reactApi.slots = [];
reactApi.classes = [];
reactApi.demos = componentInfo.getDemos();
reactApi.customAnnotation = componentInfo.customAnnotation;
reactApi.inheritance = null;
if (reactApi.demos.length === 0) {
throw new Error(
'Unable to find demos. \n' +
`Be sure to include \`components: ${reactApi.name}\` in the markdown pages where the \`${reactApi.name}\` component is relevant. ` +
'Every public component should have a demo.\nFor internal component, add the name of the component to the `skipComponent` method of the product.',
);
}
try {
const testInfo = await parseTest(reactApi.filename);
// no Object.assign to visually check for collisions
reactApi.forwardsRefTo = testInfo.forwardsRefTo;
reactApi.spread = testInfo.spread ?? spread;
reactApi.themeDefaultProps = testInfo.themeDefaultProps;
reactApi.inheritance = componentInfo.getInheritance(testInfo.inheritComponent);
} catch (error: any) {
console.error(error.message);
if (project.name.includes('grid')) {
// TODO: Use `describeConformance` for the DataGrid components
reactApi.forwardsRefTo = 'GridRoot';
}
}
if (!projectSettings.skipSlotsAndClasses) {
const { slots, classes } = parseSlotsAndClasses({
typescriptProject: project,
projectSettings,
componentName: reactApi.name,
muiName: reactApi.muiName,
slotInterfaceName: componentInfo.slotInterfaceName,
});
reactApi.slots = slots;
reactApi.classes = classes;
}
const deprecation = componentJsdoc.tags.find((tag) => tag.title === 'deprecated');
const deprecationInfo = deprecation?.description || undefined;
reactApi.deprecated = !!deprecation || undefined;
const cssVars = await extractInfoFromEnum(
`${componentInfo.name}CssVars`,
new RegExp(`${componentInfo.name}(CssVars|Classes)?.tsx?$`, 'i'),
project,
);
const dataAttributes = await extractInfoFromEnum(
`${componentInfo.name}DataAttributes`,
new RegExp(`${componentInfo.name}(DataAttributes)?.tsx?$`, 'i'),
project,
);
attachPropsTable(reactApi, projectSettings.propsSettings);
attachTable(reactApi, cssVars, 'cssVariables', 'string');
attachTable(reactApi, dataAttributes, 'dataAttributes');
attachTranslations(reactApi, deprecationInfo, projectSettings.propsSettings);
// eslint-disable-next-line no-console
console.log('Built API docs for', reactApi.apiPathname);
if (!componentInfo.skipApiGeneration) {
const {
skipAnnotatingComponentDefinition,
translationPagesDirectory,
importTranslationPagesDirectory,
generateJsonFileOnly,
} = projectSettings;
await generateApiTranslations(
path.join(process.cwd(), translationPagesDirectory),
reactApi,
projectSettings.translationLanguages,
);
// Once we have the tabs API in all projects, we can make this default
await generateApiPage(
componentInfo.apiPagesDirectory,
importTranslationPagesDirectory ?? translationPagesDirectory,
reactApi,
projectSettings.sortingStrategies,
generateJsonFileOnly,
componentInfo.layoutConfigPath,
);
if (
typeof skipAnnotatingComponentDefinition === 'function'
? !skipAnnotatingComponentDefinition(reactApi.filename)
: !skipAnnotatingComponentDefinition
) {
// Add comment about demo & api links (including inherited component) to the component file
await annotateComponentDefinition(reactApi, componentJsdoc, projectSettings);
}
}
return reactApi;
}