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 * Configuration for `expo-build-properties`
22 */
23export interface PluginConfigType {
24  android?: PluginConfigTypeAndroid;
25  ios?: PluginConfigTypeIos;
26}
27
28/**
29 * Config for Android native build properties
30 */
31export interface PluginConfigTypeAndroid {
32  /**
33   * Enable React Native new architecture mode on Android
34   */
35  newArchEnabled?: boolean;
36
37  /** Override the default `minSdkVersion` version number in `build.gradle` */
38  minSdkVersion?: number;
39
40  /** Override the default `compileSdkVersion` version number in `build.gradle` */
41  compileSdkVersion?: number;
42
43  /** Override the default `targetSdkVersion` version number in `build.gradle` */
44  targetSdkVersion?: number;
45
46  /** Override the default `buildToolsVersion` version number in `build.gradle` */
47  buildToolsVersion?: string;
48
49  /** Override the default Kotlin version when building the app */
50  kotlinVersion?: string;
51
52  /** Enable Proguard (R8) in release builds to obfuscate Java code and reduce app size */
53  enableProguardInReleaseBuilds?: boolean;
54
55  /** Append custom [Proguard rules](https://www.guardsquare.com/manual/configuration/usage) to `android/app/proguard-rules.pro` */
56  extraProguardRules?: string;
57
58  /** AGP [PackagingOptions](https://developer.android.com/reference/tools/gradle-api/7.0/com/android/build/api/dsl/PackagingOptions) */
59  packagingOptions?: PluginConfigTypeAndroidPackagingOptions;
60}
61
62/**
63 * Config for iOS native build properties
64 */
65export interface PluginConfigTypeIos {
66  /**
67   * Enable React Native new architecture mode on iOS
68   */
69  newArchEnabled?: boolean;
70
71  /**
72   * Override the default iOS *Deployment Target* version in the following projects:
73   *  - in CocoaPods projects
74   *  - `PBXNativeTarget` with `com.apple.product-type.application` productType in the app project
75   */
76  deploymentTarget?: string;
77
78  /** Enable [`use_frameworks!`](https://guides.cocoapods.org/syntax/podfile.html#use_frameworks_bang) in `Podfile` */
79  useFrameworks?: 'static' | 'dynamic';
80}
81
82/**
83 * AGP [PackagingOptions](https://developer.android.com/reference/tools/gradle-api/7.0/com/android/build/api/dsl/PackagingOptions)
84 */
85export interface PluginConfigTypeAndroidPackagingOptions {
86  /** Adds a first-pick pattern */
87  pickFirst?: string[];
88
89  /** Adds an excluded pattern */
90  exclude?: string[];
91
92  /** Adds a merge pattern */
93  merge?: string[];
94
95  /** Adds a doNotStrip pattern */
96  doNotStrip?: string[];
97}
98
99const schema: JSONSchemaType<PluginConfigType> = {
100  type: 'object',
101  properties: {
102    android: {
103      type: 'object',
104      properties: {
105        newArchEnabled: { type: 'boolean', nullable: true },
106        minSdkVersion: { type: 'integer', nullable: true },
107        compileSdkVersion: { type: 'integer', nullable: true },
108        targetSdkVersion: { type: 'integer', nullable: true },
109        buildToolsVersion: { type: 'string', nullable: true },
110        kotlinVersion: { type: 'string', nullable: true },
111
112        enableProguardInReleaseBuilds: { type: 'boolean', nullable: true },
113        extraProguardRules: { type: 'string', nullable: true },
114
115        packagingOptions: {
116          type: 'object',
117          properties: {
118            pickFirst: { type: 'array', items: { type: 'string' }, nullable: true },
119            exclude: { type: 'array', items: { type: 'string' }, nullable: true },
120            merge: { type: 'array', items: { type: 'string' }, nullable: true },
121            doNotStrip: { type: 'array', items: { type: 'string' }, nullable: true },
122          },
123          nullable: true,
124        },
125      },
126      nullable: true,
127    },
128    ios: {
129      type: 'object',
130      properties: {
131        newArchEnabled: { type: 'boolean', nullable: true },
132        deploymentTarget: { type: 'string', pattern: '\\d+\\.\\d+', nullable: true },
133        useFrameworks: { type: 'string', enum: ['static', 'dynamic'], nullable: true },
134      },
135      nullable: true,
136    },
137  },
138};
139
140/**
141 * Check versions to meet expo minimal supported versions.
142 * Will throw error message whenever there are invalid versions.
143 * For the implementation, we check items one by one because ajv does not well support custom error message.
144 *
145 * @param config the validated config passed from ajv
146 * @ignore
147 */
148function maybeThrowInvalidVersions(config: PluginConfigType) {
149  const checkItems = [
150    {
151      name: 'android.minSdkVersion',
152      configVersion: config.android?.minSdkVersion,
153      minimalVersion: EXPO_SDK_MINIMAL_SUPPORTED_VERSIONS.android.minSdkVersion,
154    },
155    {
156      name: 'android.compileSdkVersion',
157      configVersion: config.android?.compileSdkVersion,
158      minimalVersion: EXPO_SDK_MINIMAL_SUPPORTED_VERSIONS.android.compileSdkVersion,
159    },
160    {
161      name: 'android.targetSdkVersion',
162      configVersion: config.android?.targetSdkVersion,
163      minimalVersion: EXPO_SDK_MINIMAL_SUPPORTED_VERSIONS.android.targetSdkVersion,
164    },
165    {
166      name: 'android.kotlinVersion',
167      configVersion: config.android?.kotlinVersion,
168      minimalVersion: EXPO_SDK_MINIMAL_SUPPORTED_VERSIONS.android.kotlinVersion,
169    },
170    {
171      name: 'ios.deploymentTarget',
172      configVersion: config.ios?.deploymentTarget,
173      minimalVersion: EXPO_SDK_MINIMAL_SUPPORTED_VERSIONS.ios.deploymentTarget,
174    },
175  ];
176
177  for (const { name, configVersion, minimalVersion } of checkItems) {
178    if (
179      typeof configVersion === 'number' &&
180      typeof minimalVersion === 'number' &&
181      configVersion < minimalVersion
182    ) {
183      throw new Error(`\`${name}\` needs to be at least version ${minimalVersion}.`);
184    }
185    if (
186      typeof configVersion === 'string' &&
187      typeof minimalVersion === 'string' &&
188      semver.lt(semver.coerce(configVersion) ?? '0.0.0', semver.coerce(minimalVersion) ?? '0.0.0')
189    ) {
190      throw new Error(`\`${name}\` needs to be at least version ${minimalVersion}.`);
191    }
192  }
193}
194
195/**
196 * @ignore
197 */
198export function validateConfig(config: any): PluginConfigType {
199  const validate = new Ajv().compile(schema);
200  if (!validate(config)) {
201    throw new Error('Invalid expo-build-properties config: ' + JSON.stringify(validate.errors));
202  }
203
204  maybeThrowInvalidVersions(config);
205  return config;
206}
207