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