1import Ajv, { JSONSchemaType } from 'ajv';
2import semver from 'semver';
3
4/**
5 * The minimal supported versions. These values should align to SDK.
6 * @ignore
7 */
8const EXPO_SDK_MINIMAL_SUPPORTED_VERSIONS = {
9  android: {
10    minSdkVersion: 21,
11    compileSdkVersion: 31,
12    targetSdkVersion: 31,
13    kotlinVersion: '1.6.10',
14  },
15  ios: {
16    deploymentTarget: '13.0',
17  },
18};
19
20/**
21 * Interface representing base build properties configuration.
22 */
23export interface PluginConfigType {
24  /**
25   * Interface representing available configuration for Android native build properties.
26   * @platform android
27   */
28  android?: PluginConfigTypeAndroid;
29  /**
30   * Interface representing available configuration for iOS native build properties.
31   * @platform ios
32   */
33  ios?: PluginConfigTypeIos;
34}
35
36/**
37 * Interface representing available configuration for Android native build properties.
38 * @platform android
39 */
40export interface PluginConfigTypeAndroid {
41  /**
42   * Enable React Native new architecture for Android platform.
43   */
44  newArchEnabled?: boolean;
45  /**
46   * Override the default `minSdkVersion` version number in **build.gradle**.
47   * */
48  minSdkVersion?: number;
49  /**
50   * Override the default `compileSdkVersion` version number in **build.gradle**.
51   */
52  compileSdkVersion?: number;
53  /**
54   * Override the default `targetSdkVersion` version number in **build.gradle**.
55   */
56  targetSdkVersion?: number;
57  /**
58   *  Override the default `buildToolsVersion` version number in **build.gradle**.
59   */
60  buildToolsVersion?: string;
61  /**
62   * Override the Kotlin version used when building the app.
63   */
64  kotlinVersion?: string;
65  /**
66   * Enable [Proguard or R8](https://developer.android.com/studio/build/shrink-code) in release builds to obfuscate Java code and reduce app size.
67   */
68  enableProguardInReleaseBuilds?: boolean;
69  /**
70   * Append custom [Proguard rules](https://www.guardsquare.com/manual/configuration/usage) to **android/app/proguard-rules.pro**.
71   */
72  extraProguardRules?: string;
73  /**
74   * Interface representing available configuration for Android Gradle plugin [PackagingOptions](https://developer.android.com/reference/tools/gradle-api/7.0/com/android/build/api/dsl/PackagingOptions).
75   */
76  packagingOptions?: PluginConfigTypeAndroidPackagingOptions;
77
78  /**
79   * Change the [Flipper](https://fbflipper.com/) version when running your app on Android.
80   * Flipper is by default enabled with the version that comes bundled with react-native.
81   * However, you can set the `flipper` property to a semver string and specify an
82   * alternate Flipper version.
83   */
84  flipper?: string;
85}
86
87/**
88 * Interface representing available configuration for iOS native build properties.
89 * @platform ios
90 */
91export interface PluginConfigTypeIos {
92  /**
93   * Enable React Native new architecture for iOS platform.
94   */
95  newArchEnabled?: boolean;
96  /**
97   * Override the default iOS "Deployment Target" version in the following projects:
98   *  - in CocoaPods projects,
99   *  - `PBXNativeTarget` with "com.apple.product-type.application" `productType` in the app project.
100   */
101  deploymentTarget?: string;
102
103  /**
104   * Enable [`use_frameworks!`](https://guides.cocoapods.org/syntax/podfile.html#use_frameworks_bang)
105   * in `Podfile` to use frameworks instead of static libraries for Pods.
106   *
107   * Note: You cannot use `useFrameworks` and `flipper` at the same time
108   */
109  useFrameworks?: 'static' | 'dynamic';
110
111  /**
112   * Enable [Flipper](https://fbflipper.com/) when running your app on iOS in
113   * Debug mode. Setting `true` enables the default version of Flipper, while
114   * setting a semver string will enable a specific version of Flipper you've
115   * declared in your package.json. The default for this configuration is `false`.
116   *
117   * Note: You cannot use `flipper` at the same time as `useFrameworks`, and
118   * doing so will generate an error.
119   */
120  flipper?: boolean | string;
121}
122
123/**
124 * Interface representing available configuration for Android Gradle plugin [PackagingOptions](https://developer.android.com/reference/tools/gradle-api/7.0/com/android/build/api/dsl/PackagingOptions).
125 * @platform android
126 */
127export interface PluginConfigTypeAndroidPackagingOptions {
128  /**
129   * Array of patterns for native libraries where only the first occurrence is packaged in the APK.
130   */
131  pickFirst?: string[];
132  /**
133   * Array of patterns for native libraries that should be excluded from being packaged in the APK.
134   */
135  exclude?: string[];
136  /**
137   * Array of patterns for native libraries where all occurrences are concatenated and packaged in the APK.
138   */
139  merge?: string[];
140  /**
141   * Array of patterns for native libraries that should not be stripped of debug symbols.
142   */
143  doNotStrip?: string[];
144}
145
146const schema: JSONSchemaType<PluginConfigType> = {
147  type: 'object',
148  properties: {
149    android: {
150      type: 'object',
151      properties: {
152        newArchEnabled: { type: 'boolean', nullable: true },
153        minSdkVersion: { type: 'integer', nullable: true },
154        compileSdkVersion: { type: 'integer', nullable: true },
155        targetSdkVersion: { type: 'integer', nullable: true },
156        buildToolsVersion: { type: 'string', nullable: true },
157        kotlinVersion: { type: 'string', nullable: true },
158
159        enableProguardInReleaseBuilds: { type: 'boolean', nullable: true },
160        extraProguardRules: { type: 'string', nullable: true },
161
162        flipper: {
163          type: 'string',
164          nullable: true,
165        },
166
167        packagingOptions: {
168          type: 'object',
169          properties: {
170            pickFirst: { type: 'array', items: { type: 'string' }, nullable: true },
171            exclude: { type: 'array', items: { type: 'string' }, nullable: true },
172            merge: { type: 'array', items: { type: 'string' }, nullable: true },
173            doNotStrip: { type: 'array', items: { type: 'string' }, nullable: true },
174          },
175          nullable: true,
176        },
177      },
178      nullable: true,
179    },
180    ios: {
181      type: 'object',
182      properties: {
183        newArchEnabled: { type: 'boolean', nullable: true },
184        deploymentTarget: { type: 'string', pattern: '\\d+\\.\\d+', nullable: true },
185        useFrameworks: { type: 'string', enum: ['static', 'dynamic'], nullable: true },
186
187        flipper: {
188          type: ['boolean', 'string'],
189          nullable: true,
190        },
191      },
192      nullable: true,
193    },
194  },
195};
196
197// note(Kudo): For the implementation, we check items one by one because Ajv does not well support custom error message.
198/**
199 * Checks if specified versions meets Expo minimal supported versions.
200 * Will throw error message whenever there are invalid versions.
201 *
202 * @param config The validated config passed from Ajv.
203 * @ignore
204 */
205function maybeThrowInvalidVersions(config: PluginConfigType) {
206  const checkItems = [
207    {
208      name: 'android.minSdkVersion',
209      configVersion: config.android?.minSdkVersion,
210      minimalVersion: EXPO_SDK_MINIMAL_SUPPORTED_VERSIONS.android.minSdkVersion,
211    },
212    {
213      name: 'android.compileSdkVersion',
214      configVersion: config.android?.compileSdkVersion,
215      minimalVersion: EXPO_SDK_MINIMAL_SUPPORTED_VERSIONS.android.compileSdkVersion,
216    },
217    {
218      name: 'android.targetSdkVersion',
219      configVersion: config.android?.targetSdkVersion,
220      minimalVersion: EXPO_SDK_MINIMAL_SUPPORTED_VERSIONS.android.targetSdkVersion,
221    },
222    {
223      name: 'android.kotlinVersion',
224      configVersion: config.android?.kotlinVersion,
225      minimalVersion: EXPO_SDK_MINIMAL_SUPPORTED_VERSIONS.android.kotlinVersion,
226    },
227    {
228      name: 'ios.deploymentTarget',
229      configVersion: config.ios?.deploymentTarget,
230      minimalVersion: EXPO_SDK_MINIMAL_SUPPORTED_VERSIONS.ios.deploymentTarget,
231    },
232  ];
233
234  for (const { name, configVersion, minimalVersion } of checkItems) {
235    if (
236      typeof configVersion === 'number' &&
237      typeof minimalVersion === 'number' &&
238      configVersion < minimalVersion
239    ) {
240      throw new Error(`\`${name}\` needs to be at least version ${minimalVersion}.`);
241    }
242    if (
243      typeof configVersion === 'string' &&
244      typeof minimalVersion === 'string' &&
245      semver.lt(semver.coerce(configVersion) ?? '0.0.0', semver.coerce(minimalVersion) ?? '0.0.0')
246    ) {
247      throw new Error(`\`${name}\` needs to be at least version ${minimalVersion}.`);
248    }
249  }
250}
251
252/**
253 * @ignore
254 */
255export function validateConfig(config: any): PluginConfigType {
256  const validate = new Ajv({ allowUnionTypes: true }).compile(schema);
257  if (!validate(config)) {
258    throw new Error('Invalid expo-build-properties config: ' + JSON.stringify(validate.errors));
259  }
260
261  maybeThrowInvalidVersions(config);
262
263  // explicitly block using use_frameworks and Flipper in iOS
264  // https://github.com/facebook/flipper/issues/2414
265  if (config?.ios?.flipper !== undefined && config?.ios?.useFrameworks !== undefined) {
266    throw new Error('`ios.flipper` cannot be enabled when `ios.useFrameworks` is set.');
267  }
268
269  return config;
270}
271