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