1import JsonFile, { JSONObject, JSONValue } from '@expo/json-file'; 2import plist from '@expo/plist'; 3import assert from 'assert'; 4import fs, { promises } from 'fs'; 5import path from 'path'; 6import xcode, { XcodeProject } from 'xcode'; 7 8import { ExportedConfig, ModConfig } from '../Plugin.types'; 9import { Entitlements, Paths } from '../ios'; 10import { ensureApplicationTargetEntitlementsFileConfigured } from '../ios/Entitlements'; 11import { InfoPlist } from '../ios/IosConfig.types'; 12import { getPbxproj } from '../ios/utils/Xcodeproj'; 13import { getInfoPlistPathFromPbxproj } from '../ios/utils/getInfoPlistPath'; 14import { fileExists } from '../utils/modules'; 15import { sortObject } from '../utils/sortObject'; 16import { addWarningIOS } from '../utils/warnings'; 17import { ForwardedBaseModOptions, provider, withGeneratedBaseMods } from './createBaseMod'; 18 19const { readFile, writeFile } = promises; 20 21type IosModName = keyof Required<ModConfig>['ios']; 22 23function getEntitlementsPlistTemplate() { 24 // TODO: Fetch the versioned template file if possible 25 return {}; 26} 27 28function getInfoPlistTemplate() { 29 // TODO: Fetch the versioned template file if possible 30 return { 31 CFBundleDevelopmentRegion: '$(DEVELOPMENT_LANGUAGE)', 32 CFBundleExecutable: '$(EXECUTABLE_NAME)', 33 CFBundleIdentifier: '$(PRODUCT_BUNDLE_IDENTIFIER)', 34 CFBundleName: '$(PRODUCT_NAME)', 35 CFBundlePackageType: '$(PRODUCT_BUNDLE_PACKAGE_TYPE)', 36 CFBundleInfoDictionaryVersion: '6.0', 37 CFBundleSignature: '????', 38 LSRequiresIPhoneOS: true, 39 NSAppTransportSecurity: { 40 NSAllowsArbitraryLoads: true, 41 NSExceptionDomains: { 42 localhost: { 43 NSExceptionAllowsInsecureHTTPLoads: true, 44 }, 45 }, 46 }, 47 UILaunchStoryboardName: 'SplashScreen', 48 UIRequiredDeviceCapabilities: ['armv7'], 49 UIViewControllerBasedStatusBarAppearance: false, 50 UIStatusBarStyle: 'UIStatusBarStyleDefault', 51 }; 52} 53 54const defaultProviders = { 55 dangerous: provider<unknown>({ 56 getFilePath() { 57 return ''; 58 }, 59 async read() { 60 return {}; 61 }, 62 async write() {}, 63 }), 64 // Append a rule to supply AppDelegate data to mods on `mods.ios.appDelegate` 65 appDelegate: provider<Paths.AppDelegateProjectFile>({ 66 getFilePath({ modRequest: { projectRoot } }) { 67 return Paths.getAppDelegateFilePath(projectRoot); 68 }, 69 async read(filePath) { 70 return Paths.getFileInfo(filePath); 71 }, 72 async write(filePath: string, { modResults: { contents } }) { 73 await writeFile(filePath, contents); 74 }, 75 }), 76 // Append a rule to supply Expo.plist data to mods on `mods.ios.expoPlist` 77 expoPlist: provider<JSONObject>({ 78 isIntrospective: true, 79 getFilePath({ modRequest: { platformProjectRoot, projectName } }) { 80 const supportingDirectory = path.join(platformProjectRoot, projectName!, 'Supporting'); 81 return path.resolve(supportingDirectory, 'Expo.plist'); 82 }, 83 async read(filePath, { modRequest: { introspect } }) { 84 try { 85 return plist.parse(await readFile(filePath, 'utf8')); 86 } catch (error) { 87 if (introspect) { 88 return {}; 89 } 90 throw error; 91 } 92 }, 93 async write(filePath, { modResults, modRequest: { introspect } }) { 94 if (introspect) { 95 return; 96 } 97 await writeFile(filePath, plist.build(sortObject(modResults))); 98 }, 99 }), 100 // Append a rule to supply .xcodeproj data to mods on `mods.ios.xcodeproj` 101 xcodeproj: provider<XcodeProject>({ 102 getFilePath({ modRequest: { projectRoot } }) { 103 return Paths.getPBXProjectPath(projectRoot); 104 }, 105 async read(filePath) { 106 const project = xcode.project(filePath); 107 project.parseSync(); 108 return project; 109 }, 110 async write(filePath, { modResults }) { 111 await writeFile(filePath, modResults.writeSync()); 112 }, 113 }), 114 // Append a rule to supply Info.plist data to mods on `mods.ios.infoPlist` 115 infoPlist: provider<InfoPlist, ForwardedBaseModOptions>({ 116 isIntrospective: true, 117 async getFilePath(config) { 118 let project: xcode.XcodeProject | null = null; 119 try { 120 project = getPbxproj(config.modRequest.projectRoot); 121 } catch { 122 // noop 123 } 124 125 // Only check / warn if a project actually exists, this'll provide 126 // more accurate warning messages for users in managed projects. 127 if (project) { 128 const infoPlistBuildProperty = getInfoPlistPathFromPbxproj(project); 129 130 if (infoPlistBuildProperty) { 131 //: [root]/myapp/ios/MyApp/Info.plist 132 const infoPlistPath = path.join( 133 //: myapp/ios 134 config.modRequest.platformProjectRoot, 135 //: MyApp/Info.plist 136 infoPlistBuildProperty 137 ); 138 if (fileExists(infoPlistPath)) { 139 return infoPlistPath; 140 } 141 addWarningIOS( 142 'mods.ios.infoPlist', 143 `Info.plist file linked to Xcode project does not exist: ${infoPlistPath}` 144 ); 145 } else { 146 addWarningIOS('mods.ios.infoPlist', 'Failed to find Info.plist linked to Xcode project.'); 147 } 148 } 149 try { 150 // Fallback on glob... 151 return await Paths.getInfoPlistPath(config.modRequest.projectRoot); 152 } catch (error: any) { 153 if (config.modRequest.introspect) { 154 // fallback to an empty string in introspection mode. 155 return ''; 156 } 157 throw error; 158 } 159 }, 160 async read(filePath, config) { 161 // Apply all of the Info.plist values to the expo.ios.infoPlist object 162 // TODO: Remove this in favor of just overwriting the Info.plist with the Expo object. This will enable people to actually remove values. 163 if (!config.ios) config.ios = {}; 164 if (!config.ios.infoPlist) config.ios.infoPlist = {}; 165 166 let modResults: InfoPlist; 167 try { 168 const contents = await readFile(filePath, 'utf8'); 169 assert(contents, 'Info.plist is empty'); 170 modResults = plist.parse(contents) as InfoPlist; 171 } catch (error: any) { 172 // Throw errors in introspection mode. 173 if (!config.modRequest.introspect) { 174 throw error; 175 } 176 // Fallback to using the infoPlist object from the Expo config. 177 modResults = getInfoPlistTemplate(); 178 } 179 180 config.ios.infoPlist = { 181 ...(modResults || {}), 182 ...config.ios.infoPlist, 183 }; 184 185 return config.ios.infoPlist!; 186 }, 187 async write(filePath, config) { 188 // Update the contents of the static infoPlist object 189 if (!config.ios) { 190 config.ios = {}; 191 } 192 config.ios.infoPlist = config.modResults; 193 194 // Return early without writing, in introspection mode. 195 if (config.modRequest.introspect) { 196 return; 197 } 198 199 await writeFile(filePath, plist.build(sortObject(config.modResults))); 200 }, 201 }), 202 // Append a rule to supply .entitlements data to mods on `mods.ios.entitlements` 203 entitlements: provider<JSONObject, ForwardedBaseModOptions>({ 204 isIntrospective: true, 205 206 async getFilePath(config) { 207 try { 208 ensureApplicationTargetEntitlementsFileConfigured(config.modRequest.projectRoot); 209 return Entitlements.getEntitlementsPath(config.modRequest.projectRoot) ?? ''; 210 } catch (error: any) { 211 if (config.modRequest.introspect) { 212 // fallback to an empty string in introspection mode. 213 return ''; 214 } 215 throw error; 216 } 217 }, 218 219 async read(filePath, config) { 220 let modResults: JSONObject; 221 try { 222 if (fs.existsSync(filePath)) { 223 const contents = await readFile(filePath, 'utf8'); 224 assert(contents, 'Entitlements plist is empty'); 225 modResults = plist.parse(contents); 226 } else { 227 modResults = getEntitlementsPlistTemplate(); 228 } 229 } catch (error: any) { 230 // Throw errors in introspection mode. 231 if (!config.modRequest.introspect) { 232 throw error; 233 } 234 // Fallback to using the template file. 235 modResults = getEntitlementsPlistTemplate(); 236 } 237 238 // Apply all of the .entitlements values to the expo.ios.entitlements object 239 // TODO: Remove this in favor of just overwriting the .entitlements with the Expo object. This will enable people to actually remove values. 240 if (!config.ios) config.ios = {}; 241 if (!config.ios.entitlements) config.ios.entitlements = {}; 242 243 config.ios.entitlements = { 244 ...(modResults || {}), 245 ...config.ios.entitlements, 246 }; 247 248 return config.ios.entitlements!; 249 }, 250 251 async write(filePath, config) { 252 // Update the contents of the static entitlements object 253 if (!config.ios) { 254 config.ios = {}; 255 } 256 config.ios.entitlements = config.modResults; 257 258 // Return early without writing, in introspection mode. 259 if (config.modRequest.introspect) { 260 return; 261 } 262 263 await writeFile(filePath, plist.build(sortObject(config.modResults))); 264 }, 265 }), 266 267 // Append a rule to supply Podfile.properties.json data to mods on `mods.ios.podfileProperties` 268 podfileProperties: provider<Record<string, JSONValue>>({ 269 isIntrospective: true, 270 271 getFilePath({ modRequest: { platformProjectRoot } }) { 272 return path.resolve(platformProjectRoot, 'Podfile.properties.json'); 273 }, 274 async read(filePath) { 275 let results: Record<string, JSONValue> = {}; 276 try { 277 results = await JsonFile.readAsync(filePath); 278 } catch {} 279 return results; 280 }, 281 async write(filePath, { modResults, modRequest: { introspect } }) { 282 if (introspect) { 283 return; 284 } 285 await JsonFile.writeAsync(filePath, modResults); 286 }, 287 }), 288}; 289 290type IosDefaultProviders = typeof defaultProviders; 291 292export function withIosBaseMods( 293 config: ExportedConfig, 294 { 295 providers, 296 ...props 297 }: ForwardedBaseModOptions & { providers?: Partial<IosDefaultProviders> } = {} 298): ExportedConfig { 299 return withGeneratedBaseMods<IosModName>(config, { 300 ...props, 301 platform: 'ios', 302 providers: providers ?? getIosModFileProviders(), 303 }); 304} 305 306export function getIosModFileProviders() { 307 return defaultProviders; 308} 309