xref: /expo/docs/components/plugins/APISection.tsx (revision 7684899a)
1import { ClassDefinitionData, GeneratedData } from '~/components/plugins/api/APIDataTypes';
2import APISectionClasses from '~/components/plugins/api/APISectionClasses';
3import APISectionComponents from '~/components/plugins/api/APISectionComponents';
4import APISectionConstants from '~/components/plugins/api/APISectionConstants';
5import APISectionEnums from '~/components/plugins/api/APISectionEnums';
6import APISectionInterfaces from '~/components/plugins/api/APISectionInterfaces';
7import APISectionMethods from '~/components/plugins/api/APISectionMethods';
8import APISectionNamespaces from '~/components/plugins/api/APISectionNamespaces';
9import APISectionProps from '~/components/plugins/api/APISectionProps';
10import APISectionTypes from '~/components/plugins/api/APISectionTypes';
11import {
12  getCommentContent,
13  getComponentName,
14  TypeDocKind,
15} from '~/components/plugins/api/APISectionUtils';
16import { usePageApiVersion } from '~/providers/page-api-version';
17import versions from '~/public/static/constants/versions.json';
18import { P } from '~/ui/components/Text';
19
20const { LATEST_VERSION } = versions;
21
22type Props = {
23  packageName: string;
24  apiName?: string;
25  forceVersion?: string;
26  strictTypes?: boolean;
27  testRequire?: any;
28  headersMapping?: Record<string, string>;
29};
30
31const filterDataByKind = (
32  entries: GeneratedData[] = [],
33  kind: TypeDocKind | TypeDocKind[],
34  additionalCondition: (entry: GeneratedData) => boolean = () => true
35) =>
36  entries.filter(
37    (entry: GeneratedData) =>
38      (Array.isArray(kind) ? kind.includes(entry.kind) : entry.kind === kind) &&
39      additionalCondition(entry)
40  );
41
42const isHook = ({ name }: GeneratedData) =>
43  name.startsWith('use') &&
44  // note(simek): hardcode this exception until the method will be renamed
45  name !== 'useSystemBrightnessAsync';
46
47const isListener = ({ name }: GeneratedData) =>
48  name.endsWith('Listener') || name.endsWith('Listeners');
49
50const isProp = ({ name }: GeneratedData) => name.includes('Props') && name !== 'ErrorRecoveryProps';
51
52const isComponent = ({ type, extendedTypes, signatures }: GeneratedData) => {
53  if (type?.name && ['React.FC', 'ForwardRefExoticComponent'].includes(type?.name)) {
54    return true;
55  } else if (extendedTypes && extendedTypes.length) {
56    return extendedTypes[0].name === 'Component' || extendedTypes[0].name === 'PureComponent';
57  } else if (signatures && signatures.length) {
58    if (
59      signatures[0].type.name === 'Element' ||
60      (signatures[0].type.types && signatures[0].type.types.map(t => t.name).includes('Element')) ||
61      (signatures[0].parameters && signatures[0].parameters[0].name === 'props')
62    ) {
63      return true;
64    }
65  }
66  return false;
67};
68
69const isConstant = ({ name, type }: GeneratedData) =>
70  !['default', 'Constants', 'EventEmitter'].includes(name) &&
71  !(type?.name && ['React.FC', 'ForwardRefExoticComponent'].includes(type?.name));
72
73const hasCategoryHeader = ({ signatures }: GeneratedData): boolean =>
74  (signatures &&
75    signatures[0].comment?.blockTags &&
76    signatures[0].comment.blockTags.length > 0 &&
77    signatures[0].comment.blockTags.filter(tag => tag?.tag === '@header').length > 0) ??
78  false;
79
80const groupByHeader = (entries: GeneratedData[]) => {
81  return entries.reduce((group: Record<string, GeneratedData[]>, entry) => {
82    const signature = entry.signatures[0];
83    const header = getCommentContent(
84      signature.comment?.blockTags?.filter(tag => tag.tag === '@header')[0].content ?? []
85    );
86    if (header) {
87      group[header] = group[header] ?? [];
88      group[header].push(entry);
89    }
90    return group;
91  }, {});
92};
93
94const renderAPI = (
95  packageName: string,
96  version: string = 'unversioned',
97  apiName?: string,
98  strictTypes: boolean = false,
99  testRequire: any = undefined,
100  headersMapping: Record<string, string> = {}
101): JSX.Element => {
102  try {
103    const { children: data } = testRequire
104      ? testRequire(`~/public/static/data/${version}/${packageName}.json`)
105      : require(`~/public/static/data/${version}/${packageName}.json`);
106
107    const methods = filterDataByKind(
108      data,
109      TypeDocKind.Function,
110      entry =>
111        !isListener(entry) && !isHook(entry) && !isComponent(entry) && !hasCategoryHeader(entry)
112    );
113    const eventSubscriptions = filterDataByKind(
114      data,
115      TypeDocKind.Function,
116      entry => isListener(entry) && !hasCategoryHeader(entry)
117    );
118
119    const categorizedMethods = groupByHeader(
120      filterDataByKind(
121        data,
122        TypeDocKind.Function,
123        entry => !isComponent(entry) && hasCategoryHeader(entry)
124      )
125    );
126    const hasCategorizedMethods = Object.keys(categorizedMethods).length > 0;
127    const hasHeadersMapping = Object.keys(headersMapping).length;
128
129    const types = filterDataByKind(
130      data,
131      TypeDocKind.TypeAlias,
132      entry =>
133        !isProp(entry) &&
134        !!(
135          entry.type.declaration ||
136          entry.type.types ||
137          entry.type.type ||
138          entry.type.typeArguments
139        ) &&
140        (strictTypes && apiName ? entry.name.startsWith(apiName) : true)
141    );
142
143    const props = filterDataByKind(
144      data,
145      [TypeDocKind.TypeAlias, TypeDocKind.Interface],
146      entry =>
147        isProp(entry) &&
148        (entry.kind === TypeDocKind.TypeAlias
149          ? !!(entry.type.types || entry.type.declaration?.children)
150          : true)
151    );
152    const defaultProps = filterDataByKind(
153      data
154        .filter((entry: GeneratedData) => entry.kind === TypeDocKind.Class)
155        .map((entry: GeneratedData) => entry.children)
156        .flat(),
157      TypeDocKind.Property,
158      entry => entry.name === 'defaultProps'
159    )[0];
160
161    const enums = filterDataByKind(data, TypeDocKind.Enum, entry => entry.name !== 'default');
162    const interfaces = filterDataByKind(
163      data,
164      TypeDocKind.Interface,
165      entry => !entry.name.includes('Props')
166    );
167    const constants = filterDataByKind(data, TypeDocKind.Variable, entry => isConstant(entry));
168
169    const components = filterDataByKind(
170      data,
171      [TypeDocKind.Variable, TypeDocKind.Class, TypeDocKind.Function],
172      entry => isComponent(entry)
173    );
174    const componentsPropNames = components.map(
175      ({ name, children }) => `${getComponentName(name, children)}Props`
176    );
177    const componentsProps = filterDataByKind(
178      props,
179      [TypeDocKind.TypeAlias, TypeDocKind.Interface],
180      entry => componentsPropNames.includes(entry.name)
181    );
182
183    const namespaces = filterDataByKind(data, TypeDocKind.Namespace);
184
185    const classes = filterDataByKind(
186      data,
187      TypeDocKind.Class,
188      entry => !isComponent(entry) && entry.name !== 'default'
189    );
190
191    const componentsChildren = components
192      .map((cls: ClassDefinitionData) =>
193        cls.children?.filter(
194          child =>
195            (child?.kind === TypeDocKind.Method || child?.kind === TypeDocKind.Property) &&
196            !child.inheritedFrom &&
197            child.name !== 'render' &&
198            // note(simek): hide unannotated "private" methods
199            !child.name.startsWith('_')
200        )
201      )
202      .flat();
203
204    const methodsNames = methods.map(method => method.name);
205    const staticMethods = componentsChildren.filter(
206      // note(simek): hide duplicate exports from class components
207      method =>
208        method?.kind === TypeDocKind.Method &&
209        method?.flags?.isStatic === true &&
210        !methodsNames.includes(method.name) &&
211        !isHook(method as GeneratedData)
212    );
213    const componentMethods = componentsChildren
214      .filter(
215        method =>
216          method?.kind === TypeDocKind.Method &&
217          method?.flags?.isStatic !== true &&
218          !method?.overwrites
219      )
220      .filter(Boolean);
221
222    const hooks = filterDataByKind(
223      [...data, ...componentsChildren].filter(Boolean),
224      [TypeDocKind.Function, TypeDocKind.Property],
225      entry => isHook(entry) && !hasCategoryHeader(entry)
226    );
227
228    return (
229      <>
230        {hasCategorizedMethods &&
231          (hasHeadersMapping
232            ? Object.entries(headersMapping).map(([key, header], index) => (
233                <APISectionMethods
234                  data={categorizedMethods[key]}
235                  header={header}
236                  key={`${header}-${index}`}
237                />
238              ))
239            : Object.entries(categorizedMethods).map(([key, data], index) => (
240                <APISectionMethods data={data} header={key} key={`${key}-${index}`} />
241              )))}
242        <APISectionComponents data={components} componentsProps={componentsProps} />
243        <APISectionMethods data={staticMethods} header="Static Methods" />
244        <APISectionMethods data={componentMethods} header="Component Methods" />
245        <APISectionConstants data={constants} apiName={apiName} />
246        <APISectionMethods data={hooks} header="Hooks" />
247        <APISectionClasses data={classes} />
248        {props && !componentsProps.length ? (
249          <APISectionProps data={props} defaultProps={defaultProps} />
250        ) : null}
251        <APISectionMethods data={methods} apiName={apiName} />
252        <APISectionMethods
253          data={eventSubscriptions}
254          apiName={apiName}
255          header="Event Subscriptions"
256        />
257        <APISectionNamespaces data={namespaces} />
258        <APISectionInterfaces data={interfaces} />
259        <APISectionTypes data={types} />
260        <APISectionEnums data={enums} />
261      </>
262    );
263  } catch {
264    return <P>No API data file found, sorry!</P>;
265  }
266};
267
268const APISection = ({
269  packageName,
270  apiName,
271  forceVersion,
272  strictTypes = false,
273  testRequire = undefined,
274  headersMapping = {},
275}: Props) => {
276  const { version } = usePageApiVersion();
277  const resolvedVersion =
278    forceVersion ||
279    (version === 'unversioned' ? version : version === 'latest' ? LATEST_VERSION : version);
280  return renderAPI(packageName, resolvedVersion, apiName, strictTypes, testRequire, headersMapping);
281};
282
283export default APISection;
284