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