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, 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 { Cell, HeaderCell, Row, Table, TableHead } from '~/ui/components/Table'; 24import { tableWrapperStyle } from '~/ui/components/Table/Table'; 25import { A } from '~/ui/components/Text'; 26 27const isDev = process.env.NODE_ENV === 'development'; 28 29export enum TypeDocKind { 30 LegacyEnum = 4, 31 Enum = 8, 32 Variable = 32, 33 Function = 64, 34 Class = 128, 35 Interface = 256, 36 Property = 1024, 37 Method = 2048, 38 Parameter = 32768, 39 TypeAlias = 4194304, 40} 41 42export type MDComponents = React.ComponentProps<typeof ReactMarkdown>['components']; 43 44const getInvalidLinkMessage = (href: string) => 45 `Using "../" when linking other packages in doc comments produce a broken link! Please use "./" instead. Problematic link:\n\t${href}`; 46 47export const mdComponents: MDComponents = { 48 blockquote: ({ children }) => ( 49 <Quote> 50 {/* @ts-ignore - current implementation produce type issues, this would be fixed in docs redesign */} 51 {children.map(child => (child?.props?.node?.tagName === 'p' ? child?.props.children : child))} 52 </Quote> 53 ), 54 code: ({ children, className }) => 55 className ? <Code className={className}>{children}</Code> : <InlineCode>{children}</InlineCode>, 56 h1: ({ children }) => <H4>{children}</H4>, 57 ul: ({ children }) => <UL>{children}</UL>, 58 ol: ({ children }) => <OL>{children}</OL>, 59 li: ({ children }) => <LI>{children}</LI>, 60 a: ({ href, children }) => { 61 if ( 62 href?.startsWith('../') && 63 !href?.startsWith('../..') && 64 !href?.startsWith('../react-native') 65 ) { 66 if (isDev) { 67 throw new Error(getInvalidLinkMessage(href)); 68 } else { 69 console.warn(getInvalidLinkMessage(href)); 70 } 71 } 72 return <Link href={href}>{children}</Link>; 73 }, 74 p: ({ children }) => (children ? <P>{children}</P> : null), 75 strong: ({ children }) => <B>{children}</B>, 76 span: ({ children }) => (children ? <span>{children}</span> : null), 77 table: ({ children }) => <Table>{children}</Table>, 78 thead: ({ children }) => <TableHead>{children}</TableHead>, 79 tr: ({ children }) => <Row>{children}</Row>, 80 th: ({ children }) => <HeaderCell>{children}</HeaderCell>, 81 td: ({ children }) => <Cell>{children}</Cell>, 82}; 83 84export const mdInlineComponents: MDComponents = { 85 ...mdComponents, 86 p: ({ children }) => (children ? <span>{children}</span> : null), 87}; 88 89export const mdInlineComponentsNoValidation: MDComponents = { 90 ...mdInlineComponents, 91 a: ({ href, children }) => <Link href={href}>{children}</Link>, 92}; 93 94const nonLinkableTypes = [ 95 'ColorValue', 96 'Component', 97 'ComponentClass', 98 'E', 99 'EventSubscription', 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 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 return nonLinkableTypes.includes(replacedName) ? ( 161 replacedName + (type === 'array' ? '[]' : '') 162 ) : ( 163 <Link 164 href={hardcodedTypeLinks[replacedName] || `#${replacedName.toLowerCase()}`} 165 key={`type-link-${replacedName}`}> 166 {replacedName} 167 {type === 'array' && '[]'} 168 </Link> 169 ); 170}; 171 172const renderUnion = (types: TypeDefinitionData[]) => 173 types 174 .map(type => resolveTypeName(type)) 175 .map((valueToRender, index) => ( 176 <span key={`union-type-${index}`}> 177 {valueToRender} 178 {index + 1 !== types.length && ' | '} 179 </span> 180 )); 181 182export const resolveTypeName = ( 183 typeDefinition: TypeDefinitionData 184): string | JSX.Element | (string | JSX.Element)[] => { 185 if (!typeDefinition) { 186 return 'undefined'; 187 } 188 189 const { 190 elements, 191 elementType, 192 name, 193 type, 194 types, 195 typeArguments, 196 declaration, 197 value, 198 queryType, 199 operator, 200 objectType, 201 indexType, 202 } = typeDefinition; 203 204 try { 205 if (name) { 206 if (type === 'reference') { 207 if (typeArguments) { 208 if (name === 'Record' || name === 'React.ComponentProps') { 209 return ( 210 <> 211 {name}< 212 {typeArguments.map((type, index) => ( 213 <span key={`record-type-${index}`}> 214 {resolveTypeName(type)} 215 {index !== typeArguments.length - 1 ? ', ' : null} 216 </span> 217 ))} 218 > 219 </> 220 ); 221 } else { 222 return ( 223 <> 224 {renderWithLink(name)} 225 < 226 {typeArguments.map((type, index) => ( 227 <span key={`${name}-nested-type-${index}`}> 228 {resolveTypeName(type)} 229 {index !== typeArguments.length - 1 ? ', ' : null} 230 </span> 231 ))} 232 > 233 </> 234 ); 235 } 236 } else { 237 return renderWithLink(name); 238 } 239 } else { 240 return name; 241 } 242 } else if (elementType?.name) { 243 if (elementType.type === 'reference') { 244 return renderWithLink(elementType.name, type); 245 } else if (type === 'array') { 246 return elementType.name + '[]'; 247 } 248 return elementType.name + type; 249 } else if (elementType?.declaration) { 250 if (type === 'array') { 251 const { parameters, type: paramType } = elementType.declaration.indexSignature || {}; 252 if (parameters && paramType) { 253 return `{ [${listParams(parameters)}]: ${resolveTypeName(paramType)} }`; 254 } 255 } 256 return elementType.name + type; 257 } else if (type === 'union' && types?.length) { 258 return renderUnion(types); 259 } else if (elementType && elementType.type === 'union' && elementType?.types?.length) { 260 const unionTypes = elementType?.types || []; 261 return ( 262 <> 263 ({renderUnion(unionTypes)}){type === 'array' && '[]'} 264 </> 265 ); 266 } else if (declaration?.signatures) { 267 const baseSignature = declaration.signatures[0]; 268 if (baseSignature?.parameters?.length) { 269 return ( 270 <> 271 ( 272 {baseSignature.parameters?.map((param, index) => ( 273 <span key={`param-${index}-${param.name}`}> 274 {param.name}: {resolveTypeName(param.type)} 275 {index + 1 !== baseSignature.parameters?.length && ', '} 276 </span> 277 ))} 278 ) {'=>'} {resolveTypeName(baseSignature.type)} 279 </> 280 ); 281 } else { 282 return ( 283 <> 284 {'() =>'} {resolveTypeName(baseSignature.type)} 285 </> 286 ); 287 } 288 } else if (type === 'reflection' && declaration?.children) { 289 return ( 290 <> 291 {'{\n'} 292 {declaration?.children.map((child: PropData, i) => ( 293 <span key={`reflection-${name}-${i}`}> 294 {' '} 295 {child.name + ': '} 296 {resolveTypeName(child.type)} 297 {i + 1 !== declaration?.children?.length ? ', ' : null} 298 {'\n'} 299 </span> 300 ))} 301 {'}'} 302 </> 303 ); 304 } else if (type === 'tuple' && elements) { 305 return ( 306 <> 307 [ 308 {elements.map((elem, i) => ( 309 <span key={`tuple-${name}-${i}`}> 310 {resolveTypeName(elem)} 311 {i + 1 !== elements.length ? ', ' : null} 312 </span> 313 ))} 314 ] 315 </> 316 ); 317 } else if (type === 'query' && queryType) { 318 return queryType.name; 319 } else if (type === 'literal' && typeof value === 'boolean') { 320 return `${value}`; 321 } else if (type === 'literal' && value) { 322 return `'${value}'`; 323 } else if (type === 'intersection' && types) { 324 return types 325 .filter(({ name }) => !omittableTypes.includes(name ?? '')) 326 .map((value, index, array) => ( 327 <span key={`intersection-${name}-${index}`}> 328 {resolveTypeName(value)} 329 {index + 1 !== array.length && ' & '} 330 </span> 331 )); 332 } else if (type === 'indexedAccess') { 333 return `${objectType?.name}['${indexType?.value}']`; 334 } else if (type === 'typeOperator') { 335 return operator || 'undefined'; 336 } else if (value === null) { 337 return 'null'; 338 } 339 return 'undefined'; 340 } catch (e) { 341 console.warn('Type resolve has failed!', e); 342 return 'undefined'; 343 } 344}; 345 346export const parseParamName = (name: string) => (name.startsWith('__') ? name.substr(2) : name); 347 348export const renderParamRow = ({ 349 comment, 350 name, 351 type, 352 flags, 353 defaultValue, 354}: MethodParamData): JSX.Element => { 355 const initValue = parseCommentContent(defaultValue || getTagData('default', comment)?.text); 356 return ( 357 <Row key={`param-${name}`}> 358 <Cell> 359 <B>{parseParamName(name)}</B> 360 {renderFlags(flags, initValue)} 361 </Cell> 362 <Cell> 363 <APIDataType typeDefinition={type} /> 364 </Cell> 365 <Cell> 366 <CommentTextBlock 367 comment={comment} 368 components={mdInlineComponents} 369 afterContent={renderDefaultValue(initValue)} 370 emptyCommentFallback="-" 371 /> 372 </Cell> 373 </Row> 374 ); 375}; 376 377export const renderTableHeadRow = () => ( 378 <TableHead> 379 <Row> 380 <HeaderCell>Name</HeaderCell> 381 <HeaderCell>Type</HeaderCell> 382 <HeaderCell>Description</HeaderCell> 383 </Row> 384 </TableHead> 385); 386 387export const renderParams = (parameters: MethodParamData[]) => ( 388 <Table> 389 {renderTableHeadRow()} 390 <tbody>{parameters?.map(renderParamRow)}</tbody> 391 </Table> 392); 393 394export const listParams = (parameters: MethodParamData[]) => 395 parameters ? parameters?.map(param => parseParamName(param.name)).join(', ') : ''; 396 397export const renderDefaultValue = (defaultValue?: string) => 398 defaultValue && defaultValue !== '...' ? ( 399 <div css={defaultValueContainerStyle}> 400 <B>Default:</B> <InlineCode>{defaultValue}</InlineCode> 401 </div> 402 ) : undefined; 403 404export const renderTypeOrSignatureType = ( 405 type?: TypeDefinitionData, 406 signatures?: MethodSignatureData[], 407 allowBlock: boolean = false 408) => { 409 if (signatures && signatures.length) { 410 return ( 411 <InlineCode key={`signature-type-${signatures[0].name}`}> 412 ( 413 {signatures?.map(({ parameters }) => 414 parameters?.map(param => ( 415 <span key={`signature-param-${param.name}`}> 416 {param.name} 417 {param.flags?.isOptional && '?'}: {resolveTypeName(param.type)} 418 </span> 419 )) 420 )} 421 ) =>{' '} 422 {type ? ( 423 <InlineCode key={`signature-type-${type.name}`}>{resolveTypeName(type)}</InlineCode> 424 ) : ( 425 'void' 426 )} 427 </InlineCode> 428 ); 429 } else if (type) { 430 if (allowBlock) { 431 return <APIDataType typeDefinition={type} />; 432 } 433 return <InlineCode key={`signature-type-${type.name}`}>{resolveTypeName(type)}</InlineCode>; 434 } 435 return undefined; 436}; 437 438export const renderFlags = (flags?: TypePropertyDataFlags, defaultValue?: string) => 439 (flags?.isOptional || defaultValue) && ( 440 <> 441 <br /> 442 <span css={STYLES_OPTIONAL}>(optional)</span> 443 </> 444 ); 445 446export const renderIndexSignature = (kind: TypeDocKind) => 447 kind === TypeDocKind.Parameter && ( 448 <> 449 <br /> 450 <A 451 css={STYLES_OPTIONAL} 452 href="https://www.typescriptlang.org/docs/handbook/2/objects.html#index-signatures" 453 openInNewTab 454 isStyled> 455 (index signature) 456 </A> 457 </> 458 ); 459 460export type CommentTextBlockProps = { 461 comment?: CommentData; 462 components?: MDComponents; 463 withDash?: boolean; 464 beforeContent?: JSX.Element; 465 afterContent?: JSX.Element; 466 includePlatforms?: boolean; 467 emptyCommentFallback?: string; 468}; 469 470export const parseCommentContent = (content?: string): string => 471 content && content.length ? content.replace(/*/g, '*').replace(/\t/g, '') : ''; 472 473export const getCommentOrSignatureComment = ( 474 comment?: CommentData, 475 signatures?: MethodSignatureData[] 476) => comment || (signatures && signatures[0]?.comment); 477 478export const getTagData = (tagName: string, comment?: CommentData) => 479 getAllTagData(tagName, comment)?.[0]; 480 481export const getAllTagData = (tagName: string, comment?: CommentData) => 482 comment?.tags?.filter(tag => tag.tag === tagName); 483 484export const getTagNamesList = (comment?: CommentData) => 485 comment && [ 486 ...(getAllTagData('platform', comment)?.map(platformData => platformData.text) || []), 487 ...(getTagData('deprecated', comment) ? ['deprecated'] : []), 488 ...(getTagData('experimental', comment) ? ['experimental'] : []), 489 ]; 490 491export const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1); 492 493export const CommentTextBlock = ({ 494 comment, 495 components = mdComponents, 496 withDash, 497 beforeContent, 498 afterContent, 499 includePlatforms = true, 500 emptyCommentFallback, 501}: CommentTextBlockProps) => { 502 const shortText = comment?.shortText?.trim().length ? ( 503 <ReactMarkdown components={components} remarkPlugins={[remarkGfm]}> 504 {parseCommentContent(comment.shortText)} 505 </ReactMarkdown> 506 ) : null; 507 const text = comment?.text?.trim().length ? ( 508 <ReactMarkdown components={components} remarkPlugins={[remarkGfm]}> 509 {parseCommentContent(comment.text)} 510 </ReactMarkdown> 511 ) : null; 512 513 if (emptyCommentFallback && (!comment || (!shortText && !text))) { 514 return <>{emptyCommentFallback}</>; 515 } 516 517 const examples = getAllTagData('example', comment); 518 const exampleText = examples?.map((example, index) => ( 519 <React.Fragment key={'example-' + index}> 520 {components !== mdComponents ? ( 521 <div css={STYLES_EXAMPLE_IN_TABLE}> 522 <B>Example</B> 523 </div> 524 ) : ( 525 <div css={STYLES_NESTED_SECTION_HEADER}> 526 <H4>Example</H4> 527 </div> 528 )} 529 <ReactMarkdown components={components}>{example.text}</ReactMarkdown> 530 </React.Fragment> 531 )); 532 533 const see = getTagData('see', comment); 534 const seeText = see ? ( 535 <Quote> 536 <B>See: </B> 537 <ReactMarkdown components={mdInlineComponents}>{see.text}</ReactMarkdown> 538 </Quote> 539 ) : null; 540 541 const hasPlatforms = (getAllTagData('platform', comment)?.length || 0) > 0; 542 543 return ( 544 <> 545 {!withDash && includePlatforms && hasPlatforms && ( 546 <APISectionPlatformTags comment={comment} prefix="Only for:" /> 547 )} 548 {beforeContent} 549 {withDash && (shortText || text) && ' - '} 550 {withDash && includePlatforms && <APISectionPlatformTags comment={comment} />} 551 {shortText} 552 {text} 553 {afterContent} 554 {seeText} 555 {exampleText} 556 </> 557 ); 558}; 559 560export const getComponentName = (name?: string, children: PropData[] = []) => { 561 if (name && name !== 'default') return name; 562 const ctor = children.filter((child: PropData) => child.name === 'constructor')[0]; 563 return ctor?.signatures?.[0]?.type?.name ?? 'default'; 564}; 565 566export const STYLES_APIBOX = css({ 567 borderRadius: borderRadius.medium, 568 borderWidth: 1, 569 borderStyle: 'solid', 570 borderColor: theme.border.default, 571 padding: `${spacing[1]}px ${spacing[5]}px`, 572 boxShadow: shadows.micro, 573 marginBottom: spacing[6], 574 overflowX: 'hidden', 575 576 h3: { 577 marginTop: spacing[4], 578 }, 579 580 th: { 581 color: theme.text.secondary, 582 padding: `${spacing[3]}px ${spacing[4]}px`, 583 }, 584 585 [`.css-${tableWrapperStyle.name}`]: { 586 boxShadow: 'none', 587 }, 588 589 [`@media screen and (max-width: ${breakpoints.medium + 124}px)`]: { 590 padding: `0 ${spacing[4]}px`, 591 }, 592}); 593 594export const STYLES_APIBOX_NESTED = css({ 595 boxShadow: 'none', 596 597 h4: { 598 marginTop: 0, 599 }, 600}); 601 602export const STYLES_NESTED_SECTION_HEADER = css({ 603 display: 'flex', 604 borderTop: `1px solid ${theme.border.default}`, 605 borderBottom: `1px solid ${theme.border.default}`, 606 margin: `${spacing[6]}px -${spacing[5]}px ${spacing[4]}px`, 607 padding: `${spacing[2.5]}px ${spacing[5]}px`, 608 backgroundColor: theme.background.secondary, 609 610 h4: { 611 ...typography.fontSizes[16], 612 fontFamily: typography.fontFaces.medium, 613 marginBottom: 0, 614 marginTop: 0, 615 color: theme.text.secondary, 616 }, 617}); 618 619export const STYLES_NOT_EXPOSED_HEADER = css({ 620 marginTop: spacing[5], 621 marginBottom: spacing[2], 622 display: 'inline-block', 623}); 624 625export const STYLES_OPTIONAL = css({ 626 color: theme.text.secondary, 627 fontSize: '90%', 628 paddingTop: 22, 629}); 630 631export const STYLES_SECONDARY = css({ 632 color: theme.text.secondary, 633 fontSize: '90%', 634 fontWeight: 600, 635}); 636 637const defaultValueContainerStyle = css({ 638 marginTop: spacing[2], 639}); 640 641const STYLES_EXAMPLE_IN_TABLE = css({ 642 margin: `${spacing[2]}px 0`, 643}); 644