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