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   * By default, Flipper is enabled with the version that comes bundled with `react-native`.
80   *
81   * Use this to change the [Flipper](https://fbflipper.com/) version when
82   * running your app on Android. You can set the `flipper` property to a
83   * semver string and specify an alternate Flipper version.
84   */
85  flipper?: string;
86}
87
88/**
89 * Interface representing available configuration for iOS native build properties.
90 * @platform ios
91 */
92export interface PluginConfigTypeIos {
93  /**
94   * Enable React Native new architecture for iOS platform.
95   */
96  newArchEnabled?: boolean;
97  /**
98   * Override the default iOS "Deployment Target" version in the following projects:
99   *  - in CocoaPods projects,
100   *  - `PBXNativeTarget` with "com.apple.product-type.application" `productType` in the app project.
101   */
102  deploymentTarget?: string;
103
104  /**
105   * Enable [`use_frameworks!`](https://guides.cocoapods.org/syntax/podfile.html#use_frameworks_bang)
106   * in `Podfile` to use frameworks instead of static libraries for Pods.
107   *
108   * > You cannot use `useFrameworks` and `flipper` at the same time , and
109   * doing so will generate an error.
110   */
111  useFrameworks?: 'static' | 'dynamic';
112
113  /**
114   * Enable [Flipper](https://fbflipper.com/) when running your app on iOS in
115   * Debug mode. Setting `true` enables the default version of Flipper, while
116   * setting a semver string will enable a specific version of Flipper you've
117   * declared in your **package.json**. The default for this configuration is `false`.
118   *
119   * > You cannot use `flipper` at the same time as `useFrameworks`, and
120   * doing so will generate an error.
121   */
122  flipper?: boolean | string;
123}
124
125/**
126 * 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).
127 * @platform android
128 */
129export interface PluginConfigTypeAndroidPackagingOptions {
130  /**
131   * Array of patterns for native libraries where only the first occurrence is packaged in the APK.
132   */
133  pickFirst?: string[];
134  /**
135   * Array of patterns for native libraries that should be excluded from being packaged in the APK.
136   */
137  exclude?: string[];
138  /**
139   * Array of patterns for native libraries where all occurrences are concatenated and packaged in the APK.
140   */
141  merge?: string[];
142  /**
143   * Array of patterns for native libraries that should not be stripped of debug symbols.
144   */
145  doNotStrip?: string[];
146}
147
148const schema: JSONSchemaType<PluginConfigType> = {
149  type: 'object',
150  properties: {
151    android: {
152      type: 'object',
153      properties: {
154        newArchEnabled: { type: 'boolean', nullable: true },
155        minSdkVersion: { type: 'integer', nullable: true },
156        compileSdkVersion: { type: 'integer', nullable: true },
157        targetSdkVersion: { type: 'integer', nullable: true },
158        buildToolsVersion: { type: 'string', nullable: true },
159        kotlinVersion: { type: 'string', nullable: true },
160
161        enableProguardInReleaseBuilds: { type: 'boolean', nullable: true },
162        extraProguardRules: { type: 'string', nullable: true },
163
164        flipper: {
165          type: 'string',
166          nullable: true,
167        },
168
169        packagingOptions: {
170          type: 'object',
171          properties: {
172            pickFirst: { type: 'array', items: { type: 'string' }, nullable: true },
173            exclude: { type: 'array', items: { type: 'string' }, nullable: true },
174            merge: { type: 'array', items: { type: 'string' }, nullable: true },
175            doNotStrip: { type: 'array', items: { type: 'string' }, nullable: true },
176          },
177          nullable: true,
178        },
179      },
180      nullable: true,
181    },
182    ios: {
183      type: 'object',
184      properties: {
185        newArchEnabled: { type: 'boolean', nullable: true },
186        deploymentTarget: { type: 'string', pattern: '\\d+\\.\\d+', nullable: true },
187        useFrameworks: { type: 'string', enum: ['static', 'dynamic'], nullable: true },
188
189        flipper: {
190          type: ['boolean', 'string'],
191          nullable: true,
192        },
193      },
194      nullable: true,
195    },
196  },
197};
198
199// note(Kudo): For the implementation, we check items one by one because Ajv does not well support custom error message.
200/**
201 * Checks if specified versions meets Expo minimal supported versions.
202 * Will throw error message whenever there are invalid versions.
203 *
204 * @param config The validated config passed from Ajv.
205 * @ignore
206 */
207function maybeThrowInvalidVersions(config: PluginConfigType) {
208  const checkItems = [
209    {
210      name: 'android.minSdkVersion',
211      configVersion: config.android?.minSdkVersion,
212      minimalVersion: EXPO_SDK_MINIMAL_SUPPORTED_VERSIONS.android.minSdkVersion,
213    },
214    {
215      name: 'android.compileSdkVersion',
216      configVersion: config.android?.compileSdkVersion,
217      minimalVersion: EXPO_SDK_MINIMAL_SUPPORTED_VERSIONS.android.compileSdkVersion,
218    },
219    {
220      name: 'android.targetSdkVersion',
221      configVersion: config.android?.targetSdkVersion,
222      minimalVersion: EXPO_SDK_MINIMAL_SUPPORTED_VERSIONS.android.targetSdkVersion,
223    },
224    {
225      name: 'android.kotlinVersion',
226      configVersion: config.android?.kotlinVersion,
227      minimalVersion: EXPO_SDK_MINIMAL_SUPPORTED_VERSIONS.android.kotlinVersion,
228    },
229    {
230      name: 'ios.deploymentTarget',
231      configVersion: config.ios?.deploymentTarget,
232      minimalVersion: EXPO_SDK_MINIMAL_SUPPORTED_VERSIONS.ios.deploymentTarget,
233    },
234  ];
235
236  for (const { name, configVersion, minimalVersion } of checkItems) {
237    if (
238      typeof configVersion === 'number' &&
239      typeof minimalVersion === 'number' &&
240      configVersion < minimalVersion
241    ) {
242      throw new Error(`\`${name}\` needs to be at least version ${minimalVersion}.`);
243    }
244    if (
245      typeof configVersion === 'string' &&
246      typeof minimalVersion === 'string' &&
247      semver.lt(semver.coerce(configVersion) ?? '0.0.0', semver.coerce(minimalVersion) ?? '0.0.0')
248    ) {
249      throw new Error(`\`${name}\` needs to be at least version ${minimalVersion}.`);
250    }
251  }
252}
253
254/**
255 * @ignore
256 */
257export function validateConfig(config: any): PluginConfigType {
258  const validate = new Ajv({ allowUnionTypes: true }).compile(schema);
259  if (!validate(config)) {
260    throw new Error('Invalid expo-build-properties config: ' + JSON.stringify(validate.errors));
261  }
262
263  maybeThrowInvalidVersions(config);
264
265  // explicitly block using use_frameworks and Flipper in iOS
266  // https://github.com/facebook/flipper/issues/2414
267  if (config?.ios?.flipper !== undefined && config?.ios?.useFrameworks !== undefined) {
268    throw new Error('`ios.flipper` cannot be enabled when `ios.useFrameworks` is set.');
269  }
270
271  return config;
272}
273