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