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