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