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