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 Date: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date', 100 Element: 'https://www.typescriptlang.org/docs/handbook/jsx.html#function-component', 101 Error: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error', 102 ExpoConfig: 'https://github.com/expo/expo-cli/blob/main/packages/config-types/src/ExpoConfig.ts', 103 MessageEvent: 'https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent', 104 Omit: 'https://www.typescriptlang.org/docs/handbook/utility-types.html#omittype-keys', 105 Pick: 'https://www.typescriptlang.org/docs/handbook/utility-types.html#picktype-keys', 106 Partial: 'https://www.typescriptlang.org/docs/handbook/utility-types.html#partialtype', 107 Promise: 108 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise', 109 View: '../../react-native/view', 110 ViewProps: '../../react-native/view#props', 111 ViewStyle: '../../react-native/view-style-props/', 112}; 113 114const renderWithLink = (name: string, type?: string) => { 115 const replacedName = replaceableTypes[name] ?? name; 116 117 return nonLinkableTypes.includes(replacedName) ? ( 118 replacedName + (type === 'array' ? '[]' : '') 119 ) : ( 120 <Link 121 href={hardcodedTypeLinks[replacedName] || `#${replacedName.toLowerCase()}`} 122 key={`type-link-${replacedName}`} 123 > 124 {replacedName} 125 {type === 'array' && '[]'} 126 </Link> 127 ); 128}; 129 130const renderUnion = (types: TypeDefinitionData[]) => 131 types.map(resolveTypeName).map((valueToRender, index) => ( 132 <span key={`union-type-${index}`}> 133 {valueToRender} 134 {index + 1 !== types.length && ' | '} 135 </span> 136 )); 137 138export const resolveTypeName = ({ 139 elements, 140 elementType, 141 name, 142 type, 143 types, 144 typeArguments, 145 declaration, 146 value, 147 queryType, 148 operator, 149}: TypeDefinitionData): string | JSX.Element | (string | JSX.Element)[] => { 150 try { 151 if (name) { 152 if (type === 'reference') { 153 if (typeArguments) { 154 if (name === 'Record' || name === 'React.ComponentProps') { 155 return ( 156 <> 157 {name}< 158 {typeArguments.map((type, index) => ( 159 <span key={`record-type-${index}`}> 160 {resolveTypeName(type)} 161 {index !== typeArguments.length - 1 ? ', ' : null} 162 </span> 163 ))} 164 > 165 </> 166 ); 167 } else { 168 return ( 169 <> 170 {renderWithLink(name)} 171 < 172 {typeArguments.map((type, index) => ( 173 <span key={`${name}-nested-type-${index}`}> 174 {resolveTypeName(type)} 175 {index !== typeArguments.length - 1 ? ', ' : null} 176 </span> 177 ))} 178 > 179 </> 180 ); 181 } 182 } else { 183 return renderWithLink(name); 184 } 185 } else { 186 return name; 187 } 188 } else if (elementType?.name) { 189 if (elementType.type === 'reference') { 190 return renderWithLink(elementType.name, type); 191 } else if (type === 'array') { 192 return elementType.name + '[]'; 193 } 194 return elementType.name + type; 195 } else if (elementType?.declaration) { 196 if (type === 'array') { 197 const { parameters, type: paramType } = elementType.declaration.indexSignature || {}; 198 if (parameters && paramType) { 199 return `{ [${listParams(parameters)}]: ${resolveTypeName(paramType)} }`; 200 } 201 } 202 return elementType.name + type; 203 } else if (type === 'union' && types?.length) { 204 return renderUnion(types); 205 } else if (elementType && elementType.type === 'union' && elementType?.types?.length) { 206 const unionTypes = elementType?.types || []; 207 return ( 208 <> 209 ({renderUnion(unionTypes)}){type === 'array' && '[]'} 210 </> 211 ); 212 } else if (declaration?.signatures) { 213 const baseSignature = declaration.signatures[0]; 214 if (baseSignature?.parameters?.length) { 215 return ( 216 <> 217 ( 218 {baseSignature.parameters?.map((param, index) => ( 219 <span key={`param-${index}-${param.name}`}> 220 {param.name}: {resolveTypeName(param.type)} 221 {index + 1 !== baseSignature.parameters?.length && ', '} 222 </span> 223 ))} 224 ) {'=>'} {resolveTypeName(baseSignature.type)} 225 </> 226 ); 227 } else { 228 return ( 229 <> 230 {'() =>'} {resolveTypeName(baseSignature.type)} 231 </> 232 ); 233 } 234 } else if (type === 'reflection' && declaration?.children) { 235 return ( 236 <> 237 {'{ '} 238 {declaration?.children.map((child: PropData, i) => ( 239 <span key={`reflection-${name}-${i}`}> 240 {child.name + ': ' + resolveTypeName(child.type)} 241 {i + 1 !== declaration?.children?.length ? ', ' : null} 242 </span> 243 ))} 244 {' }'} 245 </> 246 ); 247 } else if (type === 'tuple' && elements) { 248 return ( 249 <> 250 [ 251 {elements.map((elem, i) => ( 252 <span key={`tuple-${name}-${i}`}> 253 {resolveTypeName(elem)} 254 {i + 1 !== elements.length ? ', ' : null} 255 </span> 256 ))} 257 ] 258 </> 259 ); 260 } else if (type === 'query' && queryType) { 261 return queryType.name; 262 } else if (type === 'literal' && typeof value === 'boolean') { 263 return `${value}`; 264 } else if (type === 'literal' && value) { 265 return `'${value}'`; 266 } else if (type === 'intersection' && types) { 267 return types 268 .filter(({ name }) => !omittableTypes.includes(name ?? '')) 269 .map((value, index, array) => ( 270 <span key={`intersection-${name}-${index}`}> 271 {resolveTypeName(value)} 272 {index + 1 !== array.length && ' & '} 273 </span> 274 )); 275 } else if (type === 'typeOperator') { 276 return operator || 'undefined'; 277 } else if (value === null) { 278 return 'null'; 279 } 280 return 'undefined'; 281 } catch (e) { 282 console.warn('Type resolve has failed!', e); 283 return 'undefined'; 284 } 285}; 286 287export const parseParamName = (name: string) => (name.startsWith('__') ? name.substr(2) : name); 288 289export const renderParam = ({ comment, name, type, flags }: MethodParamData): JSX.Element => ( 290 <LI key={`param-${name}`}> 291 <B> 292 {parseParamName(name)} 293 {flags?.isOptional && '?'} (<InlineCode>{resolveTypeName(type)}</InlineCode>) 294 </B> 295 <CommentTextBlock comment={comment} components={mdInlineComponents} withDash /> 296 </LI> 297); 298 299export const listParams = (parameters: MethodParamData[]) => 300 parameters ? parameters?.map(param => parseParamName(param.name)).join(', ') : ''; 301 302export const renderTypeOrSignatureType = ( 303 type?: TypeDefinitionData, 304 signatures?: MethodSignatureData[], 305 includeParamType: boolean = false 306) => { 307 if (type) { 308 return <InlineCode key={`signature-type-${type.name}`}>{resolveTypeName(type)}</InlineCode>; 309 } else if (signatures && signatures.length) { 310 return signatures.map(({ name, type, parameters }) => ( 311 <InlineCode key={`signature-type-${name}`}> 312 ( 313 {parameters && includeParamType 314 ? parameters.map(param => ( 315 <span key={`signature-param-${param.name}`}> 316 {param.name} 317 {param.flags?.isOptional && '?'}: {resolveTypeName(param.type)} 318 </span> 319 )) 320 : listParams(parameters)} 321 ) => {resolveTypeName(type)} 322 </InlineCode> 323 )); 324 } 325 return undefined; 326}; 327 328export const renderFlags = (flags?: TypePropertyDataFlags) => 329 flags?.isOptional ? ( 330 <> 331 <br /> 332 <span css={STYLES_OPTIONAL}>(optional)</span> 333 </> 334 ) : undefined; 335 336export type CommentTextBlockProps = { 337 comment?: CommentData; 338 components?: MDComponents; 339 withDash?: boolean; 340 beforeContent?: JSX.Element; 341 includePlatforms?: boolean; 342}; 343 344export const parseCommentContent = (content?: string): string => 345 content && content.length ? content.replace(/*/g, '*').replace(/\t/g, '') : ''; 346 347export const getCommentOrSignatureComment = ( 348 comment?: CommentData, 349 signatures?: MethodSignatureData[] 350) => comment || (signatures && signatures[0]?.comment); 351 352export const getTagData = (tagName: string, comment?: CommentData) => 353 getAllTagData(tagName, comment)?.[0]; 354 355export const getAllTagData = (tagName: string, comment?: CommentData) => 356 comment?.tags?.filter(tag => tag.tag === tagName); 357 358const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1); 359 360const formatPlatformName = (name: string) => { 361 const cleanName = name.toLowerCase().replace('\n', ''); 362 return cleanName.includes('ios') 363 ? cleanName.replace('ios', 'iOS') 364 : cleanName.includes('expo') 365 ? cleanName.replace('expo', 'Expo Go') 366 : capitalize(name); 367}; 368 369export const getPlatformTags = (comment?: CommentData, breakLine: boolean = true) => { 370 const platforms = getAllTagData('platform', comment); 371 return platforms?.length ? ( 372 <> 373 {platforms.map(platform => ( 374 <div key={platform.text} css={STYLES_PLATFORM}> 375 {formatPlatformName(platform.text)} Only 376 </div> 377 ))} 378 {breakLine && <br />} 379 </> 380 ) : null; 381}; 382 383export const CommentTextBlock = ({ 384 comment, 385 components = mdComponents, 386 withDash, 387 beforeContent, 388 includePlatforms = true, 389}: CommentTextBlockProps) => { 390 const shortText = comment?.shortText?.trim().length ? ( 391 <ReactMarkdown components={components} remarkPlugins={[remarkGfm]}> 392 {parseCommentContent(comment.shortText)} 393 </ReactMarkdown> 394 ) : null; 395 const text = comment?.text?.trim().length ? ( 396 <ReactMarkdown components={components} remarkPlugins={[remarkGfm]}> 397 {parseCommentContent(comment.text)} 398 </ReactMarkdown> 399 ) : null; 400 401 const examples = getAllTagData('example', comment); 402 const exampleText = examples?.map((example, index) => ( 403 <React.Fragment key={'Example-' + index}> 404 <H4>Example</H4> 405 <ReactMarkdown components={components}>{example.text}</ReactMarkdown> 406 </React.Fragment> 407 )); 408 409 const deprecation = getTagData('deprecated', comment); 410 const deprecationNote = deprecation ? ( 411 <Quote key="deprecation-note"> 412 {deprecation.text.trim().length ? ( 413 <ReactMarkdown components={mdInlineComponents}>{deprecation.text}</ReactMarkdown> 414 ) : ( 415 <B>Deprecated</B> 416 )} 417 </Quote> 418 ) : null; 419 420 const see = getTagData('see', comment); 421 const seeText = see ? ( 422 <Quote> 423 <B>See: </B> 424 <ReactMarkdown components={mdInlineComponents}>{see.text}</ReactMarkdown> 425 </Quote> 426 ) : null; 427 428 return ( 429 <> 430 {deprecationNote} 431 {beforeContent} 432 {withDash && (shortText || text) && ' - '} 433 {includePlatforms && getPlatformTags(comment, !withDash)} 434 {shortText} 435 {text} 436 {seeText} 437 {exampleText} 438 </> 439 ); 440}; 441 442export const STYLES_OPTIONAL = css` 443 color: ${theme.text.secondary}; 444 font-size: 90%; 445 padding-top: 22px; 446`; 447 448export const STYLES_SECONDARY = css` 449 color: ${theme.text.secondary}; 450 font-size: 90%; 451 font-weight: 600; 452`; 453 454export const STYLES_PLATFORM = css` 455 display: inline-block; 456 background-color: ${theme.background.tertiary}; 457 color: ${theme.text.default}; 458 font-size: 90%; 459 font-weight: 700; 460 padding: 6px 12px; 461 margin-bottom: 8px; 462 margin-right: 8px; 463 border-radius: 4px; 464`; 465