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: '../av/#playback-api', 118 AVPlaybackStatus: '../av/#playback-status', 119 AVPlaybackStatusToSet: '../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: '../../react-native/view', 131 ViewProps: '../../react-native/view#props', 132 ViewStyle: '../../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; 365 includePlatforms?: boolean; 366}; 367 368export const parseCommentContent = (content?: string): string => 369 content && content.length ? content.replace(/*/g, '*').replace(/\t/g, '') : ''; 370 371export const getCommentOrSignatureComment = ( 372 comment?: CommentData, 373 signatures?: MethodSignatureData[] 374) => comment || (signatures && signatures[0]?.comment); 375 376export const getTagData = (tagName: string, comment?: CommentData) => 377 getAllTagData(tagName, comment)?.[0]; 378 379export const getAllTagData = (tagName: string, comment?: CommentData) => 380 comment?.tags?.filter(tag => tag.tag === tagName); 381 382const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1); 383 384const formatPlatformName = (name: string) => { 385 const cleanName = name.toLowerCase().replace('\n', ''); 386 return cleanName.includes('ios') 387 ? cleanName.replace('ios', 'iOS') 388 : cleanName.includes('expo') 389 ? cleanName.replace('expo', 'Expo Go') 390 : capitalize(name); 391}; 392 393export const getPlatformTags = (comment?: CommentData, breakLine: boolean = true) => { 394 const platforms = getAllTagData('platform', comment); 395 return platforms?.length ? ( 396 <> 397 {platforms.map(platform => ( 398 <div key={platform.text} css={STYLES_PLATFORM}> 399 {formatPlatformName(platform.text)} Only 400 </div> 401 ))} 402 {breakLine && <br />} 403 </> 404 ) : null; 405}; 406 407export const CommentTextBlock = ({ 408 comment, 409 components = mdComponents, 410 withDash, 411 beforeContent, 412 includePlatforms = true, 413}: CommentTextBlockProps) => { 414 const shortText = comment?.shortText?.trim().length ? ( 415 <ReactMarkdown components={components} remarkPlugins={[remarkGfm]}> 416 {parseCommentContent(comment.shortText)} 417 </ReactMarkdown> 418 ) : null; 419 const text = comment?.text?.trim().length ? ( 420 <ReactMarkdown components={components} remarkPlugins={[remarkGfm]}> 421 {parseCommentContent(comment.text)} 422 </ReactMarkdown> 423 ) : null; 424 425 const examples = getAllTagData('example', comment); 426 const exampleText = examples?.map((example, index) => ( 427 <React.Fragment key={'Example-' + index}> 428 <H4>Example</H4> 429 <ReactMarkdown components={components}>{example.text}</ReactMarkdown> 430 </React.Fragment> 431 )); 432 433 const deprecation = getTagData('deprecated', comment); 434 const deprecationNote = deprecation ? ( 435 <Quote key="deprecation-note"> 436 {deprecation.text.trim().length ? ( 437 <ReactMarkdown 438 components={mdInlineComponents}>{`**Deprecated.** ${deprecation.text}`}</ReactMarkdown> 439 ) : ( 440 <B>Deprecated</B> 441 )} 442 </Quote> 443 ) : null; 444 445 const see = getTagData('see', comment); 446 const seeText = see ? ( 447 <Quote> 448 <B>See: </B> 449 <ReactMarkdown components={mdInlineComponents}>{see.text}</ReactMarkdown> 450 </Quote> 451 ) : null; 452 453 return ( 454 <> 455 {deprecationNote} 456 {beforeContent} 457 {withDash && (shortText || text) && ' - '} 458 {includePlatforms && getPlatformTags(comment, !withDash)} 459 {shortText} 460 {text} 461 {seeText} 462 {exampleText} 463 </> 464 ); 465}; 466 467export const getComponentName = (name?: string, children: PropData[] = []) => { 468 if (name && name !== 'default') return name; 469 const ctor = children.filter((child: PropData) => child.name === 'constructor')[0]; 470 return ctor?.signatures?.[0]?.type?.name ?? 'default'; 471}; 472 473export const STYLES_OPTIONAL = css` 474 color: ${theme.text.secondary}; 475 font-size: 90%; 476 padding-top: 22px; 477`; 478 479export const STYLES_SECONDARY = css` 480 color: ${theme.text.secondary}; 481 font-size: 90%; 482 font-weight: 600; 483`; 484 485export const STYLES_PLATFORM = css` 486 display: inline-block; 487 background-color: ${theme.background.tertiary}; 488 color: ${theme.text.default}; 489 font-size: 90%; 490 font-weight: 700; 491 padding: 6px 12px; 492 margin-bottom: 8px; 493 margin-right: 8px; 494 border-radius: 4px; 495`; 496