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