1import { Android, ExpoConfig, IOS } from '@expo/config-types'; 2import { getRuntimeVersionForSDKVersion } from '@expo/sdk-runtime-versions'; 3import fs from 'fs'; 4import { boolish } from 'getenv'; 5import path from 'path'; 6import resolveFrom from 'resolve-from'; 7import semver from 'semver'; 8 9import { AndroidConfig, IOSConfig } from '..'; 10 11export type ExpoConfigUpdates = Pick< 12 ExpoConfig, 13 'sdkVersion' | 'owner' | 'runtimeVersion' | 'updates' | 'slug' 14>; 15 16export function getExpoUpdatesPackageVersion(projectRoot: string): string | null { 17 const expoUpdatesPackageJsonPath = resolveFrom.silent(projectRoot, 'expo-updates/package.json'); 18 if (!expoUpdatesPackageJsonPath || !fs.existsSync(expoUpdatesPackageJsonPath)) { 19 return null; 20 } 21 const packageJson = JSON.parse(fs.readFileSync(expoUpdatesPackageJsonPath, 'utf8')); 22 return packageJson.version; 23} 24 25function shouldDefaultToClassicUpdates(config: Pick<ExpoConfigUpdates, 'updates'>): boolean { 26 return !!config.updates?.useClassicUpdates; 27} 28 29export function getUpdateUrl( 30 config: Pick<ExpoConfigUpdates, 'owner' | 'slug' | 'updates'>, 31 username: string | null 32): string | null { 33 if (config.updates?.url) { 34 return config.updates?.url; 35 } 36 37 if (!shouldDefaultToClassicUpdates(config)) { 38 return null; 39 } 40 41 const user = typeof config.owner === 'string' ? config.owner : username; 42 if (!user) { 43 return null; 44 } 45 return `https://exp.host/@${user}/${config.slug}`; 46} 47 48export function getAppVersion(config: Pick<ExpoConfig, 'version'>): string { 49 return config.version ?? '1.0.0'; 50} 51 52export function getNativeVersion( 53 config: Pick<ExpoConfig, 'version'> & { 54 android?: Pick<Android, 'versionCode'>; 55 ios?: Pick<IOS, 'buildNumber'>; 56 }, 57 platform: 'android' | 'ios' 58): string { 59 const version = IOSConfig.Version.getVersion(config); 60 switch (platform) { 61 case 'ios': { 62 const buildNumber = IOSConfig.Version.getBuildNumber(config); 63 return `${version}(${buildNumber})`; 64 } 65 case 'android': { 66 const versionCode = AndroidConfig.Version.getVersionCode(config); 67 return `${version}(${versionCode})`; 68 } 69 default: { 70 throw new Error( 71 `"${platform}" is not a supported platform. Choose either "ios" or "android".` 72 ); 73 } 74 } 75} 76 77/** 78 * Compute runtime version policies. 79 * @return an expoConfig with only string valued platform specific runtime versions. 80 */ 81export const withRuntimeVersion: (config: ExpoConfig) => ExpoConfig = (config) => { 82 if (config.ios?.runtimeVersion || config.runtimeVersion) { 83 const runtimeVersion = getRuntimeVersion(config, 'ios'); 84 if (runtimeVersion) { 85 config.ios = { 86 ...config.ios, 87 runtimeVersion, 88 }; 89 } 90 } 91 if (config.android?.runtimeVersion || config.runtimeVersion) { 92 const runtimeVersion = getRuntimeVersion(config, 'android'); 93 if (runtimeVersion) { 94 config.android = { 95 ...config.android, 96 runtimeVersion, 97 }; 98 } 99 } 100 delete config.runtimeVersion; 101 return config; 102}; 103 104export function getRuntimeVersionNullable( 105 ...[config, platform]: Parameters<typeof getRuntimeVersion> 106): string | null { 107 try { 108 return getRuntimeVersion(config, platform); 109 } catch (e) { 110 if (boolish('EXPO_DEBUG', false)) { 111 console.log(e); 112 } 113 return null; 114 } 115} 116 117export function getRuntimeVersion( 118 config: Pick<ExpoConfig, 'version' | 'runtimeVersion' | 'sdkVersion'> & { 119 android?: Pick<Android, 'versionCode' | 'runtimeVersion'>; 120 ios?: Pick<IOS, 'buildNumber' | 'runtimeVersion'>; 121 }, 122 platform: 'android' | 'ios' 123): string | null { 124 const runtimeVersion = config[platform]?.runtimeVersion ?? config.runtimeVersion; 125 if (!runtimeVersion) { 126 return null; 127 } 128 129 if (typeof runtimeVersion === 'string') { 130 return runtimeVersion; 131 } else if (runtimeVersion.policy === 'appVersion') { 132 return getAppVersion(config); 133 } else if (runtimeVersion.policy === 'nativeVersion') { 134 return getNativeVersion(config, platform); 135 } else if (runtimeVersion.policy === 'sdkVersion') { 136 if (!config.sdkVersion) { 137 throw new Error("An SDK version must be defined when using the 'sdkVersion' runtime policy."); 138 } 139 return getRuntimeVersionForSDKVersion(config.sdkVersion); 140 } 141 142 throw new Error( 143 `"${ 144 typeof runtimeVersion === 'object' ? JSON.stringify(runtimeVersion) : runtimeVersion 145 }" is not a valid runtime version. getRuntimeVersion only supports a string, "sdkVersion", "appVersion", or "nativeVersion" policy.` 146 ); 147} 148 149export function getSDKVersion(config: Pick<ExpoConfigUpdates, 'sdkVersion'>): string | null { 150 return typeof config.sdkVersion === 'string' ? config.sdkVersion : null; 151} 152 153export function getUpdatesEnabled( 154 config: Pick<ExpoConfigUpdates, 'owner' | 'slug' | 'updates'>, 155 username: string | null 156): boolean { 157 // allow override of enabled property 158 if (config.updates?.enabled !== undefined) { 159 return config.updates.enabled; 160 } 161 162 // enable if URL is set (which respects shouldDefaultToClassicUpdates) 163 return getUpdateUrl(config, username) !== null; 164} 165 166export function getUpdatesTimeout(config: Pick<ExpoConfigUpdates, 'updates'>): number { 167 return config.updates?.fallbackToCacheTimeout ?? 0; 168} 169 170export function getUpdatesCheckOnLaunch( 171 config: Pick<ExpoConfigUpdates, 'updates'>, 172 expoUpdatesPackageVersion?: string | null 173): 'NEVER' | 'ERROR_RECOVERY_ONLY' | 'ALWAYS' | 'WIFI_ONLY' { 174 if (config.updates?.checkAutomatically === 'ON_ERROR_RECOVERY') { 175 // native 'ERROR_RECOVERY_ONLY' option was only introduced in 0.11.x 176 if (expoUpdatesPackageVersion && semver.gte(expoUpdatesPackageVersion, '0.11.0')) { 177 return 'ERROR_RECOVERY_ONLY'; 178 } 179 return 'NEVER'; 180 } else if (config.updates?.checkAutomatically === 'ON_LOAD') { 181 return 'ALWAYS'; 182 } else if (config.updates?.checkAutomatically === 'WIFI_ONLY') { 183 return 'WIFI_ONLY'; 184 } else if (config.updates?.checkAutomatically === 'NEVER') { 185 return 'NEVER'; 186 } 187 return 'ALWAYS'; 188} 189 190export function getUpdatesCodeSigningCertificate( 191 projectRoot: string, 192 config: Pick<ExpoConfigUpdates, 'updates'> 193): string | undefined { 194 const codeSigningCertificatePath = config.updates?.codeSigningCertificate; 195 if (!codeSigningCertificatePath) { 196 return undefined; 197 } 198 199 const finalPath = path.join(projectRoot, codeSigningCertificatePath); 200 if (!fs.existsSync(finalPath)) { 201 throw new Error(`File not found at \`updates.codeSigningCertificate\` path: ${finalPath}`); 202 } 203 204 return fs.readFileSync(finalPath, 'utf8'); 205} 206 207export function getUpdatesCodeSigningMetadata( 208 config: Pick<ExpoConfigUpdates, 'updates'> 209): NonNullable<ExpoConfigUpdates['updates']>['codeSigningMetadata'] { 210 return config.updates?.codeSigningMetadata; 211} 212 213export function getUpdatesCodeSigningMetadataStringified( 214 config: Pick<ExpoConfigUpdates, 'updates'> 215): string | undefined { 216 const metadata = getUpdatesCodeSigningMetadata(config); 217 if (!metadata) { 218 return undefined; 219 } 220 221 return JSON.stringify(metadata); 222} 223 224export function getUpdatesRequestHeaders( 225 config: Pick<ExpoConfigUpdates, 'updates'> 226): NonNullable<ExpoConfigUpdates['updates']>['requestHeaders'] { 227 return config.updates?.requestHeaders; 228} 229 230export function getUpdatesRequestHeadersStringified( 231 config: Pick<ExpoConfigUpdates, 'updates'> 232): string | undefined { 233 const metadata = getUpdatesRequestHeaders(config); 234 if (!metadata) { 235 return undefined; 236 } 237 238 return JSON.stringify(metadata); 239} 240