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