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