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