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