import { css } from '@emotion/react'; import { theme } from '@expo/styleguide'; import React from 'react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { Code, InlineCode } from '~/components/base/code'; import { H4 } from '~/components/base/headings'; import Link from '~/components/base/link'; import { LI, UL } from '~/components/base/list'; import { B, P, Quote } from '~/components/base/paragraph'; import { CommentData, MethodParamData, MethodSignatureData, PropData, TypeDefinitionData, TypePropertyDataFlags, } from '~/components/plugins/api/APIDataTypes'; const isDev = process.env.NODE_ENV === 'development'; export enum TypeDocKind { LegacyEnum = 4, Enum = 8, Variable = 32, Function = 64, Class = 128, Interface = 256, Property = 1024, Method = 2048, TypeAlias = 4194304, } export type MDComponents = React.ComponentProps['components']; const getInvalidLinkMessage = (href: string) => `Using "../" when linking other packages in doc comments produce a broken link! Please use "./" instead. Problematic link:\n\t${href}`; export const mdComponents: MDComponents = { blockquote: ({ children }) => ( {/* @ts-ignore - current implementation produce type issues, this would be fixed in docs redesign */} {children.map(child => (child?.props?.node?.tagName === 'p' ? child?.props.children : child))} ), code: ({ children, className }) => className ? {children} : {children}, h1: ({ children }) =>

{children}

, ul: ({ children }) => , li: ({ children }) =>
  • {children}
  • , a: ({ href, children }) => { if ( href?.startsWith('../') && !href?.startsWith('../..') && !href?.startsWith('../react-native') ) { if (isDev) { throw new Error(getInvalidLinkMessage(href)); } else { console.warn(getInvalidLinkMessage(href)); } } return {children}; }, p: ({ children }) => (children ?

    {children}

    : null), strong: ({ children }) => {children}, span: ({ children }) => (children ? {children} : null), }; export const mdInlineComponents: MDComponents = { ...mdComponents, p: ({ children }) => (children ? {children} : null), }; const nonLinkableTypes = [ 'ColorValue', 'Component', 'E', 'EventSubscription', 'File', 'FileList', 'Manifest', 'NativeSyntheticEvent', 'ParsedQs', 'ServiceActionResult', 'T', 'TaskOptions', 'Uint8Array', // React & React Native 'React.FC', 'ForwardRefExoticComponent', 'StyleProp', // Cross-package permissions management 'RequestPermissionMethod', 'GetPermissionMethod', 'Options', 'PermissionHookBehavior', ]; /** * List of type names that should not be visible in the docs. */ const omittableTypes = [ // Internal React type that adds `ref` prop to the component 'RefAttributes', ]; /** * Map of internal names/type names that should be replaced with something more developer-friendly. */ const replaceableTypes: Partial> = { ForwardRefExoticComponent: 'Component', }; const hardcodedTypeLinks: Record = { AVPlaybackSource: '/versions/latest/sdk/av/#playback-api', AVPlaybackStatus: '/versions/latest/sdk/av/#playback-status', AVPlaybackStatusToSet: '/versions/latest/sdk/av/#default-initial--avplaybackstatustoset', Date: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date', Element: 'https://www.typescriptlang.org/docs/handbook/jsx.html#function-component', Error: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error', ExpoConfig: 'https://github.com/expo/expo-cli/blob/main/packages/config-types/src/ExpoConfig.ts', MessageEvent: 'https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent', Omit: 'https://www.typescriptlang.org/docs/handbook/utility-types.html#omittype-keys', Pick: 'https://www.typescriptlang.org/docs/handbook/utility-types.html#picktype-keys', Partial: 'https://www.typescriptlang.org/docs/handbook/utility-types.html#partialtype', Promise: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise', View: '/versions/latest/react-native/view', ViewProps: '/versions/latest/react-native/view#props', ViewStyle: '/versions/latest/react-native/view-style-props', }; const renderWithLink = (name: string, type?: string) => { const replacedName = replaceableTypes[name] ?? name; return nonLinkableTypes.includes(replacedName) ? ( replacedName + (type === 'array' ? '[]' : '') ) : ( {replacedName} {type === 'array' && '[]'} ); }; const renderUnion = (types: TypeDefinitionData[]) => types.map(resolveTypeName).map((valueToRender, index) => ( {valueToRender} {index + 1 !== types.length && ' | '} )); export const resolveTypeName = ({ elements, elementType, name, type, types, typeArguments, declaration, value, queryType, operator, objectType, indexType, }: TypeDefinitionData): string | JSX.Element | (string | JSX.Element)[] => { try { if (name) { if (type === 'reference') { if (typeArguments) { if (name === 'Record' || name === 'React.ComponentProps') { return ( <> {name}< {typeArguments.map((type, index) => ( {resolveTypeName(type)} {index !== typeArguments.length - 1 ? ', ' : null} ))} > ); } else { return ( <> {renderWithLink(name)} < {typeArguments.map((type, index) => ( {resolveTypeName(type)} {index !== typeArguments.length - 1 ? ', ' : null} ))} > ); } } else { return renderWithLink(name); } } else { return name; } } else if (elementType?.name) { if (elementType.type === 'reference') { return renderWithLink(elementType.name, type); } else if (type === 'array') { return elementType.name + '[]'; } return elementType.name + type; } else if (elementType?.declaration) { if (type === 'array') { const { parameters, type: paramType } = elementType.declaration.indexSignature || {}; if (parameters && paramType) { return `{ [${listParams(parameters)}]: ${resolveTypeName(paramType)} }`; } } return elementType.name + type; } else if (type === 'union' && types?.length) { return renderUnion(types); } else if (elementType && elementType.type === 'union' && elementType?.types?.length) { const unionTypes = elementType?.types || []; return ( <> ({renderUnion(unionTypes)}){type === 'array' && '[]'} ); } else if (declaration?.signatures) { const baseSignature = declaration.signatures[0]; if (baseSignature?.parameters?.length) { return ( <> ( {baseSignature.parameters?.map((param, index) => ( {param.name}: {resolveTypeName(param.type)} {index + 1 !== baseSignature.parameters?.length && ', '} ))} ) {'=>'} {resolveTypeName(baseSignature.type)} ); } else { return ( <> {'() =>'} {resolveTypeName(baseSignature.type)} ); } } else if (type === 'reflection' && declaration?.children) { return ( <> {'{ '} {declaration?.children.map((child: PropData, i) => ( {child.name + ': ' + resolveTypeName(child.type)} {i + 1 !== declaration?.children?.length ? ', ' : null} ))} {' }'} ); } else if (type === 'tuple' && elements) { return ( <> [ {elements.map((elem, i) => ( {resolveTypeName(elem)} {i + 1 !== elements.length ? ', ' : null} ))} ] ); } else if (type === 'query' && queryType) { return queryType.name; } else if (type === 'literal' && typeof value === 'boolean') { return `${value}`; } else if (type === 'literal' && value) { return `'${value}'`; } else if (type === 'intersection' && types) { return types .filter(({ name }) => !omittableTypes.includes(name ?? '')) .map((value, index, array) => ( {resolveTypeName(value)} {index + 1 !== array.length && ' & '} )); } else if (type === 'indexedAccess') { return `${objectType?.name}['${indexType?.value}']`; } else if (type === 'typeOperator') { return operator || 'undefined'; } else if (value === null) { return 'null'; } return 'undefined'; } catch (e) { console.warn('Type resolve has failed!', e); return 'undefined'; } }; export const parseParamName = (name: string) => (name.startsWith('__') ? name.substr(2) : name); export const renderParam = ({ comment, name, type, flags }: MethodParamData): JSX.Element => (
  • {parseParamName(name)} {flags?.isOptional && '?'} ({resolveTypeName(type)})
  • ); export const listParams = (parameters: MethodParamData[]) => parameters ? parameters?.map(param => parseParamName(param.name)).join(', ') : ''; export const renderTypeOrSignatureType = ( type?: TypeDefinitionData, signatures?: MethodSignatureData[], includeParamType: boolean = false ) => { if (type) { return {resolveTypeName(type)}; } else if (signatures && signatures.length) { return signatures.map(({ name, type, parameters }) => ( ( {parameters && includeParamType ? parameters.map(param => ( {param.name} {param.flags?.isOptional && '?'}: {resolveTypeName(param.type)} )) : listParams(parameters)} ) => {resolveTypeName(type)} )); } return undefined; }; export const renderFlags = (flags?: TypePropertyDataFlags) => flags?.isOptional ? ( <>
    (optional) ) : undefined; export type CommentTextBlockProps = { comment?: CommentData; components?: MDComponents; withDash?: boolean; beforeContent?: JSX.Element | null; afterContent?: JSX.Element | null; includePlatforms?: boolean; }; export const parseCommentContent = (content?: string): string => content && content.length ? content.replace(/*/g, '*').replace(/\t/g, '') : ''; export const getCommentOrSignatureComment = ( comment?: CommentData, signatures?: MethodSignatureData[] ) => comment || (signatures && signatures[0]?.comment); export const getTagData = (tagName: string, comment?: CommentData) => getAllTagData(tagName, comment)?.[0]; export const getAllTagData = (tagName: string, comment?: CommentData) => comment?.tags?.filter(tag => tag.tag === tagName); const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1); const formatPlatformName = (name: string) => { const cleanName = name.toLowerCase().replace('\n', ''); return cleanName.includes('ios') ? cleanName.replace('ios', 'iOS') : cleanName.includes('expo') ? cleanName.replace('expo', 'Expo Go') : capitalize(name); }; export const getPlatformTags = (comment?: CommentData, breakLine: boolean = true) => { const platforms = getAllTagData('platform', comment); return platforms?.length ? ( <> {platforms.map(platform => (
    {formatPlatformName(platform.text)} Only
    ))} {breakLine &&
    } ) : null; }; export const CommentTextBlock = ({ comment, components = mdComponents, withDash, beforeContent, afterContent, includePlatforms = true, }: CommentTextBlockProps) => { const shortText = comment?.shortText?.trim().length ? ( {parseCommentContent(comment.shortText)} ) : null; const text = comment?.text?.trim().length ? ( {parseCommentContent(comment.text)} ) : null; const examples = getAllTagData('example', comment); const exampleText = examples?.map((example, index) => ( {components !== mdComponents ? (
    Example
    ) : (

    Example

    )} {example.text}
    )); const deprecation = getTagData('deprecated', comment); const deprecationNote = deprecation ? ( {deprecation.text.trim().length ? ( {`**Deprecated.** ${deprecation.text}`} ) : ( Deprecated )} ) : null; const see = getTagData('see', comment); const seeText = see ? ( See: {see.text} ) : null; return ( <> {deprecationNote} {beforeContent} {withDash && (shortText || text) && ' - '} {includePlatforms && getPlatformTags(comment, !withDash)} {shortText} {text} {afterContent} {seeText} {exampleText} ); }; export const getComponentName = (name?: string, children: PropData[] = []) => { if (name && name !== 'default') return name; const ctor = children.filter((child: PropData) => child.name === 'constructor')[0]; return ctor?.signatures?.[0]?.type?.name ?? 'default'; }; export const STYLES_OPTIONAL = css` color: ${theme.text.secondary}; font-size: 90%; padding-top: 22px; `; export const STYLES_SECONDARY = css` color: ${theme.text.secondary}; font-size: 90%; font-weight: 600; `; export const STYLES_PLATFORM = css` & { display: inline-block; background-color: ${theme.background.tertiary}; color: ${theme.text.default}; font-size: 90%; font-weight: 700; padding: 6px 12px; margin-bottom: 8px; margin-right: 8px; border-radius: 4px; } table & { margin-bottom: 1rem; } `; const STYLES_EXAMPLE_IN_TABLE = css` margin: 8px 0; `;