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 getRuntimeVersionNullableAsync, 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, async (config) => { 42 const projectRoot = config.modRequest.projectRoot; 43 const expoUpdatesPackageVersion = getExpoUpdatesPackageVersion(projectRoot); 44 config.modResults = await setUpdatesConfigAsync( 45 projectRoot, 46 config, 47 config.modResults, 48 expoUpdatesPackageVersion 49 ); 50 return config; 51 }); 52}; 53 54export async function setUpdatesConfigAsync( 55 projectRoot: string, 56 config: ExpoConfigUpdates, 57 expoPlist: ExpoPlist, 58 expoUpdatesPackageVersion?: string | null 59): Promise<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 await setVersionsConfigAsync(projectRoot, config, newExpoPlist); 96} 97 98export async function setVersionsConfigAsync( 99 projectRoot: string, 100 config: ExpoConfigUpdates, 101 expoPlist: ExpoPlist 102): Promise<ExpoPlist> { 103 const newExpoPlist = { ...expoPlist }; 104 105 const runtimeVersion = await getRuntimeVersionNullableAsync(projectRoot, 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 async function isPlistConfigurationSyncedAsync( 206 projectRoot: string, 207 config: ExpoConfigUpdates, 208 expoPlist: ExpoPlist 209): Promise<boolean> { 210 return ( 211 getUpdateUrl(config) === expoPlist.EXUpdatesURL && 212 getUpdatesEnabled(config) === expoPlist.EXUpdatesEnabled && 213 getUpdatesTimeout(config) === expoPlist.EXUpdatesLaunchWaitMs && 214 getUpdatesCheckOnLaunch(config) === expoPlist.EXUpdatesCheckOnLaunch && 215 getUpdatesCodeSigningCertificate(projectRoot, config) === 216 expoPlist.EXUpdatesCodeSigningCertificate && 217 getUpdatesCodeSigningMetadata(config) === expoPlist.EXUpdatesCodeSigningMetadata && 218 (await isPlistVersionConfigurationSyncedAsync(projectRoot, config, expoPlist)) 219 ); 220} 221 222export async function isPlistVersionConfigurationSyncedAsync( 223 projectRoot: string, 224 config: Pick<ExpoConfigUpdates, 'sdkVersion' | 'runtimeVersion'>, 225 expoPlist: ExpoPlist 226): Promise<boolean> { 227 const expectedRuntimeVersion = await getRuntimeVersionNullableAsync(projectRoot, 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