import { css } from '@emotion/react'; import { shadows, theme, typography } from '@expo/styleguide'; import { borderRadius, breakpoints, spacing } from '@expo/styleguide-base'; import type { ComponentProps, ComponentType } from 'react'; import { Fragment } from 'react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { APIDataType } from './APIDataType'; import { HeadingType } from '~/common/headingManager'; import { Code as PrismCodeBlock } from '~/components/base/code'; import { CommentContentData, CommentData, MethodDefinitionData, MethodParamData, MethodSignatureData, PropData, TypeDefinitionData, TypePropertyDataFlags, TypeSignaturesData, } from '~/components/plugins/api/APIDataTypes'; import { APISectionPlatformTags } from '~/components/plugins/api/APISectionPlatformTags'; import { Callout } from '~/ui/components/Callout'; import { Cell, HeaderCell, Row, Table, TableHead } from '~/ui/components/Table'; import { tableWrapperStyle } from '~/ui/components/Table/Table'; import { Tag } from '~/ui/components/Tag'; import { A, BOLD, CODE, H4, LI, OL, P, RawH3, RawH4, UL, createPermalinkedComponent, DEMI, CALLOUT, createTextComponent, } from '~/ui/components/Text'; import { TextElement } from '~/ui/components/Text/types'; const isDev = process.env.NODE_ENV === 'development'; export enum TypeDocKind { Namespace = 4, Enum = 8, Variable = 32, Function = 64, Class = 128, Interface = 256, Property = 1024, Method = 2048, Parameter = 32768, Accessor = 262144, TypeAlias = 4194304, } export type MDComponents = 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 }) => {children}, code: ({ children, className }) => className ? ( {children} ) : ( {children} ), h1: ({ children }) =>

{children}

, ul: ({ children }) => , ol: ({ children }) =>
    {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), table: ({ children }) => {children}
    , thead: ({ children }) => {children}, tr: ({ children }) => {children}, th: ({ children }) => {children}, td: ({ children }) => {children}, }; export const mdComponentsNoValidation: MDComponents = { ...mdComponents, a: ({ href, children }) => {children}, }; const nonLinkableTypes = [ 'ColorValue', 'Component', 'ComponentClass', 'PureComponent', 'E', 'EventSubscription', 'Listener', 'NativeSyntheticEvent', 'ParsedQs', 'ServiceActionResult', 'T', 'TaskOptions', 'Uint8Array', // React & React Native 'React.FC', 'ForwardRefExoticComponent', 'StyleProp', 'HTMLInputElement', // 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', LocationAccuracy: 'Accuracy', LocationGeofencingRegionState: 'GeofencingRegionState', LocationActivityType: 'ActivityType', }; const hardcodedTypeLinks: Record = { AVPlaybackSource: '/versions/latest/sdk/av/#avplaybacksource', AVPlaybackStatus: '/versions/latest/sdk/av/#avplaybackstatus', AVPlaybackStatusToSet: '/versions/latest/sdk/av/#avplaybackstatustoset', Blob: 'https://developer.mozilla.org/en-US/docs/Web/API/Blob', Date: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date', DeviceSensor: '/versions/latest/sdk/sensors', 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/blob/main/packages/%40expo/config-types/src/ExpoConfig.ts', File: 'https://developer.mozilla.org/en-US/docs/Web/API/File', FileList: 'https://developer.mozilla.org/en-US/docs/Web/API/FileList', Manifest: '/versions/latest/sdk/constants/#manifest', MediaTrackSettings: 'https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackSettings', 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', SyntheticEvent: 'https://react.dev/reference/react-dom/components/common#react-event-object', View: 'https://reactnative.dev/docs/view', ViewProps: 'https://reactnative.dev/docs/view#props', ViewStyle: 'https://reactnative.dev/docs/view-style-props', WebGL2RenderingContext: 'https://developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext', WebGLFramebuffer: 'https://developer.mozilla.org/en-US/docs/Web/API/WebGLFramebuffer', }; const renderWithLink = (name: string, type?: string) => { const replacedName = replaceableTypes[name] ?? name; if (name.includes('.')) return name; return nonLinkableTypes.includes(replacedName) ? ( replacedName + (type === 'array' ? '[]' : '') ) : ( {replacedName} {type === 'array' && '[]'} ); }; const renderUnion = (types: TypeDefinitionData[]) => types .map(type => resolveTypeName(type)) .map((valueToRender, index) => ( {valueToRender} {index + 1 !== types.length && ' | '} )); export const resolveTypeName = ( typeDefinition: TypeDefinitionData ): string | JSX.Element | (string | JSX.Element)[] => { if (!typeDefinition) { return 'undefined'; } const { elements, elementType, name, type, types, typeArguments, declaration, value, queryType, operator, objectType, indexType, } = typeDefinition; 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 ( <> {'{\n'} {declaration?.children.map((child: PropData, i) => ( {' '} {child.name + ': '} {resolveTypeName(child.type)} {i + 1 !== declaration?.children?.length ? ', ' : null} {'\n'} ))} {'}'} ); } 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 || (typeof value === 'number' && value === 0))) { 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 (type === 'intrinsic') { return name || '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 renderParamRow = ({ comment, name, type, flags, defaultValue, }: MethodParamData): JSX.Element => { const defaultData = getTagData('default', comment); const initValue = parseCommentContent( defaultValue || (defaultData ? getCommentContent(defaultData.content) : '') ); return ( {parseParamName(name)} {renderFlags(flags, initValue)} ); }; export const ParamsTableHeadRow = () => ( Name Type Description ); const InheritPermalink = createPermalinkedComponent( createTextComponent( TextElement.SPAN, css({ fontSize: 'inherit', fontWeight: 'inherit', color: 'inherit' }) ), { baseNestingLevel: 2 } ); export const BoxSectionHeader = ({ text, exposeInSidebar, }: { text: string; exposeInSidebar?: boolean; }) => { const TextWrapper = exposeInSidebar ? InheritPermalink : Fragment; return ( {text} ); }; export const renderParams = (parameters: MethodParamData[]) => ( {parameters?.map(renderParamRow)}
    ); export const listParams = (parameters: MethodParamData[]) => parameters ? parameters?.map(param => parseParamName(param.name)).join(', ') : ''; export const renderDefaultValue = (defaultValue?: string) => defaultValue && defaultValue !== '...' ? (
    Default: {defaultValue}
    ) : undefined; export const renderTypeOrSignatureType = ( type?: TypeDefinitionData, signatures?: MethodSignatureData[] | TypeSignaturesData[], allowBlock: boolean = false ) => { if (signatures && signatures.length) { return ( ( {signatures?.map(({ parameters }) => parameters?.map(param => ( {param.name} {param.flags?.isOptional && '?'}: {resolveTypeName(param.type)} )) )} ) => {signatures[0].type ? resolveTypeName(signatures[0].type) : 'void'} ); } else if (type) { if (allowBlock) { return ; } return {resolveTypeName(type)}; } return undefined; }; export const renderFlags = (flags?: TypePropertyDataFlags, defaultValue?: string) => (flags?.isOptional || defaultValue) && ( <>
    (optional) ); export const renderIndexSignature = (kind: TypeDocKind) => kind === TypeDocKind.Parameter && ( <>
    (index signature) ); export type CommentTextBlockProps = { comment?: CommentData; components?: MDComponents; beforeContent?: JSX.Element; afterContent?: JSX.Element; includePlatforms?: boolean; inlineHeaders?: boolean; emptyCommentFallback?: string; }; export const parseCommentContent = (content?: string): string => content && content.length ? content.replace(/*/g, '*').replace(/\t/g, '') : ''; export const getCommentOrSignatureComment = ( comment?: CommentData, signatures?: MethodSignatureData[] | TypeSignaturesData[] ) => comment || (signatures && signatures[0]?.comment); export const getTagData = (tagName: string, comment?: CommentData) => getAllTagData(tagName, comment)?.[0]; export const getAllTagData = (tagName: string, comment?: CommentData) => comment?.blockTags?.filter(tag => tag.tag.substring(1) === tagName); export const getTagNamesList = (comment?: CommentData) => comment && [ ...(getAllTagData('platform', comment)?.map(platformData => getCommentContent(platformData.content) ) || []), ...(getTagData('deprecated', comment) ? ['deprecated'] : []), ...(getTagData('experimental', comment) ? ['experimental'] : []), ]; export const getMethodName = ( method: MethodDefinitionData, apiName?: string, name?: string, parameters?: MethodParamData[] ) => { const isProperty = method.kind === TypeDocKind.Property && !parameters?.length; const methodName = ((apiName && `${apiName}.`) ?? '') + (method.name || name); if (!isProperty) { return `${methodName}(${parameters ? listParams(parameters) : ''})`; } return methodName; }; export const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1); const PARAM_TAGS_REGEX = /@tag-\S*/g; const getParamTags = (shortText?: string) => { if (!shortText || !shortText.includes('@tag-')) { return undefined; } return Array.from(shortText.matchAll(PARAM_TAGS_REGEX), match => match[0]); }; export const getCommentContent = (content: CommentContentData[]) => { return content .map(entry => entry.text) .join('') .trim(); }; export const CommentTextBlock = ({ comment, beforeContent, afterContent, includePlatforms = true, inlineHeaders = false, emptyCommentFallback, }: CommentTextBlockProps) => { const content = comment && comment.summary ? getCommentContent(comment.summary) : undefined; if (emptyCommentFallback && (!comment || !content || !content.length)) { return <>{emptyCommentFallback}; } const paramTags = content ? getParamTags(content) : undefined; const parsedContent = ( {parseCommentContent(paramTags ? content?.replaceAll(PARAM_TAGS_REGEX, '') : content)} ); const examples = getAllTagData('example', comment); const exampleText = examples?.map((example, index) => ( {inlineHeaders ? (
    Example
    ) : ( )} {getCommentContent(example.content)}
    )); const see = getTagData('see', comment); const seeText = see && ( {`**See:** ` + getCommentContent(see.content)} ); const hasPlatforms = (getAllTagData('platform', comment)?.length || 0) > 0; return ( <> {includePlatforms && hasPlatforms && ( )} {paramTags && ( <> Only for:  {paramTags.map(tag => ( ))} )} {beforeContent} {parsedContent} {afterContent} {seeText} {exampleText} ); }; const getMonospaceHeader = (element: ComponentType) => { const level = parseInt(element?.displayName?.replace(/\D/g, '') ?? '0', 10); return createPermalinkedComponent(element, { baseNestingLevel: level !== 0 ? level : undefined, sidebarType: HeadingType.InlineCode, }); }; export const H3Code = getMonospaceHeader(RawH3); export const H4Code = getMonospaceHeader(RawH4); 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_APIBOX = css({ borderRadius: borderRadius.md, borderWidth: 1, borderStyle: 'solid', borderColor: theme.border.default, padding: spacing[5], boxShadow: shadows.xs, marginBottom: spacing[6], overflowX: 'hidden', h3: { marginBottom: spacing[2.5], }, 'h2, h3, h4': { marginTop: 0, }, th: { color: theme.text.secondary, padding: `${spacing[3]}px ${spacing[4]}px`, }, li: { marginBottom: 0, }, [`.css-${tableWrapperStyle.name}`]: { boxShadow: 'none', marginBottom: 0, }, [`@media screen and (max-width: ${breakpoints.medium + 124}px)`]: { paddingInline: spacing[4], }, }); export const STYLES_APIBOX_NESTED = css({ boxShadow: 'none', marginBottom: spacing[4], padding: `${spacing[4]}px ${spacing[5]}px 0`, h4: { marginTop: 0, }, }); export const STYLES_APIBOX_WRAPPER = css({ marginBottom: spacing[4], padding: `${spacing[4]}px ${spacing[5]}px 0`, [`.css-${tableWrapperStyle.name}:last-child`]: { marginBottom: spacing[4], }, }); export const STYLE_APIBOX_NO_SPACING = css({ marginBottom: -spacing[5] }); export const STYLES_NESTED_SECTION_HEADER = css({ display: 'flex', borderTop: `1px solid ${theme.border.default}`, borderBottom: `1px solid ${theme.border.default}`, margin: `${spacing[4]}px -${spacing[5]}px ${spacing[4]}px`, padding: `${spacing[2.5]}px ${spacing[5]}px`, backgroundColor: theme.background.subtle, h4: { ...typography.fontSizes[16], fontWeight: 600, marginBottom: 0, marginTop: 0, color: theme.text.secondary, }, }); export const STYLES_NOT_EXPOSED_HEADER = css({ marginBottom: spacing[1], display: 'inline-block', code: { marginBottom: 0, }, }); export const STYLES_OPTIONAL = css({ color: theme.text.secondary, fontSize: '90%', paddingTop: 22, }); export const STYLES_SECONDARY = css({ color: theme.text.secondary, fontSize: '90%', fontWeight: 600, }); const defaultValueContainerStyle = css({ marginTop: spacing[2], marginBottom: spacing[2], '&:last-child': { marginBottom: 0, }, }); const STYLES_EXAMPLE_IN_TABLE = css({ margin: `${spacing[2]}px 0`, }); export const ELEMENT_SPACING = 'mb-4';