1import { css } from '@emotion/react';
2import { theme } from '@expo/styleguide';
3import React from 'react';
4import ReactMarkdown from 'react-markdown';
5import remarkGfm from 'remark-gfm';
6
7import { Code, InlineCode } from '~/components/base/code';
8import { H4 } from '~/components/base/headings';
9import Link from '~/components/base/link';
10import { LI, UL } from '~/components/base/list';
11import { B, P, Quote } from '~/components/base/paragraph';
12import {
13  CommentData,
14  MethodParamData,
15  MethodSignatureData,
16  PropData,
17  TypeDefinitionData,
18  TypePropertyDataFlags,
19} from '~/components/plugins/api/APIDataTypes';
20
21export enum TypeDocKind {
22  LegacyEnum = 4,
23  Enum = 8,
24  Variable = 32,
25  Function = 64,
26  Class = 128,
27  Interface = 256,
28  Property = 1024,
29  Method = 2048,
30  TypeAlias = 4194304,
31}
32
33export type MDComponents = React.ComponentProps<typeof ReactMarkdown>['components'];
34
35export const mdComponents: MDComponents = {
36  blockquote: ({ children }) => (
37    <Quote>
38      {/* @ts-ignore - current implementation produce type issues, this would be fixed in docs redesign */}
39      {children.map(child => (child?.props?.node?.tagName === 'p' ? child?.props.children : child))}
40    </Quote>
41  ),
42  code: ({ children, className }) =>
43    className ? <Code className={className}>{children}</Code> : <InlineCode>{children}</InlineCode>,
44  h1: ({ children }) => <H4>{children}</H4>,
45  ul: ({ children }) => <UL>{children}</UL>,
46  li: ({ children }) => <LI>{children}</LI>,
47  a: ({ href, children }) => <Link href={href}>{children}</Link>,
48  p: ({ children }) => (children ? <P>{children}</P> : null),
49  strong: ({ children }) => <B>{children}</B>,
50  span: ({ children }) => (children ? <span>{children}</span> : null),
51};
52
53export const mdInlineComponents: MDComponents = {
54  ...mdComponents,
55  p: ({ children }) => (children ? <span>{children}</span> : null),
56};
57
58const nonLinkableTypes = [
59  'ColorValue',
60  'Component',
61  'E',
62  'EventSubscription',
63  'File',
64  'FileList',
65  'Manifest',
66  'NativeSyntheticEvent',
67  'ParsedQs',
68  'ServiceActionResult',
69  'T',
70  'TaskOptions',
71  'Uint8Array',
72  // React & React Native
73  'React.FC',
74  'ForwardRefExoticComponent',
75  'StyleProp',
76  // Cross-package permissions management
77  'RequestPermissionMethod',
78  'GetPermissionMethod',
79  'Options',
80  'PermissionHookBehavior',
81];
82
83/**
84 * List of type names that should not be visible in the docs.
85 */
86const omittableTypes = [
87  // Internal React type that adds `ref` prop to the component
88  'RefAttributes',
89];
90
91/**
92 * Map of internal names/type names that should be replaced with something more developer-friendly.
93 */
94const replaceableTypes: Partial<Record<string, string>> = {
95  ForwardRefExoticComponent: 'Component',
96};
97
98const hardcodedTypeLinks: Record<string, string> = {
99  Date: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date',
100  Element: 'https://www.typescriptlang.org/docs/handbook/jsx.html#function-component',
101  Error: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error',
102  ExpoConfig: 'https://github.com/expo/expo-cli/blob/main/packages/config-types/src/ExpoConfig.ts',
103  MessageEvent: 'https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent',
104  Omit: 'https://www.typescriptlang.org/docs/handbook/utility-types.html#omittype-keys',
105  Pick: 'https://www.typescriptlang.org/docs/handbook/utility-types.html#picktype-keys',
106  Partial: 'https://www.typescriptlang.org/docs/handbook/utility-types.html#partialtype',
107  Promise:
108    'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise',
109  View: '../../react-native/view',
110  ViewProps: '../../react-native/view#props',
111  ViewStyle: '../../react-native/view-style-props/',
112};
113
114const renderWithLink = (name: string, type?: string) => {
115  const replacedName = replaceableTypes[name] ?? name;
116
117  return nonLinkableTypes.includes(replacedName) ? (
118    replacedName + (type === 'array' ? '[]' : '')
119  ) : (
120    <Link
121      href={hardcodedTypeLinks[replacedName] || `#${replacedName.toLowerCase()}`}
122      key={`type-link-${replacedName}`}>
123      {replacedName}
124      {type === 'array' && '[]'}
125    </Link>
126  );
127};
128
129const renderUnion = (types: TypeDefinitionData[]) =>
130  types.map(resolveTypeName).map((valueToRender, index) => (
131    <span key={`union-type-${index}`}>
132      {valueToRender}
133      {index + 1 !== types.length && ' | '}
134    </span>
135  ));
136
137export const resolveTypeName = ({
138  elements,
139  elementType,
140  name,
141  type,
142  types,
143  typeArguments,
144  declaration,
145  value,
146  queryType,
147  operator,
148}: TypeDefinitionData): string | JSX.Element | (string | JSX.Element)[] => {
149  try {
150    if (name) {
151      if (type === 'reference') {
152        if (typeArguments) {
153          if (name === 'Record' || name === 'React.ComponentProps') {
154            return (
155              <>
156                {name}&lt;
157                {typeArguments.map((type, index) => (
158                  <span key={`record-type-${index}`}>
159                    {resolveTypeName(type)}
160                    {index !== typeArguments.length - 1 ? ', ' : null}
161                  </span>
162                ))}
163                &gt;
164              </>
165            );
166          } else {
167            return (
168              <>
169                {renderWithLink(name)}
170                &lt;
171                {typeArguments.map((type, index) => (
172                  <span key={`${name}-nested-type-${index}`}>
173                    {resolveTypeName(type)}
174                    {index !== typeArguments.length - 1 ? ', ' : null}
175                  </span>
176                ))}
177                &gt;
178              </>
179            );
180          }
181        } else {
182          return renderWithLink(name);
183        }
184      } else {
185        return name;
186      }
187    } else if (elementType?.name) {
188      if (elementType.type === 'reference') {
189        return renderWithLink(elementType.name, type);
190      } else if (type === 'array') {
191        return elementType.name + '[]';
192      }
193      return elementType.name + type;
194    } else if (elementType?.declaration) {
195      if (type === 'array') {
196        const { parameters, type: paramType } = elementType.declaration.indexSignature || {};
197        if (parameters && paramType) {
198          return `{ [${listParams(parameters)}]: ${resolveTypeName(paramType)} }`;
199        }
200      }
201      return elementType.name + type;
202    } else if (type === 'union' && types?.length) {
203      return renderUnion(types);
204    } else if (elementType && elementType.type === 'union' && elementType?.types?.length) {
205      const unionTypes = elementType?.types || [];
206      return (
207        <>
208          ({renderUnion(unionTypes)}){type === 'array' && '[]'}
209        </>
210      );
211    } else if (declaration?.signatures) {
212      const baseSignature = declaration.signatures[0];
213      if (baseSignature?.parameters?.length) {
214        return (
215          <>
216            (
217            {baseSignature.parameters?.map((param, index) => (
218              <span key={`param-${index}-${param.name}`}>
219                {param.name}: {resolveTypeName(param.type)}
220                {index + 1 !== baseSignature.parameters?.length && ', '}
221              </span>
222            ))}
223            ) {'=>'} {resolveTypeName(baseSignature.type)}
224          </>
225        );
226      } else {
227        return (
228          <>
229            {'() =>'} {resolveTypeName(baseSignature.type)}
230          </>
231        );
232      }
233    } else if (type === 'reflection' && declaration?.children) {
234      return (
235        <>
236          {'{ '}
237          {declaration?.children.map((child: PropData, i) => (
238            <span key={`reflection-${name}-${i}`}>
239              {child.name + ': ' + resolveTypeName(child.type)}
240              {i + 1 !== declaration?.children?.length ? ', ' : null}
241            </span>
242          ))}
243          {' }'}
244        </>
245      );
246    } else if (type === 'tuple' && elements) {
247      return (
248        <>
249          [
250          {elements.map((elem, i) => (
251            <span key={`tuple-${name}-${i}`}>
252              {resolveTypeName(elem)}
253              {i + 1 !== elements.length ? ', ' : null}
254            </span>
255          ))}
256          ]
257        </>
258      );
259    } else if (type === 'query' && queryType) {
260      return queryType.name;
261    } else if (type === 'literal' && typeof value === 'boolean') {
262      return `${value}`;
263    } else if (type === 'literal' && value) {
264      return `'${value}'`;
265    } else if (type === 'intersection' && types) {
266      return types
267        .filter(({ name }) => !omittableTypes.includes(name ?? ''))
268        .map((value, index, array) => (
269          <span key={`intersection-${name}-${index}`}>
270            {resolveTypeName(value)}
271            {index + 1 !== array.length && ' & '}
272          </span>
273        ));
274    } else if (type === 'typeOperator') {
275      return operator || 'undefined';
276    } else if (value === null) {
277      return 'null';
278    }
279    return 'undefined';
280  } catch (e) {
281    console.warn('Type resolve has failed!', e);
282    return 'undefined';
283  }
284};
285
286export const parseParamName = (name: string) => (name.startsWith('__') ? name.substr(2) : name);
287
288export const renderParam = ({ comment, name, type, flags }: MethodParamData): JSX.Element => (
289  <LI key={`param-${name}`}>
290    <B>
291      {parseParamName(name)}
292      {flags?.isOptional && '?'} (<InlineCode>{resolveTypeName(type)}</InlineCode>)
293    </B>
294    <CommentTextBlock comment={comment} components={mdInlineComponents} withDash />
295  </LI>
296);
297
298export const listParams = (parameters: MethodParamData[]) =>
299  parameters ? parameters?.map(param => parseParamName(param.name)).join(', ') : '';
300
301export const renderTypeOrSignatureType = (
302  type?: TypeDefinitionData,
303  signatures?: MethodSignatureData[],
304  includeParamType: boolean = false
305) => {
306  if (type) {
307    return <InlineCode key={`signature-type-${type.name}`}>{resolveTypeName(type)}</InlineCode>;
308  } else if (signatures && signatures.length) {
309    return signatures.map(({ name, type, parameters }) => (
310      <InlineCode key={`signature-type-${name}`}>
311        (
312        {parameters && includeParamType
313          ? parameters.map(param => (
314              <span key={`signature-param-${param.name}`}>
315                {param.name}
316                {param.flags?.isOptional && '?'}: {resolveTypeName(param.type)}
317              </span>
318            ))
319          : listParams(parameters)}
320        ) =&gt; {resolveTypeName(type)}
321      </InlineCode>
322    ));
323  }
324  return undefined;
325};
326
327export const renderFlags = (flags?: TypePropertyDataFlags) =>
328  flags?.isOptional ? (
329    <>
330      <br />
331      <span css={STYLES_OPTIONAL}>(optional)</span>
332    </>
333  ) : undefined;
334
335export type CommentTextBlockProps = {
336  comment?: CommentData;
337  components?: MDComponents;
338  withDash?: boolean;
339  beforeContent?: JSX.Element;
340  includePlatforms?: boolean;
341};
342
343export const parseCommentContent = (content?: string): string =>
344  content && content.length ? content.replace(/&ast;/g, '*').replace(/\t/g, '') : '';
345
346export const getCommentOrSignatureComment = (
347  comment?: CommentData,
348  signatures?: MethodSignatureData[]
349) => comment || (signatures && signatures[0]?.comment);
350
351export const getTagData = (tagName: string, comment?: CommentData) =>
352  getAllTagData(tagName, comment)?.[0];
353
354export const getAllTagData = (tagName: string, comment?: CommentData) =>
355  comment?.tags?.filter(tag => tag.tag === tagName);
356
357const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);
358
359const formatPlatformName = (name: string) => {
360  const cleanName = name.toLowerCase().replace('\n', '');
361  return cleanName.includes('ios')
362    ? cleanName.replace('ios', 'iOS')
363    : cleanName.includes('expo')
364    ? cleanName.replace('expo', 'Expo Go')
365    : capitalize(name);
366};
367
368export const getPlatformTags = (comment?: CommentData, breakLine: boolean = true) => {
369  const platforms = getAllTagData('platform', comment);
370  return platforms?.length ? (
371    <>
372      {platforms.map(platform => (
373        <div key={platform.text} css={STYLES_PLATFORM}>
374          {formatPlatformName(platform.text)} Only
375        </div>
376      ))}
377      {breakLine && <br />}
378    </>
379  ) : null;
380};
381
382export const CommentTextBlock = ({
383  comment,
384  components = mdComponents,
385  withDash,
386  beforeContent,
387  includePlatforms = true,
388}: CommentTextBlockProps) => {
389  const shortText = comment?.shortText?.trim().length ? (
390    <ReactMarkdown components={components} remarkPlugins={[remarkGfm]}>
391      {parseCommentContent(comment.shortText)}
392    </ReactMarkdown>
393  ) : null;
394  const text = comment?.text?.trim().length ? (
395    <ReactMarkdown components={components} remarkPlugins={[remarkGfm]}>
396      {parseCommentContent(comment.text)}
397    </ReactMarkdown>
398  ) : null;
399
400  const examples = getAllTagData('example', comment);
401  const exampleText = examples?.map((example, index) => (
402    <React.Fragment key={'Example-' + index}>
403      <H4>Example</H4>
404      <ReactMarkdown components={components}>{example.text}</ReactMarkdown>
405    </React.Fragment>
406  ));
407
408  const deprecation = getTagData('deprecated', comment);
409  const deprecationNote = deprecation ? (
410    <Quote key="deprecation-note">
411      {deprecation.text.trim().length ? (
412        <ReactMarkdown components={mdInlineComponents}>{deprecation.text}</ReactMarkdown>
413      ) : (
414        <B>Deprecated</B>
415      )}
416    </Quote>
417  ) : null;
418
419  const see = getTagData('see', comment);
420  const seeText = see ? (
421    <Quote>
422      <B>See: </B>
423      <ReactMarkdown components={mdInlineComponents}>{see.text}</ReactMarkdown>
424    </Quote>
425  ) : null;
426
427  return (
428    <>
429      {deprecationNote}
430      {beforeContent}
431      {withDash && (shortText || text) && ' - '}
432      {includePlatforms && getPlatformTags(comment, !withDash)}
433      {shortText}
434      {text}
435      {seeText}
436      {exampleText}
437    </>
438  );
439};
440
441export const STYLES_OPTIONAL = css`
442  color: ${theme.text.secondary};
443  font-size: 90%;
444  padding-top: 22px;
445`;
446
447export const STYLES_SECONDARY = css`
448  color: ${theme.text.secondary};
449  font-size: 90%;
450  font-weight: 600;
451`;
452
453export const STYLES_PLATFORM = css`
454  display: inline-block;
455  background-color: ${theme.background.tertiary};
456  color: ${theme.text.default};
457  font-size: 90%;
458  font-weight: 700;
459  padding: 6px 12px;
460  margin-bottom: 8px;
461  margin-right: 8px;
462  border-radius: 4px;
463`;
464