xref: /expo/docs/components/plugins/APISection.tsx (revision 97ff9121)
1import React, { useContext } from 'react';
2
3import DocumentationPageContext from '~/components/DocumentationPageContext';
4import { P } from '~/components/base/paragraph';
5import { ClassDefinitionData, GeneratedData } from '~/components/plugins/api/APIDataTypes';
6import APISectionClasses from '~/components/plugins/api/APISectionClasses';
7import APISectionComponents from '~/components/plugins/api/APISectionComponents';
8import APISectionConstants from '~/components/plugins/api/APISectionConstants';
9import APISectionEnums from '~/components/plugins/api/APISectionEnums';
10import APISectionInterfaces from '~/components/plugins/api/APISectionInterfaces';
11import APISectionMethods from '~/components/plugins/api/APISectionMethods';
12import APISectionProps from '~/components/plugins/api/APISectionProps';
13import APISectionTypes from '~/components/plugins/api/APISectionTypes';
14import { TypeDocKind } from '~/components/plugins/api/APISectionUtils';
15
16const LATEST_VERSION = `v${require('~/package.json').version}`;
17
18type Props = {
19  packageName: string;
20  apiName?: string;
21  forceVersion?: string;
22};
23
24const filterDataByKind = (
25  entries: GeneratedData[] = [],
26  kind: TypeDocKind | TypeDocKind[],
27  additionalCondition: (entry: GeneratedData) => boolean = () => true
28) =>
29  entries.filter(
30    (entry: GeneratedData) =>
31      (Array.isArray(kind) ? kind.includes(entry.kind) : entry.kind === kind) &&
32      additionalCondition(entry)
33  );
34
35const isHook = ({ name }: GeneratedData) =>
36  name.startsWith('use') &&
37  // note(simek): hardcode this exception until the method will be renamed
38  name !== 'useSystemBrightnessAsync';
39
40const isListener = ({ name }: GeneratedData) =>
41  name.endsWith('Listener') || name.endsWith('Listeners');
42
43const isProp = ({ name }: GeneratedData) => name.includes('Props') && name !== 'ErrorRecoveryProps';
44
45const isComponent = ({ type, extendedTypes, signatures }: GeneratedData) =>
46  (type?.name && ['React.FC', 'ForwardRefExoticComponent'].includes(type?.name)) ||
47  (extendedTypes && extendedTypes.length ? extendedTypes[0].name === 'Component' : false) ||
48  (signatures && signatures[0]
49    ? signatures[0].type.name === 'Element' ||
50      (signatures[0].parameters && signatures[0].parameters[0].name === 'props')
51    : false);
52
53const isConstant = ({ name, type }: GeneratedData) =>
54  !['default', 'Constants', 'EventEmitter'].includes(name) &&
55  !(type?.name && ['React.FC', 'ForwardRefExoticComponent'].includes(type?.name));
56
57const renderAPI = (
58  packageName: string,
59  version: string = 'unversioned',
60  apiName?: string,
61  isTestMode: boolean = false
62): JSX.Element => {
63  try {
64    // note(simek): When the path prefix is interpolated Next or Webpack fails to locate the file
65    const { children: data } = isTestMode
66      ? require(`../../public/static/data/${version}/${packageName}.json`)
67      : require(`~/public/static/data/${version}/${packageName}.json`);
68
69    const methods = filterDataByKind(
70      data,
71      TypeDocKind.Function,
72      entry => !isListener(entry) && !isHook(entry) && !isComponent(entry)
73    );
74    const hooks = filterDataByKind(data, TypeDocKind.Function, isHook);
75    const eventSubscriptions = filterDataByKind(data, TypeDocKind.Function, isListener);
76
77    const types = filterDataByKind(
78      data,
79      TypeDocKind.TypeAlias,
80      entry =>
81        !isProp(entry) &&
82        !!(
83          entry.type.declaration ||
84          entry.type.types ||
85          entry.type.type ||
86          entry.type.typeArguments
87        )
88    );
89
90    const props = filterDataByKind(
91      data,
92      TypeDocKind.TypeAlias,
93      entry => isProp(entry) && !!(entry.type.types || entry.type.declaration?.children)
94    );
95    const defaultProps = filterDataByKind(
96      data
97        .filter((entry: GeneratedData) => entry.kind === TypeDocKind.Class)
98        .map((entry: GeneratedData) => entry.children)
99        .flat(),
100      TypeDocKind.Property,
101      entry => entry.name === 'defaultProps'
102    )[0];
103
104    const enums = filterDataByKind(data, [TypeDocKind.Enum, TypeDocKind.LegacyEnum]);
105    const interfaces = filterDataByKind(data, TypeDocKind.Interface);
106    const constants = filterDataByKind(data, TypeDocKind.Variable, entry => isConstant(entry));
107
108    const components = filterDataByKind(
109      data,
110      [TypeDocKind.Variable, TypeDocKind.Class, TypeDocKind.Function],
111      entry => isComponent(entry)
112    );
113    const componentsPropNames = components.map(component => `${component.name}Props`);
114    const componentsProps = filterDataByKind(props, TypeDocKind.TypeAlias, entry =>
115      componentsPropNames.includes(entry.name)
116    );
117
118    const classes = filterDataByKind(
119      data,
120      TypeDocKind.Class,
121      entry => !isComponent(entry) && (apiName ? !entry.name.includes(apiName) : true)
122    );
123
124    const componentsChildren = components
125      .map((cls: ClassDefinitionData) =>
126        cls.children?.filter(
127          child =>
128            child.kind === TypeDocKind.Method &&
129            child?.flags?.isExternal !== true &&
130            child.name !== 'render' &&
131            // note(simek): hide unannotated "private" methods
132            !child.name.startsWith('_')
133        )
134      )
135      .flat();
136
137    const methodsNames = methods.map(method => method.name);
138    const staticMethods = componentsChildren.filter(
139      // note(simek): hide duplicate exports for Camera API
140      method => method?.flags?.isStatic === true && !methodsNames.includes(method.name)
141    );
142    const componentMethods = componentsChildren
143      .filter(method => method?.flags?.isStatic !== true && !method?.overwrites)
144      .filter(Boolean);
145
146    return (
147      <>
148        <APISectionComponents data={components} componentsProps={componentsProps} />
149        <APISectionMethods data={staticMethods} header="Static Methods" />
150        <APISectionMethods data={componentMethods} header="Component Methods" />
151        <APISectionConstants data={constants} apiName={apiName} />
152        <APISectionMethods data={hooks} header="Hooks" />
153        <APISectionClasses data={classes} />
154        {props && !componentsProps.length ? (
155          <APISectionProps data={props} defaultProps={defaultProps} />
156        ) : null}
157        <APISectionMethods data={methods} apiName={apiName} />
158        <APISectionMethods
159          data={eventSubscriptions}
160          apiName={apiName}
161          header="Event Subscriptions"
162        />
163        <APISectionTypes data={types} />
164        <APISectionInterfaces data={interfaces} />
165        <APISectionEnums data={enums} />
166      </>
167    );
168  } catch (error) {
169    return <P>No API data file found, sorry!</P>;
170  }
171};
172
173const APISection = ({ packageName, apiName, forceVersion }: Props) => {
174  const { version } = useContext(DocumentationPageContext);
175  const resolvedVersion =
176    forceVersion ||
177    (version === 'unversioned' ? version : version === 'latest' ? LATEST_VERSION : version);
178  return renderAPI(packageName, resolvedVersion, apiName, !!forceVersion);
179};
180
181export default APISection;
182