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