1import * as path from 'path'; 2import resolveFrom from 'resolve-from'; 3import xcode from 'xcode'; 4 5import { ConfigPlugin } from '../Plugin.types'; 6import { withExpoPlist } from '../plugins/ios-plugins'; 7import { 8 ExpoConfigUpdates, 9 getExpoUpdatesPackageVersion, 10 getRuntimeVersionNullable, 11 getSDKVersion, 12 getUpdatesCheckOnLaunch, 13 getUpdatesCodeSigningCertificate, 14 getUpdatesCodeSigningMetadata, 15 getUpdatesRequestHeaders, 16 getUpdatesEnabled, 17 getUpdatesTimeout, 18 getUpdateUrl, 19} from '../utils/Updates'; 20import { ExpoPlist } from './IosConfig.types'; 21 22const CREATE_MANIFEST_IOS_PATH = 'expo-updates/scripts/create-manifest-ios.sh'; 23 24export enum Config { 25 ENABLED = 'EXUpdatesEnabled', 26 CHECK_ON_LAUNCH = 'EXUpdatesCheckOnLaunch', 27 LAUNCH_WAIT_MS = 'EXUpdatesLaunchWaitMs', 28 RUNTIME_VERSION = 'EXUpdatesRuntimeVersion', 29 SDK_VERSION = 'EXUpdatesSDKVersion', 30 UPDATE_URL = 'EXUpdatesURL', 31 RELEASE_CHANNEL = 'EXUpdatesReleaseChannel', 32 UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY = 'EXUpdatesRequestHeaders', 33 CODE_SIGNING_CERTIFICATE = 'EXUpdatesCodeSigningCertificate', 34 CODE_SIGNING_METADATA = 'EXUpdatesCodeSigningMetadata', 35} 36 37export const withUpdates: ConfigPlugin<{ expoUsername: string | null }> = ( 38 config, 39 { expoUsername } 40) => { 41 return withExpoPlist(config, (config) => { 42 const projectRoot = config.modRequest.projectRoot; 43 const expoUpdatesPackageVersion = getExpoUpdatesPackageVersion(projectRoot); 44 config.modResults = setUpdatesConfig( 45 projectRoot, 46 config, 47 config.modResults, 48 expoUsername, 49 expoUpdatesPackageVersion 50 ); 51 return config; 52 }); 53}; 54 55export function setUpdatesConfig( 56 projectRoot: string, 57 config: ExpoConfigUpdates, 58 expoPlist: ExpoPlist, 59 username: string | null, 60 expoUpdatesPackageVersion?: string | null 61): ExpoPlist { 62 const newExpoPlist = { 63 ...expoPlist, 64 [Config.ENABLED]: getUpdatesEnabled(config, username), 65 [Config.CHECK_ON_LAUNCH]: getUpdatesCheckOnLaunch(config, expoUpdatesPackageVersion), 66 [Config.LAUNCH_WAIT_MS]: getUpdatesTimeout(config), 67 }; 68 69 const updateUrl = getUpdateUrl(config, username); 70 if (updateUrl) { 71 newExpoPlist[Config.UPDATE_URL] = updateUrl; 72 } else { 73 delete newExpoPlist[Config.UPDATE_URL]; 74 } 75 76 const codeSigningCertificate = getUpdatesCodeSigningCertificate(projectRoot, config); 77 if (codeSigningCertificate) { 78 newExpoPlist[Config.CODE_SIGNING_CERTIFICATE] = codeSigningCertificate; 79 } else { 80 delete newExpoPlist[Config.CODE_SIGNING_CERTIFICATE]; 81 } 82 83 const codeSigningMetadata = getUpdatesCodeSigningMetadata(config); 84 if (codeSigningMetadata) { 85 newExpoPlist[Config.CODE_SIGNING_METADATA] = codeSigningMetadata; 86 } else { 87 delete newExpoPlist[Config.CODE_SIGNING_METADATA]; 88 } 89 90 const requestHeaders = getUpdatesRequestHeaders(config); 91 if (requestHeaders) { 92 newExpoPlist[Config.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY] = requestHeaders; 93 } else { 94 delete newExpoPlist[Config.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY]; 95 } 96 97 return setVersionsConfig(config, newExpoPlist); 98} 99 100export function setVersionsConfig(config: ExpoConfigUpdates, expoPlist: ExpoPlist): ExpoPlist { 101 const newExpoPlist = { ...expoPlist }; 102 103 const runtimeVersion = getRuntimeVersionNullable(config, 'ios'); 104 if (!runtimeVersion && expoPlist[Config.RUNTIME_VERSION]) { 105 throw new Error( 106 'A runtime version is set in your Expo.plist, but is missing from your app.json/app.config.js. Please either set runtimeVersion in your app.json/app.config.js or remove EXUpdatesRuntimeVersion from your Expo.plist.' 107 ); 108 } 109 const sdkVersion = getSDKVersion(config); 110 if (runtimeVersion) { 111 delete newExpoPlist[Config.SDK_VERSION]; 112 newExpoPlist[Config.RUNTIME_VERSION] = runtimeVersion; 113 } else if (sdkVersion) { 114 /** 115 * runtime version maybe null in projects using classic updates. In that 116 * case we use SDK version 117 */ 118 delete newExpoPlist[Config.RUNTIME_VERSION]; 119 newExpoPlist[Config.SDK_VERSION] = sdkVersion; 120 } else { 121 delete newExpoPlist[Config.SDK_VERSION]; 122 delete newExpoPlist[Config.RUNTIME_VERSION]; 123 } 124 125 return newExpoPlist; 126} 127 128function formatConfigurationScriptPath(projectRoot: string): string { 129 const buildScriptPath = resolveFrom.silent(projectRoot, CREATE_MANIFEST_IOS_PATH); 130 131 if (!buildScriptPath) { 132 throw new Error( 133 "Could not find the build script for iOS. This could happen in case of outdated 'node_modules'. Run 'npm install' to make sure that it's up-to-date." 134 ); 135 } 136 137 const relativePath = path.relative(path.join(projectRoot, 'ios'), buildScriptPath); 138 return process.platform === 'win32' ? relativePath.replace(/\\/g, '/') : relativePath; 139} 140 141interface ShellScriptBuildPhase { 142 isa: 'PBXShellScriptBuildPhase'; 143 name: string; 144 shellScript: string; 145 [key: string]: any; 146} 147 148export function getBundleReactNativePhase(project: xcode.XcodeProject): ShellScriptBuildPhase { 149 const shellScriptBuildPhase = project.hash.project.objects.PBXShellScriptBuildPhase as Record< 150 string, 151 ShellScriptBuildPhase 152 >; 153 const bundleReactNative = Object.values(shellScriptBuildPhase).find( 154 (buildPhase) => buildPhase.name === '"Bundle React Native code and images"' 155 ); 156 157 if (!bundleReactNative) { 158 throw new Error(`Couldn't find a build phase "Bundle React Native code and images"`); 159 } 160 161 return bundleReactNative; 162} 163 164export function ensureBundleReactNativePhaseContainsConfigurationScript( 165 projectRoot: string, 166 project: xcode.XcodeProject 167): xcode.XcodeProject { 168 const bundleReactNative = getBundleReactNativePhase(project); 169 const buildPhaseShellScriptPath = formatConfigurationScriptPath(projectRoot); 170 171 if (!isShellScriptBuildPhaseConfigured(projectRoot, project)) { 172 // check if there's already another path to create-manifest-ios.sh 173 // this might be the case for monorepos 174 if (bundleReactNative.shellScript.includes(CREATE_MANIFEST_IOS_PATH)) { 175 bundleReactNative.shellScript = bundleReactNative.shellScript.replace( 176 new RegExp(`(\\\\n)(\\.\\.)+/node_modules/${CREATE_MANIFEST_IOS_PATH}`), 177 '' 178 ); 179 } 180 bundleReactNative.shellScript = `${bundleReactNative.shellScript.replace( 181 /"$/, 182 '' 183 )}${buildPhaseShellScriptPath}\\n"`; 184 } 185 return project; 186} 187 188export function isShellScriptBuildPhaseConfigured( 189 projectRoot: string, 190 project: xcode.XcodeProject 191): boolean { 192 const bundleReactNative = getBundleReactNativePhase(project); 193 const buildPhaseShellScriptPath = formatConfigurationScriptPath(projectRoot); 194 return bundleReactNative.shellScript.includes(buildPhaseShellScriptPath); 195} 196 197export function isPlistConfigurationSet(expoPlist: ExpoPlist): boolean { 198 return Boolean( 199 expoPlist.EXUpdatesURL && (expoPlist.EXUpdatesSDKVersion || expoPlist.EXUpdatesRuntimeVersion) 200 ); 201} 202 203export function isPlistConfigurationSynced( 204 projectRoot: string, 205 config: ExpoConfigUpdates, 206 expoPlist: ExpoPlist, 207 username: string | null 208): boolean { 209 return ( 210 getUpdateUrl(config, username) === expoPlist.EXUpdatesURL && 211 getUpdatesEnabled(config, username) === expoPlist.EXUpdatesEnabled && 212 getUpdatesTimeout(config) === expoPlist.EXUpdatesLaunchWaitMs && 213 getUpdatesCheckOnLaunch(config) === expoPlist.EXUpdatesCheckOnLaunch && 214 getUpdatesCodeSigningCertificate(projectRoot, config) === 215 expoPlist.EXUpdatesCodeSigningCertificate && 216 getUpdatesCodeSigningMetadata(config) === expoPlist.EXUpdatesCodeSigningMetadata && 217 isPlistVersionConfigurationSynced(config, expoPlist) 218 ); 219} 220 221export function isPlistVersionConfigurationSynced( 222 config: Pick<ExpoConfigUpdates, 'sdkVersion' | 'runtimeVersion'>, 223 expoPlist: ExpoPlist 224): boolean { 225 const expectedRuntimeVersion = getRuntimeVersionNullable(config, 'ios'); 226 const expectedSdkVersion = getSDKVersion(config); 227 228 const currentRuntimeVersion = expoPlist.EXUpdatesRuntimeVersion ?? null; 229 const currentSdkVersion = expoPlist.EXUpdatesSDKVersion ?? null; 230 231 if (expectedRuntimeVersion !== null) { 232 return currentRuntimeVersion === expectedRuntimeVersion && currentSdkVersion === null; 233 } else if (expectedSdkVersion !== null) { 234 return currentSdkVersion === expectedSdkVersion && currentRuntimeVersion === null; 235 } else { 236 return true; 237 } 238} 239