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