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