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  /**
96   *
97   */
98  ForwardRefExoticComponent: 'Component',
99};
100
101const hardcodedTypeLinks: Record<string, string> = {
102  Date: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date',
103  Element: 'https://www.typescriptlang.org/docs/handbook/jsx.html#function-component',
104  Error: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error',
105  ExpoConfig:
106    'https://github.com/expo/expo-cli/blob/master/packages/config-types/src/ExpoConfig.ts',
107  MessageEvent: 'https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent',
108  Omit: 'https://www.typescriptlang.org/docs/handbook/utility-types.html#omittype-keys',
109  Pick: 'https://www.typescriptlang.org/docs/handbook/utility-types.html#picktype-keys',
110  Partial: 'https://www.typescriptlang.org/docs/handbook/utility-types.html#partialtype',
111  Promise:
112    'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise',
113  View: '../../react-native/view',
114  ViewProps: '../../react-native/view#props',
115  ViewStyle: '../../react-native/view-style-props/',
116};
117
118const renderWithLink = (name: string, type?: string) => {
119  const replacedName = replaceableTypes[name] ?? name;
120
121  return nonLinkableTypes.includes(replacedName) ? (
122    replacedName + (type === 'array' ? '[]' : '')
123  ) : (
124    <Link
125      href={hardcodedTypeLinks[replacedName] || `#${replacedName.toLowerCase()}`}
126      key={`type-link-${replacedName}`}>
127      {replacedName}
128      {type === 'array' && '[]'}
129    </Link>
130  );
131};
132
133const renderUnion = (types: TypeDefinitionData[]) =>
134  types.map(resolveTypeName).map((valueToRender, index) => (
135    <span key={`union-type-${index}`}>
136      {valueToRender}
137      {index + 1 !== types.length && ' | '}
138    </span>
139  ));
140
141export const resolveTypeName = ({
142  elements,
143  elementType,
144  name,
145  type,
146  types,
147  typeArguments,
148  declaration,
149  value,
150  queryType,
151}: TypeDefinitionData): string | JSX.Element | (string | JSX.Element)[] => {
152  try {
153    if (name) {
154      if (type === 'reference') {
155        if (typeArguments) {
156          if (name === 'Record' || name === 'React.ComponentProps') {
157            return (
158              <>
159                {name}&lt;
160                {typeArguments.map((type, index) => (
161                  <span key={`record-type-${index}`}>
162                    {resolveTypeName(type)}
163                    {index !== typeArguments.length - 1 ? ', ' : null}
164                  </span>
165                ))}
166                &gt;
167              </>
168            );
169          } else {
170            return (
171              <>
172                {renderWithLink(name)}
173                &lt;
174                {typeArguments.map((type, index) => (
175                  <span key={`${name}-nested-type-${index}`}>
176                    {resolveTypeName(type)}
177                    {index !== typeArguments.length - 1 ? ', ' : null}
178                  </span>
179                ))}
180                &gt;
181              </>
182            );
183          }
184        } else {
185          return renderWithLink(name);
186        }
187      } else {
188        return name;
189      }
190    } else if (elementType?.name) {
191      if (elementType.type === 'reference') {
192        return renderWithLink(elementType.name, type);
193      } else if (type === 'array') {
194        return elementType.name + '[]';
195      }
196      return elementType.name + type;
197    } else if (elementType?.declaration) {
198      if (type === 'array') {
199        const { parameters, type: paramType } = elementType.declaration.indexSignature || {};
200        if (parameters && paramType) {
201          return `{ [${listParams(parameters)}]: ${resolveTypeName(paramType)} }`;
202        }
203      }
204      return elementType.name + type;
205    } else if (type === 'union' && types?.length) {
206      return renderUnion(types);
207    } else if (elementType && elementType.type === 'union' && elementType?.types?.length) {
208      const unionTypes = elementType?.types || [];
209      return (
210        <>
211          ({renderUnion(unionTypes)}){type === 'array' && '[]'}
212        </>
213      );
214    } else if (declaration?.signatures) {
215      const baseSignature = declaration.signatures[0];
216      if (baseSignature?.parameters?.length) {
217        return (
218          <>
219            (
220            {baseSignature.parameters?.map((param, index) => (
221              <span key={`param-${index}-${param.name}`}>
222                {param.name}: {resolveTypeName(param.type)}
223                {index + 1 !== baseSignature.parameters?.length && ', '}
224              </span>
225            ))}
226            ) {'=>'} {resolveTypeName(baseSignature.type)}
227          </>
228        );
229      } else {
230        return (
231          <>
232            {'() =>'} {resolveTypeName(baseSignature.type)}
233          </>
234        );
235      }
236    } else if (type === 'reflection' && declaration?.children) {
237      return (
238        <>
239          {'{ '}
240          {declaration?.children.map((child: PropData, i) => (
241            <span key={`reflection-${name}-${i}`}>
242              {child.name + ': ' + resolveTypeName(child.type)}
243              {i + 1 !== declaration?.children?.length ? ', ' : null}
244            </span>
245          ))}
246          {' }'}
247        </>
248      );
249    } else if (type === 'tuple' && elements) {
250      return (
251        <>
252          [
253          {elements.map((elem, i) => (
254            <span key={`tuple-${name}-${i}`}>
255              {resolveTypeName(elem)}
256              {i + 1 !== elements.length ? ', ' : null}
257            </span>
258          ))}
259          ]
260        </>
261      );
262    } else if (type === 'query' && queryType) {
263      return queryType.name;
264    } else if (type === 'literal' && typeof value === 'boolean') {
265      return `${value}`;
266    } else if (type === 'literal' && value) {
267      return `'${value}'`;
268    } else if (type === 'intersection' && types) {
269      return types
270        .filter(({ name }) => !omittableTypes.includes(name ?? ''))
271        .map((value, index, array) => (
272          <span key={`intersection-${name}-${index}`}>
273            {resolveTypeName(value)}
274            {index + 1 !== array.length && ' & '}
275          </span>
276        ));
277    } else if (value === null) {
278      return 'null';
279    }
280    return 'undefined';
281  } catch (e) {
282    console.warn('Type resolve has failed!', e);
283    return 'undefined';
284  }
285};
286
287export const parseParamName = (name: string) => (name.startsWith('__') ? name.substr(2) : name);
288
289export const renderParam = ({ comment, name, type, flags }: MethodParamData): JSX.Element => (
290  <LI key={`param-${name}`}>
291    <B>
292      {parseParamName(name)}
293      {flags?.isOptional && '?'} (<InlineCode>{resolveTypeName(type)}</InlineCode>)
294    </B>
295    <CommentTextBlock comment={comment} components={mdInlineComponents} withDash />
296  </LI>
297);
298
299export const listParams = (parameters: MethodParamData[]) =>
300  parameters ? parameters?.map(param => parseParamName(param.name)).join(', ') : '';
301
302export const renderTypeOrSignatureType = (
303  type?: TypeDefinitionData,
304  signatures?: MethodSignatureData[],
305  includeParamType: boolean = false
306) => {
307  if (type) {
308    return <InlineCode key={`signature-type-${type.name}`}>{resolveTypeName(type)}</InlineCode>;
309  } else if (signatures && signatures.length) {
310    return signatures.map(({ name, type, parameters }) => (
311      <InlineCode key={`signature-type-${name}`}>
312        (
313        {parameters && includeParamType
314          ? parameters.map(param => (
315              <span key={`signature-param-${param.name}`}>
316                {param.name}
317                {param.flags?.isOptional && '?'}: {resolveTypeName(param.type)}
318              </span>
319            ))
320          : listParams(parameters)}
321        ) =&gt; {resolveTypeName(type)}
322      </InlineCode>
323    ));
324  }
325  return undefined;
326};
327
328export const renderFlags = (flags?: TypePropertyDataFlags) =>
329  flags?.isOptional ? (
330    <>
331      <br />
332      <span css={STYLES_OPTIONAL}>(optional)</span>
333    </>
334  ) : undefined;
335
336export type CommentTextBlockProps = {
337  comment?: CommentData;
338  components?: MDComponents;
339  withDash?: boolean;
340  beforeContent?: JSX.Element;
341  includePlatforms?: boolean;
342};
343
344export const parseCommentContent = (content?: string): string =>
345  content && content.length ? content.replace(/&ast;/g, '*').replace(/\t/g, '') : '';
346
347export const getCommentOrSignatureComment = (
348  comment?: CommentData,
349  signatures?: MethodSignatureData[]
350) => comment || (signatures && signatures[0]?.comment);
351
352export const getTagData = (tagName: string, comment?: CommentData) =>
353  getAllTagData(tagName, comment)?.[0];
354
355export const getAllTagData = (tagName: string, comment?: CommentData) =>
356  comment?.tags?.filter(tag => tag.tag === tagName);
357
358const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);
359
360const formatPlatformName = (name: string) => {
361  const cleanName = name.toLowerCase().replace('\n', '');
362  return cleanName.includes('ios')
363    ? cleanName.replace('ios', 'iOS')
364    : cleanName.includes('expo')
365    ? cleanName.replace('expo', 'Expo Go')
366    : capitalize(name);
367};
368
369export const getPlatformTags = (comment?: CommentData, breakLine: boolean = true) => {
370  const platforms = getAllTagData('platform', comment);
371  return platforms?.length ? (
372    <>
373      {platforms.map(platform => (
374        <div key={platform.text} css={STYLES_PLATFORM}>
375          {formatPlatformName(platform.text)} Only
376        </div>
377      ))}
378      {breakLine && <br />}
379    </>
380  ) : null;
381};
382
383export const CommentTextBlock = ({
384  comment,
385  components = mdComponents,
386  withDash,
387  beforeContent,
388  includePlatforms = true,
389}: CommentTextBlockProps) => {
390  const shortText = comment?.shortText?.trim().length ? (
391    <ReactMarkdown components={components} remarkPlugins={[remarkGfm]}>
392      {parseCommentContent(comment.shortText)}
393    </ReactMarkdown>
394  ) : null;
395  const text = comment?.text?.trim().length ? (
396    <ReactMarkdown components={components} remarkPlugins={[remarkGfm]}>
397      {parseCommentContent(comment.text)}
398    </ReactMarkdown>
399  ) : null;
400
401  const examples = getAllTagData('example', comment);
402  const exampleText = examples?.map((example, index) => (
403    <React.Fragment key={'Example-' + index}>
404      <H4>Example</H4>
405      <ReactMarkdown components={components}>{example.text}</ReactMarkdown>
406    </React.Fragment>
407  ));
408
409  const deprecation = getTagData('deprecated', comment);
410  const deprecationNote = deprecation ? (
411    <Quote key="deprecation-note">
412      {deprecation.text.trim().length ? (
413        <ReactMarkdown components={mdInlineComponents}>{deprecation.text}</ReactMarkdown>
414      ) : (
415        <B>Deprecated</B>
416      )}
417    </Quote>
418  ) : null;
419
420  const see = getTagData('see', comment);
421  const seeText = see ? (
422    <Quote>
423      <B>See: </B>
424      <ReactMarkdown components={mdInlineComponents}>{see.text}</ReactMarkdown>
425    </Quote>
426  ) : null;
427
428  return (
429    <>
430      {deprecationNote}
431      {beforeContent}
432      {withDash && (shortText || text) && ' - '}
433      {includePlatforms && getPlatformTags(comment, !withDash)}
434      {shortText}
435      {text}
436      {seeText}
437      {exampleText}
438    </>
439  );
440};
441
442export const STYLES_OPTIONAL = css`
443  color: ${theme.text.secondary};
444  font-size: 90%;
445  padding-top: 22px;
446`;
447
448export const STYLES_SECONDARY = css`
449  color: ${theme.text.secondary};
450  font-size: 90%;
451  font-weight: 600;
452`;
453
454export const STYLES_PLATFORM = css`
455  display: inline-block;
456  background-color: ${theme.background.tertiary};
457  color: ${theme.text.default};
458  font-size: 90%;
459  font-weight: 700;
460  padding: 6px 12px;
461  margin-bottom: 8px;
462  margin-right: 8px;
463  border-radius: 4px;
464`;
465