1import { css } from '@emotion/react'; 2import { theme, typography } from '@expo/styleguide'; 3import { borderRadius, breakpoints, spacing } from '@expo/styleguide-base'; 4import ReactMarkdown from 'react-markdown'; 5 6import { HeadingType } from '~/common/headingManager'; 7import { APIBox } from '~/components/plugins/APIBox'; 8import { mdComponents } from '~/components/plugins/api/APISectionUtils'; 9import { Callout } from '~/ui/components/Callout'; 10import { Collapsible } from '~/ui/components/Collapsible'; 11import { P, CALLOUT, CODE, createPermalinkedComponent, BOLD } from '~/ui/components/Text'; 12 13type PropertyMeta = { 14 regexHuman?: string; 15 deprecated?: boolean; 16 hidden?: boolean; 17 expoKit?: string; 18 bareWorkflow?: string; 19}; 20 21export type Property = { 22 description?: string; 23 type?: string | string[]; 24 meta?: PropertyMeta; 25 pattern?: string; 26 enum?: string[]; 27 example?: any; 28 exampleString?: string; 29 host?: object; 30 properties?: Record<string, Property>; 31 items?: { 32 properties?: Record<string, Property>; 33 [key: string]: any; 34 }; 35 uniqueItems?: boolean; 36 additionalProperties?: boolean; 37}; 38 39type FormattedProperty = { 40 name: string; 41 description: string; 42 type?: string; 43 example?: string; 44 expoKit?: string; 45 bareWorkflow?: string; 46 subproperties: FormattedProperty[]; 47 parent?: string; 48}; 49 50type AppConfigSchemaProps = { 51 schema: Record<string, Property>; 52}; 53 54const Anchor = createPermalinkedComponent(P, { 55 baseNestingLevel: 3, 56 sidebarType: HeadingType.InlineCode, 57}); 58 59const PropertyName = ({ name, nestingLevel }: { name: string; nestingLevel: number }) => ( 60 <Anchor level={nestingLevel} data-testid={name} data-heading="true" css={propertyNameStyle}> 61 <CODE css={typography.fontSizes[16]}>{name}</CODE> 62 </Anchor> 63); 64 65const propertyNameStyle = css({ marginBottom: spacing[4] }); 66 67export function formatSchema(rawSchema: [string, Property][]) { 68 const formattedSchema: FormattedProperty[] = []; 69 70 rawSchema.map(property => { 71 appendProperty(formattedSchema, property); 72 }); 73 74 return formattedSchema; 75} 76 77function appendProperty(formattedSchema: FormattedProperty[], property: [string, Property]) { 78 const propertyValue = property[1]; 79 80 if (propertyValue.meta && (propertyValue.meta.deprecated || propertyValue.meta.hidden)) { 81 return; 82 } 83 84 formattedSchema.push(formatProperty(property)); 85} 86 87function formatProperty(property: [string, Property], parent?: string): FormattedProperty { 88 const propertyKey = property[0]; 89 const propertyValue = property[1]; 90 91 const subproperties: FormattedProperty[] = []; 92 93 if (propertyValue.properties) { 94 Object.entries(propertyValue.properties).forEach(subproperty => { 95 subproperties.push( 96 formatProperty(subproperty, parent ? `${parent}.${propertyKey}` : propertyKey) 97 ); 98 }); 99 } // note: sub-properties are sometimes nested within "items" 100 else if (propertyValue.items && propertyValue.items.properties) { 101 Object.entries(propertyValue.items.properties).forEach(subproperty => { 102 subproperties.push( 103 formatProperty(subproperty, parent ? `${parent}.${propertyKey}` : propertyKey) 104 ); 105 }); 106 } 107 108 return { 109 name: propertyKey, 110 description: createDescription(property), 111 type: _getType(propertyValue), 112 example: propertyValue.exampleString?.replaceAll('\n', ''), 113 expoKit: propertyValue?.meta?.expoKit, 114 bareWorkflow: propertyValue?.meta?.bareWorkflow, 115 subproperties, 116 parent, 117 }; 118} 119 120export function _getType({ enum: enm, type }: Partial<Property>) { 121 return enm ? 'enum' : type?.toString().replace(',', ' || '); 122} 123 124export function createDescription(propertyEntry: [string, Property]) { 125 const { description, meta } = propertyEntry[1]; 126 127 let propertyDescription = ``; 128 if (description) { 129 propertyDescription += description; 130 } 131 if (meta && meta.regexHuman) { 132 propertyDescription += `\n\n` + meta.regexHuman; 133 } 134 135 return propertyDescription; 136} 137 138const AppConfigSchemaPropertiesTable = ({ schema }: AppConfigSchemaProps) => { 139 const rawSchema = Object.entries(schema); 140 const formattedSchema = formatSchema(rawSchema); 141 142 return ( 143 <> 144 {formattedSchema.map((formattedProperty, index) => ( 145 <AppConfigProperty 146 {...formattedProperty} 147 key={`${formattedProperty.name}-${index}`} 148 nestingLevel={0} 149 /> 150 ))} 151 </> 152 ); 153}; 154 155const AppConfigProperty = ({ 156 name, 157 description, 158 example, 159 expoKit, 160 bareWorkflow, 161 type, 162 nestingLevel, 163 subproperties, 164 parent, 165}: FormattedProperty & { nestingLevel: number }) => ( 166 <APIBox css={boxStyle}> 167 <PropertyName name={name} nestingLevel={nestingLevel} /> 168 <CALLOUT theme="secondary" data-text="true" css={typeRow}> 169 Type: <CODE>{type || 'undefined'}</CODE> 170 {nestingLevel > 0 && ( 171 <> 172  • Path:{' '} 173 <code css={secondaryCodeLineStyle}> 174 {parent}.{name} 175 </code> 176 </> 177 )} 178 </CALLOUT> 179 <ReactMarkdown components={mdComponents}>{description}</ReactMarkdown> 180 {expoKit && ( 181 <Collapsible summary="ExpoKit"> 182 <ReactMarkdown components={mdComponents}>{expoKit}</ReactMarkdown> 183 </Collapsible> 184 )} 185 {bareWorkflow && ( 186 <Collapsible summary="Bare Workflow"> 187 <ReactMarkdown components={mdComponents}>{bareWorkflow}</ReactMarkdown> 188 </Collapsible> 189 )} 190 {example && ( 191 <Callout> 192 <BOLD>Example</BOLD> 193 <ReactMarkdown components={mdComponents}>{example}</ReactMarkdown> 194 </Callout> 195 )} 196 <div> 197 {subproperties.length > 0 && 198 subproperties.map((formattedProperty, index) => ( 199 <AppConfigProperty 200 {...formattedProperty} 201 key={`${name}-${index}`} 202 nestingLevel={nestingLevel + 1} 203 /> 204 ))} 205 </div> 206 </APIBox> 207); 208 209const boxStyle = css({ 210 boxShadow: 'none', 211 marginBottom: 0, 212 borderRadius: 0, 213 borderBottomWidth: 0, 214 paddingBottom: 0, 215 216 '&:first-of-type': { 217 borderTopLeftRadius: borderRadius.md, 218 borderTopRightRadius: borderRadius.md, 219 }, 220 221 '&:last-of-type': { 222 borderBottomLeftRadius: borderRadius.md, 223 borderBottomRightRadius: borderRadius.md, 224 marginBottom: spacing[4], 225 borderBottomWidth: 1, 226 }, 227 228 [`@media screen and (max-width: ${breakpoints.medium + 124}px)`]: { 229 paddingTop: spacing[4], 230 }, 231}); 232 233const secondaryCodeLineStyle = css({ 234 color: theme.text.secondary, 235 padding: `0 ${spacing[1]}px`, 236 wordBreak: 'break-word', 237}); 238 239const typeRow = css({ 240 margin: `${spacing[3]}px 0`, 241}); 242 243export default AppConfigSchemaPropertiesTable; 244