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