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 getNativeVersion( 41 config: Pick<ExpoConfig, 'version'> & { 42 android?: Pick<Android, 'versionCode'>; 43 ios?: Pick<IOS, 'buildNumber'>; 44 }, 45 platform: 'android' | 'ios' 46): string { 47 const version = IOSConfig.Version.getVersion(config); 48 switch (platform) { 49 case 'ios': { 50 const buildNumber = IOSConfig.Version.getBuildNumber(config); 51 return `${version}(${buildNumber})`; 52 } 53 case 'android': { 54 const versionCode = AndroidConfig.Version.getVersionCode(config); 55 return `${version}(${versionCode})`; 56 } 57 default: { 58 throw new Error( 59 `"${platform}" is not a supported platform. Choose either "ios" or "android".` 60 ); 61 } 62 } 63} 64 65/** 66 * Compute runtime version policies. 67 * @return an expoConfig with only string valued platform specific runtime versions. 68 */ 69export const withRuntimeVersion: (config: ExpoConfig) => ExpoConfig = (config) => { 70 if (config.ios?.runtimeVersion || config.runtimeVersion) { 71 const runtimeVersion = getRuntimeVersion(config, 'ios'); 72 if (runtimeVersion) { 73 config.ios = { 74 ...config.ios, 75 runtimeVersion, 76 }; 77 } 78 } 79 if (config.android?.runtimeVersion || config.runtimeVersion) { 80 const runtimeVersion = getRuntimeVersion(config, 'android'); 81 if (runtimeVersion) { 82 config.android = { 83 ...config.android, 84 runtimeVersion, 85 }; 86 } 87 } 88 delete config.runtimeVersion; 89 return config; 90}; 91 92export function getRuntimeVersionNullable( 93 ...[config, platform]: Parameters<typeof getRuntimeVersion> 94): string | null { 95 try { 96 return getRuntimeVersion(config, platform); 97 } catch (e) { 98 if (boolish('EXPO_DEBUG', false)) { 99 console.log(e); 100 } 101 return null; 102 } 103} 104 105export function getRuntimeVersion( 106 config: Pick<ExpoConfig, 'version' | 'runtimeVersion' | 'sdkVersion'> & { 107 android?: Pick<Android, 'versionCode' | 'runtimeVersion'>; 108 ios?: Pick<IOS, 'buildNumber' | 'runtimeVersion'>; 109 }, 110 platform: 'android' | 'ios' 111): string | null { 112 const runtimeVersion = config[platform]?.runtimeVersion ?? config.runtimeVersion; 113 if (!runtimeVersion) { 114 return null; 115 } 116 117 if (typeof runtimeVersion === 'string') { 118 return runtimeVersion; 119 } else if (runtimeVersion.policy === 'nativeVersion') { 120 return getNativeVersion(config, platform); 121 } else if (runtimeVersion.policy === 'sdkVersion') { 122 if (!config.sdkVersion) { 123 throw new Error("An SDK version must be defined when using the 'sdkVersion' runtime policy."); 124 } 125 return getRuntimeVersionForSDKVersion(config.sdkVersion); 126 } 127 128 throw new Error( 129 `"${ 130 typeof runtimeVersion === 'object' ? JSON.stringify(runtimeVersion) : runtimeVersion 131 }" is not a valid runtime version. getRuntimeVersion only supports a string, "sdkVersion", or "nativeVersion" policy.` 132 ); 133} 134 135export function getSDKVersion(config: Pick<ExpoConfigUpdates, 'sdkVersion'>): string | null { 136 return typeof config.sdkVersion === 'string' ? config.sdkVersion : null; 137} 138 139export function getUpdatesEnabled(config: Pick<ExpoConfigUpdates, 'updates'>): boolean { 140 return config.updates?.enabled !== false; 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' { 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 } 160 return 'ALWAYS'; 161} 162 163export function getUpdatesCodeSigningCertificate( 164 projectRoot: string, 165 config: Pick<ExpoConfigUpdates, 'updates'> 166): string | undefined { 167 const codeSigningCertificatePath = config.updates?.codeSigningCertificate; 168 if (!codeSigningCertificatePath) { 169 return undefined; 170 } 171 172 const finalPath = path.join(projectRoot, codeSigningCertificatePath); 173 if (!fs.existsSync(finalPath)) { 174 throw new Error(`File not found at \`updates.codeSigningCertificate\` path: ${finalPath}`); 175 } 176 177 return fs.readFileSync(finalPath, 'utf8'); 178} 179 180export function getUpdatesCodeSigningMetadata( 181 config: Pick<ExpoConfigUpdates, 'updates'> 182): NonNullable<ExpoConfigUpdates['updates']>['codeSigningMetadata'] { 183 return config.updates?.codeSigningMetadata; 184} 185 186export function getUpdatesCodeSigningMetadataStringified( 187 config: Pick<ExpoConfigUpdates, 'updates'> 188): string | undefined { 189 const metadata = getUpdatesCodeSigningMetadata(config); 190 if (!metadata) { 191 return undefined; 192 } 193 194 return JSON.stringify(metadata); 195} 196