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/**
80 * Interface representing available configuration for iOS native build properties.
81 * @platform ios
82 */
83export interface PluginConfigTypeIos {
84  /**
85   * Enable React Native new architecture for iOS platform.
86   */
87  newArchEnabled?: boolean;
88  /**
89   * Override the default iOS "Deployment Target" version in the following projects:
90   *  - in CocoaPods projects,
91   *  - `PBXNativeTarget` with "com.apple.product-type.application" `productType` in the app project.
92   */
93  deploymentTarget?: string;
94  /**
95   * Enable [`use_frameworks!`](https://guides.cocoapods.org/syntax/podfile.html#use_frameworks_bang)
96   * in `Podfile` to use frameworks instead of static libraries for Pods.
97   */
98  useFrameworks?: 'static' | 'dynamic';
99}
100
101/**
102 * 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).
103 * @platform android
104 */
105export interface PluginConfigTypeAndroidPackagingOptions {
106  /**
107   * Array of patterns for native libraries where only the first occurrence is packaged in the APK.
108   */
109  pickFirst?: string[];
110  /**
111   * Array of patterns for native libraries that should be excluded from being packaged in the APK.
112   */
113  exclude?: string[];
114  /**
115   * Array of patterns for native libraries where all occurrences are concatenated and packaged in the APK.
116   */
117  merge?: string[];
118  /**
119   * Array of patterns for native libraries that should not be stripped of debug symbols.
120   */
121  doNotStrip?: string[];
122}
123
124const schema: JSONSchemaType<PluginConfigType> = {
125  type: 'object',
126  properties: {
127    android: {
128      type: 'object',
129      properties: {
130        newArchEnabled: { type: 'boolean', nullable: true },
131        minSdkVersion: { type: 'integer', nullable: true },
132        compileSdkVersion: { type: 'integer', nullable: true },
133        targetSdkVersion: { type: 'integer', nullable: true },
134        buildToolsVersion: { type: 'string', nullable: true },
135        kotlinVersion: { type: 'string', nullable: true },
136
137        enableProguardInReleaseBuilds: { type: 'boolean', nullable: true },
138        extraProguardRules: { type: 'string', nullable: true },
139
140        packagingOptions: {
141          type: 'object',
142          properties: {
143            pickFirst: { type: 'array', items: { type: 'string' }, nullable: true },
144            exclude: { type: 'array', items: { type: 'string' }, nullable: true },
145            merge: { type: 'array', items: { type: 'string' }, nullable: true },
146            doNotStrip: { type: 'array', items: { type: 'string' }, nullable: true },
147          },
148          nullable: true,
149        },
150      },
151      nullable: true,
152    },
153    ios: {
154      type: 'object',
155      properties: {
156        newArchEnabled: { type: 'boolean', nullable: true },
157        deploymentTarget: { type: 'string', pattern: '\\d+\\.\\d+', nullable: true },
158        useFrameworks: { type: 'string', enum: ['static', 'dynamic'], nullable: true },
159      },
160      nullable: true,
161    },
162  },
163};
164
165// note(Kudo): For the implementation, we check items one by one because Ajv does not well support custom error message.
166/**
167 * Checks if specified versions meets Expo minimal supported versions.
168 * Will throw error message whenever there are invalid versions.
169 *
170 * @param config The validated config passed from Ajv.
171 * @ignore
172 */
173function maybeThrowInvalidVersions(config: PluginConfigType) {
174  const checkItems = [
175    {
176      name: 'android.minSdkVersion',
177      configVersion: config.android?.minSdkVersion,
178      minimalVersion: EXPO_SDK_MINIMAL_SUPPORTED_VERSIONS.android.minSdkVersion,
179    },
180    {
181      name: 'android.compileSdkVersion',
182      configVersion: config.android?.compileSdkVersion,
183      minimalVersion: EXPO_SDK_MINIMAL_SUPPORTED_VERSIONS.android.compileSdkVersion,
184    },
185    {
186      name: 'android.targetSdkVersion',
187      configVersion: config.android?.targetSdkVersion,
188      minimalVersion: EXPO_SDK_MINIMAL_SUPPORTED_VERSIONS.android.targetSdkVersion,
189    },
190    {
191      name: 'android.kotlinVersion',
192      configVersion: config.android?.kotlinVersion,
193      minimalVersion: EXPO_SDK_MINIMAL_SUPPORTED_VERSIONS.android.kotlinVersion,
194    },
195    {
196      name: 'ios.deploymentTarget',
197      configVersion: config.ios?.deploymentTarget,
198      minimalVersion: EXPO_SDK_MINIMAL_SUPPORTED_VERSIONS.ios.deploymentTarget,
199    },
200  ];
201
202  for (const { name, configVersion, minimalVersion } of checkItems) {
203    if (
204      typeof configVersion === 'number' &&
205      typeof minimalVersion === 'number' &&
206      configVersion < minimalVersion
207    ) {
208      throw new Error(`\`${name}\` needs to be at least version ${minimalVersion}.`);
209    }
210    if (
211      typeof configVersion === 'string' &&
212      typeof minimalVersion === 'string' &&
213      semver.lt(semver.coerce(configVersion) ?? '0.0.0', semver.coerce(minimalVersion) ?? '0.0.0')
214    ) {
215      throw new Error(`\`${name}\` needs to be at least version ${minimalVersion}.`);
216    }
217  }
218}
219
220/**
221 * @ignore
222 */
223export function validateConfig(config: any): PluginConfigType {
224  const validate = new Ajv().compile(schema);
225  if (!validate(config)) {
226    throw new Error('Invalid expo-build-properties config: ' + JSON.stringify(validate.errors));
227  }
228
229  maybeThrowInvalidVersions(config);
230  return config;
231}
232