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