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