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