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 // TODO: Get application AppDelegate file from pbxproj. 68 return Paths.getAppDelegateFilePath(projectRoot); 69 }, 70 async read(filePath) { 71 return Paths.getFileInfo(filePath); 72 }, 73 async write(filePath: string, { modResults: { contents } }) { 74 await writeFile(filePath, contents); 75 }, 76 }), 77 // Append a rule to supply Expo.plist data to mods on `mods.ios.expoPlist` 78 expoPlist: provider<JSONObject>({ 79 isIntrospective: true, 80 getFilePath({ modRequest: { platformProjectRoot, projectName } }) { 81 const supportingDirectory = path.join(platformProjectRoot, projectName!, 'Supporting'); 82 return path.resolve(supportingDirectory, 'Expo.plist'); 83 }, 84 async read(filePath, { modRequest: { introspect } }) { 85 try { 86 return plist.parse(await readFile(filePath, 'utf8')); 87 } catch (error) { 88 if (introspect) { 89 return {}; 90 } 91 throw error; 92 } 93 }, 94 async write(filePath, { modResults, modRequest: { introspect } }) { 95 if (introspect) { 96 return; 97 } 98 await writeFile(filePath, plist.build(sortObject(modResults))); 99 }, 100 }), 101 // Append a rule to supply .xcodeproj data to mods on `mods.ios.xcodeproj` 102 xcodeproj: provider<XcodeProject>({ 103 getFilePath({ modRequest: { projectRoot } }) { 104 return Paths.getPBXProjectPath(projectRoot); 105 }, 106 async read(filePath) { 107 const project = xcode.project(filePath); 108 project.parseSync(); 109 return project; 110 }, 111 async write(filePath, { modResults }) { 112 await writeFile(filePath, modResults.writeSync()); 113 }, 114 }), 115 // Append a rule to supply Info.plist data to mods on `mods.ios.infoPlist` 116 infoPlist: provider<InfoPlist, ForwardedBaseModOptions>({ 117 isIntrospective: true, 118 async getFilePath(config) { 119 let project: xcode.XcodeProject | null = null; 120 try { 121 project = getPbxproj(config.modRequest.projectRoot); 122 } catch { 123 // noop 124 } 125 126 // Only check / warn if a project actually exists, this'll provide 127 // more accurate warning messages for users in managed projects. 128 if (project) { 129 const infoPlistBuildProperty = getInfoPlistPathFromPbxproj(project); 130 131 if (infoPlistBuildProperty) { 132 //: [root]/myapp/ios/MyApp/Info.plist 133 const infoPlistPath = path.join( 134 //: myapp/ios 135 config.modRequest.platformProjectRoot, 136 //: MyApp/Info.plist 137 infoPlistBuildProperty 138 ); 139 if (fileExists(infoPlistPath)) { 140 return infoPlistPath; 141 } 142 addWarningIOS( 143 'mods.ios.infoPlist', 144 `Info.plist file linked to Xcode project does not exist: ${infoPlistPath}` 145 ); 146 } else { 147 addWarningIOS('mods.ios.infoPlist', 'Failed to find Info.plist linked to Xcode project.'); 148 } 149 } 150 try { 151 // Fallback on glob... 152 return await Paths.getInfoPlistPath(config.modRequest.projectRoot); 153 } catch (error: any) { 154 if (config.modRequest.introspect) { 155 // fallback to an empty string in introspection mode. 156 return ''; 157 } 158 throw error; 159 } 160 }, 161 async read(filePath, config) { 162 // Apply all of the Info.plist values to the expo.ios.infoPlist object 163 // TODO: Remove this in favor of just overwriting the Info.plist with the Expo object. This will enable people to actually remove values. 164 if (!config.ios) config.ios = {}; 165 if (!config.ios.infoPlist) config.ios.infoPlist = {}; 166 167 let modResults: InfoPlist; 168 try { 169 const contents = await readFile(filePath, 'utf8'); 170 assert(contents, 'Info.plist is empty'); 171 modResults = plist.parse(contents) as InfoPlist; 172 } catch (error: any) { 173 // Throw errors in introspection mode. 174 if (!config.modRequest.introspect) { 175 throw error; 176 } 177 // Fallback to using the infoPlist object from the Expo config. 178 modResults = getInfoPlistTemplate(); 179 } 180 181 config.ios.infoPlist = { 182 ...(modResults || {}), 183 ...config.ios.infoPlist, 184 }; 185 186 return config.ios.infoPlist!; 187 }, 188 async write(filePath, config) { 189 // Update the contents of the static infoPlist object 190 if (!config.ios) { 191 config.ios = {}; 192 } 193 config.ios.infoPlist = config.modResults; 194 195 // Return early without writing, in introspection mode. 196 if (config.modRequest.introspect) { 197 return; 198 } 199 200 await writeFile(filePath, plist.build(sortObject(config.modResults))); 201 }, 202 }), 203 // Append a rule to supply .entitlements data to mods on `mods.ios.entitlements` 204 entitlements: provider<JSONObject, ForwardedBaseModOptions>({ 205 isIntrospective: true, 206 207 async getFilePath(config) { 208 try { 209 ensureApplicationTargetEntitlementsFileConfigured(config.modRequest.projectRoot); 210 return Entitlements.getEntitlementsPath(config.modRequest.projectRoot) ?? ''; 211 } catch (error: any) { 212 if (config.modRequest.introspect) { 213 // fallback to an empty string in introspection mode. 214 return ''; 215 } 216 throw error; 217 } 218 }, 219 220 async read(filePath, config) { 221 let modResults: JSONObject; 222 try { 223 if (fs.existsSync(filePath)) { 224 const contents = await readFile(filePath, 'utf8'); 225 assert(contents, 'Entitlements plist is empty'); 226 modResults = plist.parse(contents); 227 } else { 228 modResults = getEntitlementsPlistTemplate(); 229 } 230 } catch (error: any) { 231 // Throw errors in introspection mode. 232 if (!config.modRequest.introspect) { 233 throw error; 234 } 235 // Fallback to using the template file. 236 modResults = getEntitlementsPlistTemplate(); 237 } 238 239 // Apply all of the .entitlements values to the expo.ios.entitlements object 240 // TODO: Remove this in favor of just overwriting the .entitlements with the Expo object. This will enable people to actually remove values. 241 if (!config.ios) config.ios = {}; 242 if (!config.ios.entitlements) config.ios.entitlements = {}; 243 244 config.ios.entitlements = { 245 ...(modResults || {}), 246 ...config.ios.entitlements, 247 }; 248 249 return config.ios.entitlements!; 250 }, 251 252 async write(filePath, config) { 253 // Update the contents of the static entitlements object 254 if (!config.ios) { 255 config.ios = {}; 256 } 257 config.ios.entitlements = config.modResults; 258 259 // Return early without writing, in introspection mode. 260 if (config.modRequest.introspect) { 261 return; 262 } 263 264 await writeFile(filePath, plist.build(sortObject(config.modResults))); 265 }, 266 }), 267 268 // Append a rule to supply Podfile.properties.json data to mods on `mods.ios.podfileProperties` 269 podfileProperties: provider<Record<string, JSONValue>>({ 270 isIntrospective: true, 271 272 getFilePath({ modRequest: { platformProjectRoot } }) { 273 return path.resolve(platformProjectRoot, 'Podfile.properties.json'); 274 }, 275 async read(filePath) { 276 let results: Record<string, JSONValue> = {}; 277 try { 278 results = await JsonFile.readAsync(filePath); 279 } catch {} 280 return results; 281 }, 282 async write(filePath, { modResults, modRequest: { introspect } }) { 283 if (introspect) { 284 return; 285 } 286 await JsonFile.writeAsync(filePath, modResults); 287 }, 288 }), 289}; 290 291type IosDefaultProviders = typeof defaultProviders; 292 293export function withIosBaseMods( 294 config: ExportedConfig, 295 { 296 providers, 297 ...props 298 }: ForwardedBaseModOptions & { providers?: Partial<IosDefaultProviders> } = {} 299): ExportedConfig { 300 return withGeneratedBaseMods<IosModName>(config, { 301 ...props, 302 platform: 'ios', 303 providers: providers ?? getIosModFileProviders(), 304 }); 305} 306 307export function getIosModFileProviders() { 308 return defaultProviders; 309} 310