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