xref: /expo/docs/components/plugins/APISection.tsx (revision 8b790cc2)
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 { getComponentName, TypeDocKind } 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  if (type?.name && ['React.FC', 'ForwardRefExoticComponent'].includes(type?.name)) {
48    return true;
49  } else if (extendedTypes && extendedTypes.length) {
50    return extendedTypes[0].name === 'Component';
51  } else if (signatures && signatures.length) {
52    if (
53      signatures[0].type.name === 'Element' ||
54      (signatures[0].parameters && signatures[0].parameters[0].name === 'props')
55    ) {
56      return true;
57    }
58  }
59  return false;
60};
61
62const isConstant = ({ name, type }: GeneratedData) =>
63  !['default', 'Constants', 'EventEmitter'].includes(name) &&
64  !(type?.name && ['React.FC', 'ForwardRefExoticComponent'].includes(type?.name));
65
66const renderAPI = (
67  packageName: string,
68  version: string = 'unversioned',
69  apiName?: string,
70  strictTypes: boolean = false,
71  isTestMode: boolean = false
72): JSX.Element => {
73  try {
74    // note(simek): When the path prefix is interpolated Next or Webpack fails to locate the file
75    const { children: data } = isTestMode
76      ? require(`../../public/static/data/${version}/${packageName}.json`)
77      : require(`~/public/static/data/${version}/${packageName}.json`);
78
79    const methods = filterDataByKind(
80      data,
81      TypeDocKind.Function,
82      entry => !isListener(entry) && !isHook(entry) && !isComponent(entry)
83    );
84    const eventSubscriptions = filterDataByKind(data, TypeDocKind.Function, isListener);
85
86    const types = filterDataByKind(
87      data,
88      TypeDocKind.TypeAlias,
89      entry =>
90        !isProp(entry) &&
91        !!(
92          entry.type.declaration ||
93          entry.type.types ||
94          entry.type.type ||
95          entry.type.typeArguments
96        ) &&
97        (strictTypes && apiName ? entry.name.startsWith(apiName) : true)
98    );
99
100    const props = filterDataByKind(
101      data,
102      TypeDocKind.TypeAlias,
103      entry => isProp(entry) && !!(entry.type.types || entry.type.declaration?.children)
104    );
105    const defaultProps = filterDataByKind(
106      data
107        .filter((entry: GeneratedData) => entry.kind === TypeDocKind.Class)
108        .map((entry: GeneratedData) => entry.children)
109        .flat(),
110      TypeDocKind.Property,
111      entry => entry.name === 'defaultProps'
112    )[0];
113
114    const enums = filterDataByKind(
115      data,
116      [TypeDocKind.Enum, TypeDocKind.LegacyEnum],
117      entry => entry.name !== 'default'
118    );
119    const interfaces = filterDataByKind(data, TypeDocKind.Interface);
120    const constants = filterDataByKind(data, TypeDocKind.Variable, entry => isConstant(entry));
121
122    const components = filterDataByKind(
123      data,
124      [TypeDocKind.Variable, TypeDocKind.Class, TypeDocKind.Function],
125      entry => isComponent(entry)
126    );
127    const componentsPropNames = components.map(
128      ({ name, children }) => `${getComponentName(name, children)}Props`
129    );
130    const componentsProps = filterDataByKind(props, TypeDocKind.TypeAlias, entry =>
131      componentsPropNames.includes(entry.name)
132    );
133
134    const classes = filterDataByKind(data, TypeDocKind.Class, entry => !isComponent(entry));
135
136    const componentsChildren = components
137      .map((cls: ClassDefinitionData) =>
138        cls.children?.filter(
139          child =>
140            (child?.kind === TypeDocKind.Method || child?.kind === TypeDocKind.Property) &&
141            child?.flags?.isExternal !== true &&
142            !child.inheritedFrom &&
143            child.name !== 'render' &&
144            // note(simek): hide unannotated "private" methods
145            !child.name.startsWith('_')
146        )
147      )
148      .flat();
149
150    const methodsNames = methods.map(method => method.name);
151    const staticMethods = componentsChildren.filter(
152      // note(simek): hide duplicate exports from class components
153      method =>
154        method?.kind === TypeDocKind.Method &&
155        method?.flags?.isStatic === true &&
156        !methodsNames.includes(method.name) &&
157        !isHook(method as GeneratedData)
158    );
159    const componentMethods = componentsChildren
160      .filter(
161        method =>
162          method?.kind === TypeDocKind.Method &&
163          method?.flags?.isStatic !== true &&
164          !method?.overwrites
165      )
166      .filter(Boolean);
167
168    const hooks = filterDataByKind(
169      [...data, ...componentsChildren].filter(Boolean),
170      [TypeDocKind.Function, TypeDocKind.Property],
171      isHook
172    );
173
174    return (
175      <>
176        <APISectionComponents data={components} componentsProps={componentsProps} />
177        <APISectionMethods data={staticMethods} header="Static Methods" />
178        <APISectionMethods data={componentMethods} header="Component Methods" />
179        <APISectionConstants data={constants} apiName={apiName} />
180        <APISectionMethods data={hooks} header="Hooks" />
181        <APISectionClasses data={classes} />
182        {props && !componentsProps.length ? (
183          <APISectionProps data={props} defaultProps={defaultProps} />
184        ) : null}
185        <APISectionMethods data={methods} apiName={apiName} />
186        <APISectionMethods
187          data={eventSubscriptions}
188          apiName={apiName}
189          header="Event Subscriptions"
190        />
191        <APISectionInterfaces data={interfaces} />
192        <APISectionTypes data={types} />
193        <APISectionEnums data={enums} />
194      </>
195    );
196  } catch {
197    return <P>No API data file found, sorry!</P>;
198  }
199};
200
201const APISection = ({ packageName, apiName, forceVersion, strictTypes = false }: Props) => {
202  const { version } = usePageApiVersion();
203  const resolvedVersion =
204    forceVersion ||
205    (version === 'unversioned' ? version : version === 'latest' ? LATEST_VERSION : version);
206  return renderAPI(packageName, resolvedVersion, apiName, strictTypes, !!forceVersion);
207};
208
209export default APISection;
210