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