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