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