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 Network Inspector.
94   *
95   * @default true
96   */
97  networkInspector?: boolean;
98
99  /**
100   * Add extra maven repositories to all gradle projects.
101   *
102   * This acts like to add the following code to **android/build.gradle**:
103   * ```groovy
104   * allprojects {
105   *   repositories {
106   *     maven {
107   *       url [THE_EXTRA_MAVEN_REPOSITORY]
108   *     }
109   *   }
110   * }
111   * ```
112   *
113   * @hide For the implementation details,
114   * this property is actually handled by `expo-modules-autolinking` but not the config-plugins inside expo-build-properties.
115   */
116  extraMavenRepos?: string[];
117  /**
118   * Indicates whether the app intends to use cleartext network traffic.
119   *
120   * @default false
121   *
122   * @see [Android documentation](https://developer.android.com/guide/topics/manifest/application-element#usesCleartextTraffic)
123   */
124  usesCleartextTraffic?: boolean;
125}
126
127/**
128 * Interface representing available configuration for iOS native build properties.
129 * @platform ios
130 */
131export interface PluginConfigTypeIos {
132  /**
133   * Enable React Native new architecture for iOS platform.
134   */
135  newArchEnabled?: boolean;
136  /**
137   * Override the default iOS "Deployment Target" version in the following projects:
138   *  - in CocoaPods projects,
139   *  - `PBXNativeTarget` with "com.apple.product-type.application" `productType` in the app project.
140   */
141  deploymentTarget?: string;
142
143  /**
144   * Enable [`use_frameworks!`](https://guides.cocoapods.org/syntax/podfile.html#use_frameworks_bang)
145   * in `Podfile` to use frameworks instead of static libraries for Pods.
146   *
147   * > You cannot use `useFrameworks` and `flipper` at the same time, and
148   * doing so will generate an error.
149   */
150  useFrameworks?: 'static' | 'dynamic';
151
152  /**
153   * Enable [Flipper](https://fbflipper.com/) when running your app on iOS in
154   * Debug mode. Setting `true` enables the default version of Flipper, while
155   * setting a semver string will enable a specific version of Flipper you've
156   * declared in your **package.json**. The default for this configuration is `false`.
157   *
158   * > You cannot use `flipper` at the same time as `useFrameworks`, and
159   * doing so will generate an error.
160   */
161  flipper?: boolean | string;
162
163  /**
164   * Enable the Network Inspector.
165   *
166   * @default true
167   */
168  networkInspector?: boolean;
169
170  /**
171   * Add extra CocoaPods dependencies for all targets.
172   *
173   * This acts like to add the following code to **ios/Podfile**:
174   * ```
175   * pod '[EXTRA_POD_NAME]', '~> [EXTRA_POD_VERSION]'
176   * # e.g.
177   * pod 'Protobuf', '~> 3.14.0'
178   * ```
179   *
180   * @hide For the implementation details,
181   * this property is actually handled by `expo-modules-autolinking` but not the config-plugins inside expo-build-properties.
182   */
183  extraPods?: ExtraIosPodDependency[];
184}
185
186/**
187 * Interface representing extra CocoaPods dependency.
188 * @see [Podfile syntax reference](https://guides.cocoapods.org/syntax/podfile.html#pod)
189 * @platform ios
190 */
191export interface ExtraIosPodDependency {
192  /**
193   * Name of the pod.
194   */
195  name: string;
196  /**
197   * Version of the pod.
198   * CocoaPods supports various [versioning options](https://guides.cocoapods.org/using/the-podfile.html#pod).
199   * @example `~> 0.1.2`
200   */
201  version?: string;
202  /**
203   * Build configurations for which the pod should be installed.
204   * @example `['Debug', 'Release']`
205   */
206  configurations?: string[];
207  /**
208   * Whether this pod should use modular headers.
209   */
210  modular_headers?: boolean;
211  /**
212   * Custom source to search for this dependency.
213   * @example `https://github.com/CocoaPods/Specs.git`
214   */
215  source?: string;
216  /**
217   * Custom local filesystem path to add the dependency.
218   * @example `~/Documents/AFNetworking`
219   */
220  path?: string;
221  /**
222   * Custom podspec path.
223   * @example `https://example.com/JSONKit.podspec`
224   */
225  podspec?: string;
226  /**
227   * Test specs can be optionally included via the :testspecs option. By default, none of a Pod's test specs are included.
228   * @example `['UnitTests', 'SomeOtherTests']`
229   */
230  testspecs?: string[];
231  /**
232   * Use the bleeding edge version of a Pod.
233   *
234   * @example
235   * ```
236   * { "name": "AFNetworking", "git": "https://github.com/gowalla/AFNetworking.git", "tag": "0.7.0" }
237   * ```
238   *
239   * This acts like to add this pod dependency statement:
240   * ```
241   * pod 'AFNetworking', :git => 'https://github.com/gowalla/AFNetworking.git', :tag => '0.7.0'
242   * ```
243   */
244  git?: string;
245  /**
246   * The git branch to fetch. See the {@link git} property for more information.
247   */
248  branch?: string;
249  /**
250   * The git tag to fetch. See the {@link git} property for more information.
251   */
252  tag?: string;
253  /**
254   * The git commit to fetch. See the {@link git} property for more information.
255   */
256  commit?: string;
257}
258
259/**
260 * 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).
261 * @platform android
262 */
263export interface PluginConfigTypeAndroidPackagingOptions {
264  /**
265   * Array of patterns for native libraries where only the first occurrence is packaged in the APK.
266   */
267  pickFirst?: string[];
268  /**
269   * Array of patterns for native libraries that should be excluded from being packaged in the APK.
270   */
271  exclude?: string[];
272  /**
273   * Array of patterns for native libraries where all occurrences are concatenated and packaged in the APK.
274   */
275  merge?: string[];
276  /**
277   * Array of patterns for native libraries that should not be stripped of debug symbols.
278   */
279  doNotStrip?: string[];
280}
281
282const schema: JSONSchemaType<PluginConfigType> = {
283  type: 'object',
284  properties: {
285    android: {
286      type: 'object',
287      properties: {
288        newArchEnabled: { type: 'boolean', nullable: true },
289        minSdkVersion: { type: 'integer', nullable: true },
290        compileSdkVersion: { type: 'integer', nullable: true },
291        targetSdkVersion: { type: 'integer', nullable: true },
292        buildToolsVersion: { type: 'string', nullable: true },
293        kotlinVersion: { type: 'string', nullable: true },
294
295        enableProguardInReleaseBuilds: { type: 'boolean', nullable: true },
296        enableShrinkResourcesInReleaseBuilds: { type: 'boolean', nullable: true },
297        extraProguardRules: { type: 'string', nullable: true },
298
299        flipper: {
300          type: 'string',
301          nullable: true,
302        },
303
304        packagingOptions: {
305          type: 'object',
306          properties: {
307            pickFirst: { type: 'array', items: { type: 'string' }, nullable: true },
308            exclude: { type: 'array', items: { type: 'string' }, nullable: true },
309            merge: { type: 'array', items: { type: 'string' }, nullable: true },
310            doNotStrip: { type: 'array', items: { type: 'string' }, nullable: true },
311          },
312          nullable: true,
313        },
314
315        networkInspector: { type: 'boolean', nullable: true },
316
317        extraMavenRepos: { type: 'array', items: { type: 'string' }, nullable: true },
318
319        usesCleartextTraffic: { type: 'boolean', nullable: true },
320      },
321      nullable: true,
322    },
323    ios: {
324      type: 'object',
325      properties: {
326        newArchEnabled: { type: 'boolean', nullable: true },
327        deploymentTarget: { type: 'string', pattern: '\\d+\\.\\d+', nullable: true },
328        useFrameworks: { type: 'string', enum: ['static', 'dynamic'], nullable: true },
329
330        flipper: {
331          type: ['boolean', 'string'],
332          nullable: true,
333        },
334
335        networkInspector: { type: 'boolean', nullable: true },
336
337        extraPods: {
338          type: 'array',
339          items: {
340            type: 'object',
341            required: ['name'],
342            properties: {
343              name: { type: 'string' },
344              version: { type: 'string', nullable: true },
345              configurations: { type: 'array', items: { type: 'string' }, nullable: true },
346              modular_headers: { type: 'boolean', nullable: true },
347              source: { type: 'string', nullable: true },
348              path: { type: 'string', nullable: true },
349              podspec: { type: 'string', nullable: true },
350              testspecs: { type: 'array', items: { type: 'string' }, nullable: true },
351              git: { type: 'string', nullable: true },
352              branch: { type: 'string', nullable: true },
353              tag: { type: 'string', nullable: true },
354              commit: { type: 'string', nullable: true },
355            },
356          },
357          nullable: true,
358        },
359      },
360      nullable: true,
361    },
362  },
363};
364
365// note(Kudo): For the implementation, we check items one by one because Ajv does not well support custom error message.
366/**
367 * Checks if specified versions meets Expo minimal supported versions.
368 * Will throw error message whenever there are invalid versions.
369 *
370 * @param config The validated config passed from Ajv.
371 * @ignore
372 */
373function maybeThrowInvalidVersions(config: PluginConfigType) {
374  const checkItems = [
375    {
376      name: 'android.minSdkVersion',
377      configVersion: config.android?.minSdkVersion,
378      minimalVersion: EXPO_SDK_MINIMAL_SUPPORTED_VERSIONS.android.minSdkVersion,
379    },
380    {
381      name: 'android.compileSdkVersion',
382      configVersion: config.android?.compileSdkVersion,
383      minimalVersion: EXPO_SDK_MINIMAL_SUPPORTED_VERSIONS.android.compileSdkVersion,
384    },
385    {
386      name: 'android.targetSdkVersion',
387      configVersion: config.android?.targetSdkVersion,
388      minimalVersion: EXPO_SDK_MINIMAL_SUPPORTED_VERSIONS.android.targetSdkVersion,
389    },
390    {
391      name: 'android.kotlinVersion',
392      configVersion: config.android?.kotlinVersion,
393      minimalVersion: EXPO_SDK_MINIMAL_SUPPORTED_VERSIONS.android.kotlinVersion,
394    },
395    {
396      name: 'ios.deploymentTarget',
397      configVersion: config.ios?.deploymentTarget,
398      minimalVersion: EXPO_SDK_MINIMAL_SUPPORTED_VERSIONS.ios.deploymentTarget,
399    },
400  ];
401
402  for (const { name, configVersion, minimalVersion } of checkItems) {
403    if (
404      typeof configVersion === 'number' &&
405      typeof minimalVersion === 'number' &&
406      configVersion < minimalVersion
407    ) {
408      throw new Error(`\`${name}\` needs to be at least version ${minimalVersion}.`);
409    }
410    if (
411      typeof configVersion === 'string' &&
412      typeof minimalVersion === 'string' &&
413      semver.lt(semver.coerce(configVersion) ?? '0.0.0', semver.coerce(minimalVersion) ?? '0.0.0')
414    ) {
415      throw new Error(`\`${name}\` needs to be at least version ${minimalVersion}.`);
416    }
417  }
418}
419
420/**
421 * @ignore
422 */
423export function validateConfig(config: any): PluginConfigType {
424  const validate = new Ajv({ allowUnionTypes: true }).compile(schema);
425  if (!validate(config)) {
426    throw new Error('Invalid expo-build-properties config: ' + JSON.stringify(validate.errors));
427  }
428
429  maybeThrowInvalidVersions(config);
430
431  // explicitly block using use_frameworks and Flipper in iOS
432  // https://github.com/facebook/flipper/issues/2414
433  if (Boolean(config.ios?.flipper) && config.ios?.useFrameworks !== undefined) {
434    throw new Error('`ios.flipper` cannot be enabled when `ios.useFrameworks` is set.');
435  }
436
437  if (
438    config.android?.enableShrinkResourcesInReleaseBuilds === true &&
439    config.android?.enableProguardInReleaseBuilds !== true
440  ) {
441    throw new Error(
442      '`android.enableShrinkResourcesInReleaseBuilds` requires `android.enableProguardInReleaseBuilds` to be enabled.'
443    );
444  }
445
446  return config;
447}
448