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