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