From 817470c5a7c2ae721401e308b961005730cdf2ab Mon Sep 17 00:00:00 2001 From: Vlad Moroz Date: Tue, 10 Dec 2024 13:17:00 +0100 Subject: [PATCH] [api-docs-builder] Allow custom annotations (#44707) --- .../ApiBuilders/ComponentApiBuilder.ts | 58 +++++++++++------ .../ApiBuilders/HookApiBuilder.ts | 64 +++++++++++++------ .../types/ApiBuilder.types.ts | 1 + .../api-docs-builder/types/utils.types.ts | 8 +++ 4 files changed, 91 insertions(+), 40 deletions(-) diff --git a/packages/api-docs-builder/ApiBuilders/ComponentApiBuilder.ts b/packages/api-docs-builder/ApiBuilders/ComponentApiBuilder.ts index ba1a6dddea7e2d..726674e48b01e4 100644 --- a/packages/api-docs-builder/ApiBuilders/ComponentApiBuilder.ts +++ b/packages/api-docs-builder/ApiBuilders/ComponentApiBuilder.ts @@ -10,6 +10,7 @@ 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 escapeRegExp from 'lodash/escapeRegExp'; import { renderCodeTags, renderMarkdown } from '../buildApi'; import { ProjectSettings, SortingStrategiesType } from '../ProjectSettings'; import { toGitHubPath, writePrettifiedFile } from '../buildApiUtils'; @@ -229,26 +230,36 @@ async function annotateComponentDefinition( if (markdownLines[markdownLines.length - 1] !== '') { markdownLines.push(''); } - 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 (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) { @@ -764,7 +775,13 @@ export default async function generateComponentApi( reactApi.description = componentJsdoc.description; // Ignore what we might have generated in `annotateComponentDefinition` - const annotatedDescriptionMatch = reactApi.description.match(/(Demos|API):\r?\n\r?\n/); + 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(); } @@ -778,6 +795,7 @@ export default async function generateComponentApi( reactApi.slots = []; reactApi.classes = []; reactApi.demos = componentInfo.getDemos(); + reactApi.customAnnotation = componentInfo.customAnnotation; reactApi.inheritance = null; if (reactApi.demos.length === 0) { throw new Error( diff --git a/packages/api-docs-builder/ApiBuilders/HookApiBuilder.ts b/packages/api-docs-builder/ApiBuilders/HookApiBuilder.ts index e57edd26502925..194dcb14207ca6 100644 --- a/packages/api-docs-builder/ApiBuilders/HookApiBuilder.ts +++ b/packages/api-docs-builder/ApiBuilders/HookApiBuilder.ts @@ -8,6 +8,7 @@ import { defaultHandlers, parse as docgenParse } from 'react-docgen'; import kebabCase from 'lodash/kebabCase'; import upperFirst from 'lodash/upperFirst'; import { parse as parseDoctrine, Annotation } from 'doctrine'; +import escapeRegExp from 'lodash/escapeRegExp'; import { escapeEntities, renderMarkdown } from '../buildApi'; import { ProjectSettings } from '../ProjectSettings'; import { computeApiDescription } from './ComponentApiBuilder'; @@ -184,32 +185,42 @@ async function annotateHookDefinition( } 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.demos && api.demos.length > 0) { + if (api.customAnnotation) { markdownLines.push( - 'Demos:', - '', - ...api.demos.map((item) => { - return `- [${item.demoPageTitle}](${ - item.demoPathname.startsWith('http') ? item.demoPathname : `${HOST}${item.demoPathname}` - })`; - }), + ...api.customAnnotation + .split('\n') + .map((line) => line.trim()) + .filter(Boolean), + ); + } else { + if (api.demos && api.demos.length > 0) { + markdownLines.push( + 'Demos:', + '', + ...api.demos.map((item) => { + return `- [${item.demoPageTitle}](${ + item.demoPathname.startsWith('http') ? item.demoPathname : `${HOST}${item.demoPathname}` + })`; + }), + '', + ); + } + + markdownLines.push( + 'API:', '', + `- [${api.name} API](${ + api.apiPathname.startsWith('http') ? api.apiPathname : `${HOST}${api.apiPathname}` + })`, ); } - markdownLines.push( - 'API:', - '', - `- [${api.name} API](${ - api.apiPathname.startsWith('http') ? api.apiPathname : `${HOST}${api.apiPathname}` - })`, - ); - if (hookJsdoc.tags.length > 0) { markdownLines.push(''); } @@ -410,8 +421,16 @@ export default async function generateHookApi( project: TypeScriptProject, projectSettings: ProjectSettings, ) { - const { filename, name, apiPathname, apiPagesDirectory, getDemos, readFile, skipApiGeneration } = - hooksInfo; + const { + filename, + name, + apiPathname, + apiPagesDirectory, + getDemos, + readFile, + skipApiGeneration, + customAnnotation, + } = hooksInfo; const { shouldSkip, EOL, src } = readFile(); @@ -445,8 +464,12 @@ export default async function generateHookApi( // the former can include JSDoc tags that we don't want to render in the docs. reactApi.description = hookJsdoc.description; - // Ignore what we might have generated in `annotateHookDefinition` - const annotatedDescriptionMatch = reactApi.description.match(/(Demos|API):\r?\n\r?\n/); + // Ignore what we might have generated in `annotateComponentDefinition` + let annotationBoundary: RegExp = /(Demos|API):\r?\n\r?\n/; + if (customAnnotation) { + annotationBoundary = new RegExp(escapeRegExp(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(); } @@ -458,6 +481,7 @@ export default async function generateHookApi( reactApi.apiPathname = apiPathname; reactApi.EOL = EOL; reactApi.demos = getDemos(); + reactApi.customAnnotation = customAnnotation; if (reactApi.demos.length === 0) { // TODO: Enable this error once all public hooks are documented // throw new Error( diff --git a/packages/api-docs-builder/types/ApiBuilder.types.ts b/packages/api-docs-builder/types/ApiBuilder.types.ts index 7b7f692c40e851..a78e4d44fb5ba8 100644 --- a/packages/api-docs-builder/types/ApiBuilder.types.ts +++ b/packages/api-docs-builder/types/ApiBuilder.types.ts @@ -33,6 +33,7 @@ interface CommonReactApi extends ReactDocgenApi { */ apiDocsTranslationFolder?: string; deprecated: true | undefined; + customAnnotation?: string; } export interface PropsTableItem { diff --git a/packages/api-docs-builder/types/utils.types.ts b/packages/api-docs-builder/types/utils.types.ts index f0d1b30d39a97e..62a1177cc00d80 100644 --- a/packages/api-docs-builder/types/utils.types.ts +++ b/packages/api-docs-builder/types/utils.types.ts @@ -58,6 +58,10 @@ export type ComponentInfo = { * If `true`, the component's name match one of the MUI System components. */ isSystemComponent?: boolean; + /** + * If provided, this annotation will be used instead of the auto-generated demo & API links + */ + customAnnotation?: string; }; export type HookInfo = { @@ -74,4 +78,8 @@ export type HookInfo = { getDemos: ComponentInfo['getDemos']; apiPagesDirectory: string; skipApiGeneration?: boolean; + /** + * If provided, this annotation will be used instead of the auto-generated demo & API links + */ + customAnnotation?: string; };