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