Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@
"prettier": "^3.3.3",
"pretty-quick": "^4.0.0",
"process": "^0.11.10",
"react-docgen-typescript": "^2.2.2",
"rimraf": "^5.0.10",
"serve": "^14.2.3",
"stylelint": "^15.11.0",
Expand Down
148 changes: 53 additions & 95 deletions packages/api-docs-builder/ApiBuilders/ComponentApiBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,31 @@
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 { renderMarkdown } from '@mui/internal-markdown';
import { Annotation, parse as parseDoctrine } from 'doctrine';
import { readFileSync, writeFileSync } from 'fs';
import * as _ from 'lodash';
import kebabCase from 'lodash/kebabCase';
import type { Link } from 'mdast';
import path from 'path';
import { ComponentDoc, parse as docgenParse } from 'react-docgen-typescript';
import remark from 'remark';
import remarkVisit from 'unist-util-visit';
import type { Link } from 'mdast';
import { defaultHandlers, parse as docgenParse } from 'react-docgen';
import { renderMarkdown } from '@mui/internal-markdown';
import { parse as parseDoctrine, Annotation } from 'doctrine';
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,
} from '../types/ApiBuilder.types';
import { Slot, ComponentInfo } from '../types/utils.types';
import { ComponentInfo, Slot } from '../types/utils.types';
import createDescribableProp, { DescribablePropDescriptor } from '../utils/createDescribableProp';
import { TypeScriptProject } from '../utils/createTypeScriptProject';
import generateApiTranslations from '../utils/generateApiTranslation';
import generatePropDescription from '../utils/generatePropDescription';
import generatePropTypeDescription from '../utils/generatePropTypeDescription';
import parseSlotsAndClasses from '../utils/parseSlotsAndClasses';
import parseTest from '../utils/parseTest';
import { sortAlphabetical } from '../utils/sortObjects';

const cssComponents = ['Box', 'Grid', 'Typography', 'Stack'];

Expand Down Expand Up @@ -394,21 +389,17 @@ const generateApiPage = async (
}
};

const attachTranslations = (
reactApi: ComponentReactApi,
deprecationInfo: string | undefined,
settings?: CreateDescribeablePropSettings,
) => {
const attachTranslations = (reactApi: ComponentReactApi, deprecationInfo: string | undefined) => {
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;
let prop: DescribablePropDescriptor | null;
try {
prop = createDescribeableProp(propDescriptor, propName, settings);
prop = createDescribableProp(propDescriptor, propName);
} catch (error) {
prop = null;
}
Expand Down Expand Up @@ -463,17 +454,14 @@ const attachTranslations = (
reactApi.translations = translations;
};

const attachPropsTable = (
reactApi: ComponentReactApi,
settings?: CreateDescribeablePropSettings,
) => {
const attachPropsTable = (reactApi: ComponentReactApi) => {
const propErrors: Array<[propName: string, error: Error]> = [];
type Pair = [string, ComponentReactApi['propsTable'][string]];
const componentProps: ComponentReactApi['propsTable'] = _.fromPairs(
Object.entries(reactApi.props!).map(([propName, propDescriptor]): Pair => {
let prop: DescribeablePropDescriptor | null;
Object.entries(reactApi.props).map(([propName, propDescriptor]): Pair => {
let prop: DescribablePropDescriptor | null;
try {
prop = createDescribeableProp(propDescriptor, propName, settings);
prop = createDescribableProp(propDescriptor, propName);
} catch (error) {
propErrors.push([`[${reactApi.name}] \`${propName}\``, error as Error]);
prop = null;
Expand All @@ -483,7 +471,7 @@ const attachPropsTable = (
return [] as any;
}

const defaultValue = propDescriptor.jsdocDefaultValue?.value;
const defaultValue = propDescriptor.defaultValue?.value;

const {
signature: signatureType,
Expand All @@ -492,12 +480,8 @@ const attachPropsTable = (
seeMore,
} = generatePropDescription(prop, propName);
const propTypeDescription = generatePropTypeDescription(propDescriptor.type);
const chainedPropType = getChained(prop.type);

const requiredProp =
prop.required ||
/\.isRequired/.test(prop.type.raw) ||
(chainedPropType !== false && chainedPropType.required);
const requiredProp = prop.required;

const deprecation = (propDescriptor.description || '').match(/@deprecated(\s+(?<info>.*))?/);

Expand Down Expand Up @@ -607,6 +591,18 @@ const defaultGetComponentImports = (name: string, filename: string) => {
return [subpathImport, rootImport];
};

const componentDocToComponentApi = (componentDoc?: ComponentDoc): ComponentReactApi => {
if (!componentDoc) {
return {} as ComponentReactApi;
}

return {
description: componentDoc.description,
name: componentDoc.displayName,
props: componentDoc.props,
} as ComponentReactApi;
};

/**
* - Build react component (specified filename) api by lookup at its definition (.d.ts or ts)
* and then generate the API page + json data
Expand All @@ -626,66 +622,28 @@ export default async function generateComponentApi(
}

const filename = componentInfo.filename;
let reactApi: ComponentReactApi;
const options = {
savePropValueAsString: false,
shouldExtractLiteralValuesFromEnum: true,
shouldExtractValuesFromUnion: true,
shouldRemoveUndefinedFromOptional: true,
shouldIncludePropTagMap: true,
};

if (componentInfo.isSystemComponent || componentInfo.name === 'Grid2') {
try {
reactApi = docgenParse(
src,
(ast) => {
let node;
astTypes.visit(ast, {
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,
{ 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, null, defaultHandlers.concat(muiDefaultPropsHandler), {
filename,
});
} else {
throw error;
}
}
} else {
reactApi = docgenParse(src, null, defaultHandlers.concat(muiDefaultPropsHandler), {
filename,
});
const reactApi = componentDocToComponentApi(docgenParse(componentInfo.filename, options)?.at(0));

if (!reactApi) {
throw new Error(`No suitable component definition found in ${filename}`);
}

if (!reactApi.props) {
reactApi.props = {};
}

if (!reactApi.description) {
reactApi.description = '';
}

const { getComponentImports = defaultGetComponentImports } = projectSettings;
const componentJsdoc = parseDoctrine(reactApi.description);

Expand Down Expand Up @@ -750,8 +708,8 @@ export default async function generateComponentApi(

reactApi.deprecated = !!deprecation || undefined;

attachPropsTable(reactApi, projectSettings.propsSettings);
attachTranslations(reactApi, deprecationInfo, projectSettings.propsSettings);
attachPropsTable(reactApi);
attachTranslations(reactApi, deprecationInfo);

// eslint-disable-next-line no-console
console.log('Built API docs for', reactApi.apiPathname);
Expand Down
8 changes: 4 additions & 4 deletions packages/api-docs-builder/ProjectSettings.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { CreateTypeScriptProjectOptions } from './utils/createTypeScriptProject';
import { CreateDescribeablePropSettings } from './utils/createDescribeableProp';
import {
ComponentClassDefinition,
ComponentReactApi,
HookReactApi,
} from './types/ApiBuilder.types';
import { Slot, ComponentInfo, HookInfo } from './types/utils.types';
import { ComponentInfo, HookInfo, Slot } from './types/utils.types';
import { CreateDescribablePropSettings } from './utils/createDescribableProp';
import { CreateTypeScriptProjectOptions } from './utils/createTypeScriptProject';

export type SortingStrategiesType = {
/**
Expand Down Expand Up @@ -89,7 +89,7 @@ export interface ProjectSettings {
/**
* Settings to configure props definition tests.
*/
propsSettings?: CreateDescribeablePropSettings;
propsSettings?: CreateDescribablePropSettings;
/**
* If `true`, the script does not generate JS page file.
* Once we have the API tabs in all projects, we can make this `true` by default.
Expand Down
6 changes: 3 additions & 3 deletions packages/api-docs-builder/types/ApiBuilder.types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ReactDocgenApi } from 'react-docgen';
import { ComponentDoc } from 'react-docgen-typescript';
import { JSDocTagInfo } from 'typescript';
import { ComponentInfo, Slot, HookInfo, SeeMore } from './utils.types';
import { ComponentInfo, HookInfo, SeeMore, Slot } from './utils.types';

export type AdditionalPropsInfo = {
cssApi?: boolean;
Expand All @@ -14,7 +14,7 @@ export type AdditionalPropsInfo = {
/**
* Common interface for both Component and Hook API builders.
*/
interface CommonReactApi extends ReactDocgenApi {
interface CommonReactApi extends Pick<ComponentDoc, 'description' | 'props'> {
demos: ReturnType<HookInfo['getDemos']>;
EOL: string;
filename: string;
Expand Down
61 changes: 61 additions & 0 deletions packages/api-docs-builder/utils/createDescribableProp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import * as doctrine from 'doctrine';
import { PropItem, PropItemType } from 'react-docgen-typescript';

export interface DescribablePropDescriptor {
annotation: doctrine.Annotation;
defaultValue: string | null;
required: boolean;
type: PropItemType;
tags: PropItem['tags'];
}

export type CreateDescribablePropSettings = {
/**
* Names of props that do not check if the annotations equal runtime default.
*/
propsWithoutDefaultVerification?: string[];
};

/**
* Returns `null` if the prop should be ignored.
* Throws if it is invalid.
* @param prop
* @param propName
*/
export default function createDescribableProp(
prop: PropItem,
propName: string,
): DescribablePropDescriptor | null {
const { defaultValue, description, required, type, tags } = prop;

const renderedDefaultValue = defaultValue?.value.replace(/\r?\n/g, '');
const renderDefaultValue = Boolean(
renderedDefaultValue &&
// Ignore "large" default values that would break the table layout.
renderedDefaultValue.length <= 150,
);

if (description === undefined) {
throw new Error(`The "${propName}" prop is missing a description.`);
}

const annotation = doctrine.parse(description, {
sloppy: true,
});

if (
description.trim() === '' ||
// @ts-expect-error empty object type
tags?.ignore
) {
return null;
}

return {
annotation,
defaultValue: renderDefaultValue ? renderedDefaultValue! : null,
required: Boolean(required),
type,
tags: prop.tags,
};
}
Loading