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