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/#playback-api', 128 AVPlaybackStatus: '/versions/latest/sdk/av/#playback-status', 129 AVPlaybackStatusToSet: '/versions/latest/sdk/av/#default-initial--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.map(resolveTypeName).map((valueToRender, index) => ( 162 <span key={`union-type-${index}`}> 163 {valueToRender} 164 {index + 1 !== types.length && ' | '} 165 </span> 166 )); 167 168export const resolveTypeName = ({ 169 elements, 170 elementType, 171 name, 172 type, 173 types, 174 typeArguments, 175 declaration, 176 value, 177 queryType, 178 operator, 179 objectType, 180 indexType, 181}: TypeDefinitionData): string | JSX.Element | (string | JSX.Element)[] => { 182 try { 183 if (name) { 184 if (type === 'reference') { 185 if (typeArguments) { 186 if (name === 'Record' || name === 'React.ComponentProps') { 187 return ( 188 <> 189 {name}< 190 {typeArguments.map((type, index) => ( 191 <span key={`record-type-${index}`}> 192 {resolveTypeName(type)} 193 {index !== typeArguments.length - 1 ? ', ' : null} 194 </span> 195 ))} 196 > 197 </> 198 ); 199 } else { 200 return ( 201 <> 202 {renderWithLink(name)} 203 < 204 {typeArguments.map((type, index) => ( 205 <span key={`${name}-nested-type-${index}`}> 206 {resolveTypeName(type)} 207 {index !== typeArguments.length - 1 ? ', ' : null} 208 </span> 209 ))} 210 > 211 </> 212 ); 213 } 214 } else { 215 return renderWithLink(name); 216 } 217 } else { 218 return name; 219 } 220 } else if (elementType?.name) { 221 if (elementType.type === 'reference') { 222 return renderWithLink(elementType.name, type); 223 } else if (type === 'array') { 224 return elementType.name + '[]'; 225 } 226 return elementType.name + type; 227 } else if (elementType?.declaration) { 228 if (type === 'array') { 229 const { parameters, type: paramType } = elementType.declaration.indexSignature || {}; 230 if (parameters && paramType) { 231 return `{ [${listParams(parameters)}]: ${resolveTypeName(paramType)} }`; 232 } 233 } 234 return elementType.name + type; 235 } else if (type === 'union' && types?.length) { 236 return renderUnion(types); 237 } else if (elementType && elementType.type === 'union' && elementType?.types?.length) { 238 const unionTypes = elementType?.types || []; 239 return ( 240 <> 241 ({renderUnion(unionTypes)}){type === 'array' && '[]'} 242 </> 243 ); 244 } else if (declaration?.signatures) { 245 const baseSignature = declaration.signatures[0]; 246 if (baseSignature?.parameters?.length) { 247 return ( 248 <> 249 ( 250 {baseSignature.parameters?.map((param, index) => ( 251 <span key={`param-${index}-${param.name}`}> 252 {param.name}: {resolveTypeName(param.type)} 253 {index + 1 !== baseSignature.parameters?.length && ', '} 254 </span> 255 ))} 256 ) {'=>'} {resolveTypeName(baseSignature.type)} 257 </> 258 ); 259 } else { 260 return ( 261 <> 262 {'() =>'} {resolveTypeName(baseSignature.type)} 263 </> 264 ); 265 } 266 } else if (type === 'reflection' && declaration?.children) { 267 return ( 268 <> 269 {'{ '} 270 {declaration?.children.map((child: PropData, i) => ( 271 <span key={`reflection-${name}-${i}`}> 272 {child.name + ': ' + resolveTypeName(child.type)} 273 {i + 1 !== declaration?.children?.length ? ', ' : null} 274 </span> 275 ))} 276 {' }'} 277 </> 278 ); 279 } else if (type === 'tuple' && elements) { 280 return ( 281 <> 282 [ 283 {elements.map((elem, i) => ( 284 <span key={`tuple-${name}-${i}`}> 285 {resolveTypeName(elem)} 286 {i + 1 !== elements.length ? ', ' : null} 287 </span> 288 ))} 289 ] 290 </> 291 ); 292 } else if (type === 'query' && queryType) { 293 return queryType.name; 294 } else if (type === 'literal' && typeof value === 'boolean') { 295 return `${value}`; 296 } else if (type === 'literal' && value) { 297 return `'${value}'`; 298 } else if (type === 'intersection' && types) { 299 return types 300 .filter(({ name }) => !omittableTypes.includes(name ?? '')) 301 .map((value, index, array) => ( 302 <span key={`intersection-${name}-${index}`}> 303 {resolveTypeName(value)} 304 {index + 1 !== array.length && ' & '} 305 </span> 306 )); 307 } else if (type === 'indexedAccess') { 308 return `${objectType?.name}['${indexType?.value}']`; 309 } else if (type === 'typeOperator') { 310 return operator || 'undefined'; 311 } else if (value === null) { 312 return 'null'; 313 } 314 return 'undefined'; 315 } catch (e) { 316 console.warn('Type resolve has failed!', e); 317 return 'undefined'; 318 } 319}; 320 321export const parseParamName = (name: string) => (name.startsWith('__') ? name.substr(2) : name); 322 323export const renderParamRow = ({ 324 comment, 325 name, 326 type, 327 flags, 328 defaultValue, 329}: MethodParamData): JSX.Element => { 330 const initValue = parseCommentContent(defaultValue || getTagData('default', comment)?.text); 331 return ( 332 <Row key={`param-${name}`}> 333 <Cell> 334 <B>{parseParamName(name)}</B> 335 {renderFlags(flags, initValue)} 336 </Cell> 337 <Cell> 338 <InlineCode>{resolveTypeName(type)}</InlineCode> 339 </Cell> 340 <Cell> 341 <CommentTextBlock 342 comment={comment} 343 components={mdInlineComponents} 344 afterContent={renderDefaultValue(initValue)} 345 emptyCommentFallback="-" 346 /> 347 </Cell> 348 </Row> 349 ); 350}; 351 352export const renderTableHeadRow = () => ( 353 <TableHead> 354 <Row> 355 <HeaderCell>Name</HeaderCell> 356 <HeaderCell>Type</HeaderCell> 357 <HeaderCell>Description</HeaderCell> 358 </Row> 359 </TableHead> 360); 361 362export const renderParams = (parameters: MethodParamData[]) => ( 363 <Table> 364 {renderTableHeadRow()} 365 <tbody>{parameters?.map(renderParamRow)}</tbody> 366 </Table> 367); 368 369export const listParams = (parameters: MethodParamData[]) => 370 parameters ? parameters?.map(param => parseParamName(param.name)).join(', ') : ''; 371 372export const renderDefaultValue = (defaultValue?: string) => 373 defaultValue && defaultValue !== '...' ? ( 374 <div css={defaultValueContainerStyle}> 375 <B>Default:</B> <InlineCode>{defaultValue}</InlineCode> 376 </div> 377 ) : undefined; 378 379export const renderTypeOrSignatureType = ( 380 type?: TypeDefinitionData, 381 signatures?: MethodSignatureData[] 382) => { 383 if (signatures && signatures.length) { 384 return ( 385 <InlineCode key={`signature-type-${signatures[0].name}`}> 386 ( 387 {signatures?.map(({ parameters }) => 388 parameters?.map(param => ( 389 <span key={`signature-param-${param.name}`}> 390 {param.name} 391 {param.flags?.isOptional && '?'}: {resolveTypeName(param.type)} 392 </span> 393 )) 394 )} 395 ) =>{' '} 396 {type ? ( 397 <InlineCode key={`signature-type-${type.name}`}>{resolveTypeName(type)}</InlineCode> 398 ) : ( 399 'void' 400 )} 401 </InlineCode> 402 ); 403 } else if (type) { 404 return <InlineCode key={`signature-type-${type.name}`}>{resolveTypeName(type)}</InlineCode>; 405 } 406 return undefined; 407}; 408 409export const renderFlags = (flags?: TypePropertyDataFlags, defaultValue?: string) => 410 flags?.isOptional || defaultValue ? ( 411 <> 412 <br /> 413 <span css={STYLES_OPTIONAL}>(optional)</span> 414 </> 415 ) : undefined; 416 417export type CommentTextBlockProps = { 418 comment?: CommentData; 419 components?: MDComponents; 420 withDash?: boolean; 421 beforeContent?: JSX.Element; 422 afterContent?: JSX.Element; 423 includePlatforms?: boolean; 424 emptyCommentFallback?: string; 425}; 426 427export const parseCommentContent = (content?: string): string => 428 content && content.length ? content.replace(/*/g, '*').replace(/\t/g, '') : ''; 429 430export const getCommentOrSignatureComment = ( 431 comment?: CommentData, 432 signatures?: MethodSignatureData[] 433) => comment || (signatures && signatures[0]?.comment); 434 435export const getTagData = (tagName: string, comment?: CommentData) => 436 getAllTagData(tagName, comment)?.[0]; 437 438export const getAllTagData = (tagName: string, comment?: CommentData) => 439 comment?.tags?.filter(tag => tag.tag === tagName); 440 441export const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1); 442 443export const CommentTextBlock = ({ 444 comment, 445 components = mdComponents, 446 withDash, 447 beforeContent, 448 afterContent, 449 includePlatforms = true, 450 emptyCommentFallback, 451}: CommentTextBlockProps) => { 452 const shortText = comment?.shortText?.trim().length ? ( 453 <ReactMarkdown components={components} remarkPlugins={[remarkGfm]}> 454 {parseCommentContent(comment.shortText)} 455 </ReactMarkdown> 456 ) : null; 457 const text = comment?.text?.trim().length ? ( 458 <ReactMarkdown components={components} remarkPlugins={[remarkGfm]}> 459 {parseCommentContent(comment.text)} 460 </ReactMarkdown> 461 ) : null; 462 463 if (emptyCommentFallback && (!comment || (!shortText && !text))) { 464 return <>{emptyCommentFallback}</>; 465 } 466 467 const examples = getAllTagData('example', comment); 468 const exampleText = examples?.map((example, index) => ( 469 <React.Fragment key={'example-' + index}> 470 {components !== mdComponents ? ( 471 <div css={STYLES_EXAMPLE_IN_TABLE}> 472 <B>Example</B> 473 </div> 474 ) : ( 475 <div css={STYLES_NESTED_SECTION_HEADER}> 476 <H4>Example</H4> 477 </div> 478 )} 479 <ReactMarkdown components={components}>{example.text}</ReactMarkdown> 480 </React.Fragment> 481 )); 482 483 const see = getTagData('see', comment); 484 const seeText = see ? ( 485 <Quote> 486 <B>See: </B> 487 <ReactMarkdown components={mdInlineComponents}>{see.text}</ReactMarkdown> 488 </Quote> 489 ) : null; 490 491 const hasPlatforms = (getAllTagData('platform', comment)?.length || 0) > 0; 492 493 return ( 494 <> 495 {!withDash && includePlatforms && hasPlatforms && ( 496 <PlatformTags comment={comment} prefix="Only for:" /> 497 )} 498 {beforeContent} 499 {withDash && (shortText || text) && ' - '} 500 {withDash && includePlatforms && <PlatformTags comment={comment} />} 501 {shortText} 502 {text} 503 {afterContent} 504 {seeText} 505 {exampleText} 506 </> 507 ); 508}; 509 510export const getComponentName = (name?: string, children: PropData[] = []) => { 511 if (name && name !== 'default') return name; 512 const ctor = children.filter((child: PropData) => child.name === 'constructor')[0]; 513 return ctor?.signatures?.[0]?.type?.name ?? 'default'; 514}; 515 516export const STYLES_APIBOX = css({ 517 borderRadius: borderRadius.medium, 518 borderWidth: 1, 519 borderStyle: 'solid', 520 borderColor: theme.border.default, 521 padding: `${spacing[1]}px ${spacing[5]}px`, 522 boxShadow: shadows.micro, 523 marginBottom: spacing[6], 524 overflowX: 'hidden', 525 526 h3: { 527 marginTop: spacing[4], 528 }, 529 530 th: { 531 color: theme.text.secondary, 532 padding: `${spacing[3]}px ${spacing[4]}px`, 533 }, 534 535 [`.css-${tableWrapperStyle.name}`]: { 536 boxShadow: 'none', 537 }, 538 539 [`@media screen and (max-width: ${Constants.breakpoints.mobile})`]: { 540 padding: `0 ${spacing[4]}px`, 541 }, 542}); 543 544export const STYLES_APIBOX_NESTED = css({ 545 boxShadow: 'none', 546}); 547 548export const STYLES_NESTED_SECTION_HEADER = css({ 549 display: 'flex', 550 borderTop: `1px solid ${theme.border.default}`, 551 borderBottom: `1px solid ${theme.border.default}`, 552 margin: `${spacing[6]}px -${spacing[5]}px ${spacing[4]}px`, 553 padding: `${spacing[2.5]}px ${spacing[5]}px`, 554 backgroundColor: theme.background.secondary, 555 556 h4: { 557 ...typography.fontSizes[16], 558 marginBottom: 0, 559 }, 560}); 561 562export const STYLES_NOT_EXPOSED_HEADER = css({ 563 marginTop: spacing[5], 564 marginBottom: spacing[2], 565 display: 'inline-block', 566}); 567 568export const STYLES_OPTIONAL = css({ 569 color: theme.text.secondary, 570 fontSize: '90%', 571 paddingTop: 22, 572}); 573 574export const STYLES_SECONDARY = css({ 575 color: theme.text.secondary, 576 fontSize: '90%', 577 fontWeight: 600, 578}); 579 580const defaultValueContainerStyle = css({ 581 marginTop: spacing[2], 582}); 583 584const STYLES_EXAMPLE_IN_TABLE = css({ 585 margin: `${spacing[2]}px 0`, 586}); 587