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