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