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