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 { A } from '~/ui/components/Text'; 28 29const isDev = process.env.NODE_ENV === 'development'; 30 31export enum TypeDocKind { 32 LegacyEnum = 4, 33 Enum = 8, 34 Variable = 32, 35 Function = 64, 36 Class = 128, 37 Interface = 256, 38 Property = 1024, 39 Method = 2048, 40 Parameter = 32768, 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 <Link href={href}>{children}</Link>; 70 }, 71 p: ({ children }) => (children ? <P>{children}</P> : null), 72 strong: ({ children }) => <B>{children}</B>, 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 }) => <Link href={href}>{children}</Link>, 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 return nonLinkableTypes.includes(replacedName) ? ( 160 replacedName + (type === 'array' ? '[]' : '') 161 ) : ( 162 <Link 163 href={hardcodedTypeLinks[replacedName] || `#${replacedName.toLowerCase()}`} 164 key={`type-link-${replacedName}`}> 165 {replacedName} 166 {type === 'array' && '[]'} 167 </Link> 168 ); 169}; 170 171const renderUnion = (types: TypeDefinitionData[]) => 172 types 173 .map(type => resolveTypeName(type)) 174 .map((valueToRender, index) => ( 175 <span key={`union-type-${index}`}> 176 {valueToRender} 177 {index + 1 !== types.length && ' | '} 178 </span> 179 )); 180 181export const resolveTypeName = ( 182 typeDefinition: TypeDefinitionData 183): string | JSX.Element | (string | JSX.Element)[] => { 184 if (!typeDefinition) { 185 return 'undefined'; 186 } 187 188 const { 189 elements, 190 elementType, 191 name, 192 type, 193 types, 194 typeArguments, 195 declaration, 196 value, 197 queryType, 198 operator, 199 objectType, 200 indexType, 201 } = typeDefinition; 202 203 try { 204 if (name) { 205 if (type === 'reference') { 206 if (typeArguments) { 207 if (name === 'Record' || name === 'React.ComponentProps') { 208 return ( 209 <> 210 {name}< 211 {typeArguments.map((type, index) => ( 212 <span key={`record-type-${index}`}> 213 {resolveTypeName(type)} 214 {index !== typeArguments.length - 1 ? ', ' : null} 215 </span> 216 ))} 217 > 218 </> 219 ); 220 } else { 221 return ( 222 <> 223 {renderWithLink(name)} 224 < 225 {typeArguments.map((type, index) => ( 226 <span key={`${name}-nested-type-${index}`}> 227 {resolveTypeName(type)} 228 {index !== typeArguments.length - 1 ? ', ' : null} 229 </span> 230 ))} 231 > 232 </> 233 ); 234 } 235 } else { 236 return renderWithLink(name); 237 } 238 } else { 239 return name; 240 } 241 } else if (elementType?.name) { 242 if (elementType.type === 'reference') { 243 return renderWithLink(elementType.name, type); 244 } else if (type === 'array') { 245 return elementType.name + '[]'; 246 } 247 return elementType.name + type; 248 } else if (elementType?.declaration) { 249 if (type === 'array') { 250 const { parameters, type: paramType } = elementType.declaration.indexSignature || {}; 251 if (parameters && paramType) { 252 return `{ [${listParams(parameters)}]: ${resolveTypeName(paramType)} }`; 253 } 254 } 255 return elementType.name + type; 256 } else if (type === 'union' && types?.length) { 257 return renderUnion(types); 258 } else if (elementType && elementType.type === 'union' && elementType?.types?.length) { 259 const unionTypes = elementType?.types || []; 260 return ( 261 <> 262 ({renderUnion(unionTypes)}){type === 'array' && '[]'} 263 </> 264 ); 265 } else if (declaration?.signatures) { 266 const baseSignature = declaration.signatures[0]; 267 if (baseSignature?.parameters?.length) { 268 return ( 269 <> 270 ( 271 {baseSignature.parameters?.map((param, index) => ( 272 <span key={`param-${index}-${param.name}`}> 273 {param.name}: {resolveTypeName(param.type)} 274 {index + 1 !== baseSignature.parameters?.length && ', '} 275 </span> 276 ))} 277 ) {'=>'} {resolveTypeName(baseSignature.type)} 278 </> 279 ); 280 } else { 281 return ( 282 <> 283 {'() =>'} {resolveTypeName(baseSignature.type)} 284 </> 285 ); 286 } 287 } else if (type === 'reflection' && declaration?.children) { 288 return ( 289 <> 290 {'{\n'} 291 {declaration?.children.map((child: PropData, i) => ( 292 <span key={`reflection-${name}-${i}`}> 293 {' '} 294 {child.name + ': '} 295 {resolveTypeName(child.type)} 296 {i + 1 !== declaration?.children?.length ? ', ' : null} 297 {'\n'} 298 </span> 299 ))} 300 {'}'} 301 </> 302 ); 303 } else if (type === 'tuple' && elements) { 304 return ( 305 <> 306 [ 307 {elements.map((elem, i) => ( 308 <span key={`tuple-${name}-${i}`}> 309 {resolveTypeName(elem)} 310 {i + 1 !== elements.length ? ', ' : null} 311 </span> 312 ))} 313 ] 314 </> 315 ); 316 } else if (type === 'query' && queryType) { 317 return queryType.name; 318 } else if (type === 'literal' && typeof value === 'boolean') { 319 return `${value}`; 320 } else if (type === 'literal' && (value || (typeof value === 'number' && value === 0))) { 321 return `'${value}'`; 322 } else if (type === 'intersection' && types) { 323 return types 324 .filter(({ name }) => !omittableTypes.includes(name ?? '')) 325 .map((value, index, array) => ( 326 <span key={`intersection-${name}-${index}`}> 327 {resolveTypeName(value)} 328 {index + 1 !== array.length && ' & '} 329 </span> 330 )); 331 } else if (type === 'indexedAccess') { 332 return `${objectType?.name}['${indexType?.value}']`; 333 } else if (type === 'typeOperator') { 334 return operator || 'undefined'; 335 } else if (value === null) { 336 return 'null'; 337 } 338 return 'undefined'; 339 } catch (e) { 340 console.warn('Type resolve has failed!', e); 341 return 'undefined'; 342 } 343}; 344 345export const parseParamName = (name: string) => (name.startsWith('__') ? name.substr(2) : name); 346 347export const renderParamRow = ({ 348 comment, 349 name, 350 type, 351 flags, 352 defaultValue, 353}: MethodParamData): JSX.Element => { 354 const initValue = parseCommentContent(defaultValue || getTagData('default', comment)?.text); 355 return ( 356 <Row key={`param-${name}`}> 357 <Cell> 358 <B>{parseParamName(name)}</B> 359 {renderFlags(flags, initValue)} 360 </Cell> 361 <Cell> 362 <APIDataType typeDefinition={type} /> 363 </Cell> 364 <Cell> 365 <CommentTextBlock 366 comment={comment} 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 getMethodName = ( 490 method: MethodDefinitionData, 491 apiName?: string, 492 name?: string, 493 parameters?: MethodParamData[] 494) => { 495 const isProperty = method.kind === TypeDocKind.Property && !parameters?.length; 496 const methodName = ((apiName && `${apiName}.`) ?? '') + (method.name || name); 497 if (!isProperty) { 498 return `${methodName}(${parameters ? listParams(parameters) : ''})`; 499 } 500 501 return methodName; 502}; 503 504export const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1); 505 506export const CommentTextBlock = ({ 507 comment, 508 components = mdComponents, 509 withDash, 510 beforeContent, 511 afterContent, 512 includePlatforms = true, 513 emptyCommentFallback, 514}: CommentTextBlockProps) => { 515 const shortText = comment?.shortText?.trim().length ? ( 516 <ReactMarkdown components={components} remarkPlugins={[remarkGfm]}> 517 {parseCommentContent(comment.shortText)} 518 </ReactMarkdown> 519 ) : null; 520 const text = comment?.text?.trim().length ? ( 521 <ReactMarkdown components={components} remarkPlugins={[remarkGfm]}> 522 {parseCommentContent(comment.text)} 523 </ReactMarkdown> 524 ) : null; 525 526 if (emptyCommentFallback && (!comment || (!shortText && !text))) { 527 return <>{emptyCommentFallback}</>; 528 } 529 530 const examples = getAllTagData('example', comment); 531 const exampleText = examples?.map((example, index) => ( 532 <React.Fragment key={'example-' + index}> 533 {components !== mdComponents ? ( 534 <div css={STYLES_EXAMPLE_IN_TABLE}> 535 <B>Example</B> 536 </div> 537 ) : ( 538 <div css={STYLES_NESTED_SECTION_HEADER}> 539 <H4>Example</H4> 540 </div> 541 )} 542 <ReactMarkdown components={components}>{example.text}</ReactMarkdown> 543 </React.Fragment> 544 )); 545 546 const see = getTagData('see', comment); 547 const seeText = see && ( 548 <Callout> 549 <B>See: </B> 550 <ReactMarkdown components={mdInlineComponents}>{see.text}</ReactMarkdown> 551 </Callout> 552 ); 553 554 const hasPlatforms = (getAllTagData('platform', comment)?.length || 0) > 0; 555 556 return ( 557 <> 558 {!withDash && includePlatforms && hasPlatforms && ( 559 <APISectionPlatformTags comment={comment} prefix="Only for:" /> 560 )} 561 {beforeContent} 562 {withDash && (shortText || text) && ' - '} 563 {withDash && includePlatforms && <APISectionPlatformTags comment={comment} />} 564 {shortText} 565 {text} 566 {afterContent} 567 {seeText} 568 {exampleText} 569 </> 570 ); 571}; 572 573export const getComponentName = (name?: string, children: PropData[] = []) => { 574 if (name && name !== 'default') return name; 575 const ctor = children.filter((child: PropData) => child.name === 'constructor')[0]; 576 return ctor?.signatures?.[0]?.type?.name ?? 'default'; 577}; 578 579export const STYLES_APIBOX = css({ 580 borderRadius: borderRadius.medium, 581 borderWidth: 1, 582 borderStyle: 'solid', 583 borderColor: theme.border.default, 584 padding: `${spacing[5]}px ${spacing[5]}px 0`, 585 boxShadow: shadows.micro, 586 marginBottom: spacing[6], 587 overflowX: 'hidden', 588 589 h3: { 590 marginBottom: spacing[2], 591 }, 592 593 'h3, h4': { 594 marginTop: 0, 595 }, 596 597 th: { 598 color: theme.text.secondary, 599 padding: `${spacing[3]}px ${spacing[4]}px`, 600 }, 601 602 [`.css-${tableWrapperStyle.name}`]: { 603 boxShadow: 'none', 604 }, 605 606 [`@media screen and (max-width: ${breakpoints.medium + 124}px)`]: { 607 padding: `0 ${spacing[4]}px`, 608 }, 609}); 610 611export const STYLES_APIBOX_NESTED = css({ 612 boxShadow: 'none', 613 614 h4: { 615 marginTop: 0, 616 }, 617}); 618 619export const STYLES_NESTED_SECTION_HEADER = css({ 620 display: 'flex', 621 borderTop: `1px solid ${theme.border.default}`, 622 borderBottom: `1px solid ${theme.border.default}`, 623 margin: `${spacing[4]}px -${spacing[5]}px ${spacing[4]}px`, 624 padding: `${spacing[2.5]}px ${spacing[5]}px`, 625 backgroundColor: theme.background.secondary, 626 627 h4: { 628 ...typography.fontSizes[16], 629 fontFamily: typography.fontFaces.medium, 630 marginBottom: 0, 631 marginTop: 0, 632 color: theme.text.secondary, 633 }, 634}); 635 636export const STYLES_NOT_EXPOSED_HEADER = css({ 637 marginBottom: spacing[1], 638 display: 'inline-block', 639 640 code: { 641 marginBottom: 0, 642 }, 643}); 644 645export const STYLES_OPTIONAL = css({ 646 color: theme.text.secondary, 647 fontSize: '90%', 648 paddingTop: 22, 649}); 650 651export const STYLES_SECONDARY = css({ 652 color: theme.text.secondary, 653 fontSize: '90%', 654 fontWeight: 600, 655}); 656 657const defaultValueContainerStyle = css({ 658 marginTop: spacing[2], 659 marginBottom: spacing[2], 660 661 '&:last-child': { 662 marginBottom: 0, 663 }, 664}); 665 666const STYLES_EXAMPLE_IN_TABLE = css({ 667 margin: `${spacing[2]}px 0`, 668}); 669