1import { 2 compileModsAsync, 3 evalModsAsync, 4 ExportedConfig, 5 IOSConfig, 6 withGradleProperties, 7 XML, 8} from '@expo/config-plugins'; 9import JsonFile from '@expo/json-file'; 10import plist from '@expo/plist'; 11import fs from 'fs-extra'; 12import { vol } from 'memfs'; 13import * as path from 'path'; 14import xcode from 'xcode'; 15 16import { 17 withAndroidExpoPlugins, 18 withIosExpoPlugins, 19 withVersionedExpoSDKPlugins, 20} from '../withDefaultPlugins'; 21import rnFixture from './fixtures/react-native-project'; 22import { getDirFromFS } from './getDirFromFS'; 23 24const { withOrientation } = IOSConfig.Orientation; 25 26const { readXMLAsync } = XML; 27const fsReal = jest.requireActual('fs') as typeof fs; 28 29jest.mock('fs'); 30// Weird issues with Android Icon module make it hard to mock test. 31jest.mock('../icons/withAndroidIcons', () => { 32 return { 33 withAndroidIcons(config) { 34 return config; 35 }, 36 setIconAsync() {}, 37 }; 38}); 39const NotificationsPlugin = require('../unversioned/expo-notifications/withAndroidNotifications'); 40NotificationsPlugin.withNotificationIcons = jest.fn((config) => config); 41 42function getLargeConfig(): ExportedConfig { 43 // A very extensive Expo Config. 44 return { 45 name: 'my cool app', 46 slug: 'mycoolapp', 47 description: 'my app is great because it uses expo', 48 // owner?: string; 49 // privacy?: 'public' | 'unlisted' | 'hidden'; 50 // sdkVersion?: string; 51 // runtimeVersion?: string; 52 splash: { 53 backgroundColor: '#ff00ff', 54 }, 55 version: '1.0.0', 56 platforms: ['android', 'ios', 'web'], 57 githubUrl: 'https://github.com/expo/expo', 58 orientation: 'default', 59 userInterfaceStyle: 'dark', 60 backgroundColor: 'orange', 61 primaryColor: '#fff000', 62 // icon: './icons/icon.png', 63 notification: { 64 icon: './icons/notification-icon.png', 65 color: 'green', 66 iosDisplayInForeground: true, 67 androidMode: 'collapse', 68 androidCollapsedTitle: '#{unread_notifications} new interactions', 69 }, 70 androidStatusBar: { 71 barStyle: 'light-content', 72 backgroundColor: '#000FFF', 73 hidden: false, 74 translucent: true, 75 }, 76 androidNavigationBar: { 77 visible: 'sticky-immersive', 78 barStyle: 'dark-content', 79 80 backgroundColor: '#ff0000', 81 }, 82 developmentClient: { 83 silentLaunch: true, 84 }, 85 scheme: 'my-app-redirect', 86 packagerOpts: { 87 extraThing: true, 88 }, 89 updates: { 90 enabled: true, 91 checkAutomatically: 'ON_ERROR_RECOVERY', 92 fallbackToCacheTimeout: 650, 93 }, 94 locales: { 95 en: './locales/en-US.json', 96 es: { foo: 'el bar' }, 97 }, 98 facebookAppId: '1234567890', 99 facebookAutoInitEnabled: true, 100 facebookAutoLogAppEventsEnabled: true, 101 facebookAdvertiserIDCollectionEnabled: true, 102 facebookDisplayName: 'my-fb-test-app', 103 facebookScheme: 'fb1234567890', 104 ios: { 105 bundleIdentifier: 'com.bacon.tester.expoapp', 106 buildNumber: '6.5.0', 107 backgroundColor: '#ff0000', 108 appStoreUrl: 'https://itunes.apple.com/us/app/pillar-valley/id1336398804?ls=1&mt=8', 109 config: { 110 branch: { 111 apiKey: 'MY_BRANCH_KEY', 112 }, 113 usesNonExemptEncryption: true, 114 googleMapsApiKey: 'TEST_googleMapsApiKey', 115 googleMobileAdsAppId: 'TEST_googleMobileAdsAppId', 116 googleMobileAdsAutoInit: true, 117 }, 118 googleServicesFile: './config/GoogleService-Info.plist', 119 supportsTablet: true, 120 isTabletOnly: false, 121 requireFullScreen: true, 122 userInterfaceStyle: 'automatic', 123 infoPlist: { bar: { val: ['foo'] } }, 124 entitlements: { foo: 'bar' }, 125 associatedDomains: ['applinks:https://pillarvalley.netlify.app'], 126 usesIcloudStorage: true, 127 usesAppleSignIn: true, 128 accessesContactNotes: true, 129 }, 130 android: { 131 package: 'com.bacon.tester.expoapp', 132 versionCode: 6, 133 backgroundColor: '#ff0000', 134 userInterfaceStyle: 'light', 135 adaptiveIcon: { 136 foregroundImage: './icons/foreground.png', 137 backgroundImage: './icons/background.png', 138 }, 139 splash: { 140 backgroundColor: '#ff00ff', 141 dark: { 142 backgroundColor: '#00ffff', 143 }, 144 }, 145 blockedPermissions: [ 146 'android.permission.RECORD_AUDIO', 147 'android.permission.ACCESS_FINE_LOCATION', 148 ], 149 permissions: [ 150 'CAMERA', 151 'com.sec.android.provider.badge.permission.WRITE', 152 'android.permission.RECORD_AUDIO', 153 ], 154 googleServicesFile: './config/google-services.json', 155 config: { 156 branch: { 157 apiKey: 'MY_BRANCH_ANDROID_KEY', 158 }, 159 googleMaps: { 160 apiKey: 'MY_GOOGLE_MAPS_ANDROID_KEY', 161 }, 162 googleMobileAdsAppId: 'MY_GOOGLE_MOBILE_ADS_APP_ID', 163 googleMobileAdsAutoInit: true, 164 }, 165 intentFilters: [ 166 { 167 autoVerify: true, 168 action: 'VIEW', 169 data: { 170 scheme: 'https', 171 host: '*.expo.dev', 172 }, 173 category: ['BROWSABLE', 'DEFAULT'], 174 }, 175 ], 176 allowBackup: true, 177 softwareKeyboardLayoutMode: 'pan', 178 }, 179 _internal: { projectRoot: '/app' }, 180 mods: null, 181 }; 182} 183 184function getPrebuildConfig() { 185 let config = { ...getLargeConfig() }; 186 config = withVersionedExpoSDKPlugins(config, { expoUsername: 'bacon' }); 187 188 config = withIosExpoPlugins(config, { 189 bundleIdentifier: 'com.bacon.todo', 190 }); 191 config = withAndroidExpoPlugins(config, { 192 package: 'com.bacon.todo', 193 }); 194 return config; 195} 196describe(evalModsAsync, () => { 197 it(`runs with no core mods`, async () => { 198 let config: ExportedConfig = { 199 name: 'app', 200 slug: '', 201 }; 202 config = await evalModsAsync(config, { projectRoot: '/' }); 203 expect(config.ios).toBeUndefined(); 204 }); 205}); 206 207describe('built-in plugins', () => { 208 const projectRoot = '/app'; 209 const iconPath = path.resolve(__dirname, './fixtures/icon.png'); 210 const icon = fsReal.readFileSync(iconPath) as any; 211 const googleServiceInfoFixture = fsReal.readFileSync( 212 path.resolve(__dirname, './fixtures/GoogleService-Info.plist'), 213 'utf8' 214 ) as any; 215 216 const originalWarn = console.warn; 217 218 beforeEach(async () => { 219 console.warn = jest.fn(); 220 // Trick XDL Info.plist reading 221 Object.defineProperty(process, 'platform', { 222 value: 'not-darwin', 223 }); 224 vol.fromJSON( 225 { 226 // Required to link react-native-maps 227 './node_modules/react-native-maps/package.json': JSON.stringify({}), 228 // App files 229 ...rnFixture, 230 'config/GoogleService-Info.plist': googleServiceInfoFixture, 231 'config/google-services.json': '{}', 232 './icons/foreground.png': icon, 233 './icons/background.png': icon, 234 './icons/notification-icon.png': icon, 235 './icons/ios-icon.png': icon, 236 'locales/en-US.json': JSON.stringify({ foo: 'uhh bar', fallback: 'fallback' }, null, 2), 237 }, 238 projectRoot 239 ); 240 }); 241 242 afterEach(() => { 243 vol.reset(); 244 console.warn = originalWarn; 245 }); 246 247 // Ensure helpful error messages are thrown 248 it(`fails to locate the project name in an invalid project`, async () => { 249 const config = withOrientation({ 250 name: 'app', 251 slug: '', 252 ios: {}, 253 }); 254 await expect(compileModsAsync(config, { projectRoot: '/invalid' })).rejects.toThrow( 255 'Failed to locate Info.plist files relative' 256 ); 257 }); 258 259 it(`skips platforms`, async () => { 260 const config = withOrientation({ 261 name: 'app', 262 slug: '', 263 ios: {}, 264 }); 265 266 // should throw if the platform isn't skipped 267 await compileModsAsync(config, { projectRoot: '/invalid', platforms: ['android'] }); 268 }); 269 270 it('allows conflicts with info.plist overrides', async () => { 271 let config: ExportedConfig = { 272 name: 'app', 273 slug: '', 274 _internal: { projectRoot: '.' }, 275 ios: { 276 config: { 277 usesNonExemptEncryption: false, 278 }, 279 infoPlist: { 280 ITSAppUsesNonExemptEncryption: true, 281 }, 282 }, 283 }; 284 285 config = withIosExpoPlugins(config, { 286 bundleIdentifier: 'com.bacon.todo', 287 }); 288 // Apply mod 289 config = await compileModsAsync(config, { projectRoot: '/app' }); 290 // This should be false because ios.config.usesNonExemptEncryption is used in favor of ios.infoPlist.ITSAppUsesNonExemptEncryption 291 expect(config.ios?.infoPlist?.ITSAppUsesNonExemptEncryption).toBe(true); 292 }); 293 294 it('sends a valid modRequest', async () => { 295 let config = getPrebuildConfig(); 296 297 let modRequest; 298 config = withGradleProperties(config, (config) => { 299 modRequest = config.modRequest; 300 return config; 301 }); 302 // Apply mod 303 await compileModsAsync(config, { introspect: true, projectRoot: '/app' }); 304 305 expect(modRequest).toStrictEqual({ 306 introspect: true, 307 modName: 'gradleProperties', 308 platform: 'android', 309 platformProjectRoot: '/app/android', 310 projectName: undefined, 311 projectRoot: '/app', 312 }); 313 }); 314 it('compiles mods', async () => { 315 let config = getPrebuildConfig(); 316 // Apply mod 317 config = await compileModsAsync(config, { projectRoot: '/app' }); 318 319 // App config should have been modified 320 expect(config.name).toBe('my cool app'); 321 expect(config.ios?.infoPlist).toBeDefined(); 322 expect(config.ios?.entitlements).toBeDefined(); 323 324 // Google Sign In 325 expect( 326 config.ios?.infoPlist?.CFBundleURLTypes?.find(({ CFBundleURLSchemes }) => 327 CFBundleURLSchemes.includes('com.googleusercontent.apps.1234567890123-abcdef') 328 ) 329 ).toBeDefined(); 330 // Branch 331 expect(config.ios?.infoPlist?.branch_key?.live).toBe('MY_BRANCH_KEY'); 332 333 // Mods should all be functions 334 expect(Object.values(config.mods!.ios!).every((value) => typeof value === 'function')).toBe( 335 true 336 ); 337 338 delete config.mods; 339 340 // Shape 341 expect(config).toMatchSnapshot(); 342 343 // Test the written files... 344 const after = getDirFromFS(vol.toJSON(), projectRoot); 345 346 expect(Object.keys(after)).toEqual([ 347 'node_modules/react-native-maps/package.json', 348 'ios/.xcode.env', 349 'ios/HelloWorld/AppDelegate.h', 350 'ios/HelloWorld/AppDelegate.mm', 351 'ios/HelloWorld/Images.xcassets/AppIcon.appiconset/Contents.json', 352 'ios/HelloWorld/Images.xcassets/Contents.json', 353 'ios/HelloWorld/Images.xcassets/SplashScreenBackground.imageset/image.png', 354 'ios/HelloWorld/Images.xcassets/SplashScreenBackground.imageset/Contents.json', 355 'ios/HelloWorld/Info.plist', 356 'ios/HelloWorld/SplashScreen.storyboard', 357 'ios/HelloWorld/Supporting/Expo.plist', 358 'ios/HelloWorld/Supporting/en.lproj/InfoPlist.strings', 359 'ios/HelloWorld/Supporting/es.lproj/InfoPlist.strings', 360 'ios/HelloWorld/main.m', 361 'ios/HelloWorld/GoogleService-Info.plist', 362 'ios/HelloWorld/noop-file.swift', 363 'ios/HelloWorld/HelloWorld-Bridging-Header.h', 364 'ios/HelloWorld/mycoolapp.entitlements', 365 'ios/HelloWorld.xcodeproj/project.pbxproj', 366 'ios/HelloWorld.xcodeproj/project.xcworkspace/contents.xcworkspacedata', 367 'ios/HelloWorld.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist', 368 'ios/HelloWorld.xcodeproj/xcshareddata/xcschemes/HelloWorld.xcscheme', 369 'ios/HelloWorld.xcworkspace/contents.xcworkspacedata', 370 'ios/HelloWorld.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist', 371 'ios/Podfile', 372 'ios/Podfile.properties.json', 373 'ios/gitignore', 374 'android/app/build.gradle', 375 'android/app/debug.keystore', 376 'android/app/proguard-rules.pro', 377 'android/app/src/debug/AndroidManifest.xml', 378 'android/app/src/debug/java/com/bacon/todo/ReactNativeFlipper.java', 379 'android/app/src/main/AndroidManifest.xml', 380 'android/app/src/main/java/com/bacon/todo/MainActivity.java', 381 'android/app/src/main/java/com/bacon/todo/MainApplication.java', 382 'android/app/src/main/res/drawable/rn_edit_text_material.xml', 383 'android/app/src/main/res/drawable/splashscreen.xml', 384 'android/app/src/main/res/values/colors.xml', 385 'android/app/src/main/res/values/strings.xml', 386 'android/app/src/main/res/values/styles.xml', 387 'android/app/src/main/res/values-night/colors.xml', 388 'android/app/src/release/java/com/bacon/todo/ReactNativeFlipper.java', 389 'android/app/google-services.json', 390 'android/build.gradle', 391 'android/gitignore', 392 'android/gradle/wrapper/gradle-wrapper.jar', 393 'android/gradle/wrapper/gradle-wrapper.properties', 394 'android/gradle.properties', 395 'android/gradlew', 396 'android/gradlew.bat', 397 'android/settings.gradle', 398 'config/GoogleService-Info.plist', 399 'config/google-services.json', 400 'locales/en-US.json', 401 ]); 402 403 expect(after['ios/HelloWorld/mycoolapp.entitlements']).toMatch( 404 'com.apple.developer.associated-domains' 405 ); 406 407 expect(after['ios/HelloWorld/Info.plist']).toMatch(/com.bacon.todo/); 408 expect(after['ios/HelloWorld/Supporting/en.lproj/InfoPlist.strings']).toMatch( 409 /foo = "uhh bar"/ 410 ); 411 expect(after['ios/HelloWorld/GoogleService-Info.plist']).toBe(googleServiceInfoFixture); 412 413 expect(after['android/app/src/main/java/com/bacon/todo/MainApplication.java']).toMatch( 414 'package com.bacon.todo;' 415 ); 416 417 expect(after['android/app/src/main/res/values/strings.xml']).toMatch( 418 '<string name="app_name">my cool app</string>' 419 ); 420 421 // Ensure files are always written in the correct format 422 for (const xmlPath of [ 423 'android/app/src/main/AndroidManifest.xml', 424 'android/app/src/main/res/values/styles.xml', 425 'android/app/src/main/res/values/strings.xml', 426 'android/app/src/main/res/values/colors.xml', 427 'ios/HelloWorld/Info.plist', 428 ]) { 429 const isValid = await isValidXMLAsync(path.join(projectRoot, xmlPath)); 430 if (!isValid) throw new Error(`Invalid XML file format at: "${xmlPath}"`); 431 } 432 433 // Ensure the infoPlist object is merged correctly 434 const infoPlist = await plist.parse( 435 fs.readFileSync(path.join(projectRoot, 'ios/HelloWorld/Info.plist'), 'utf8') 436 ); 437 expect(infoPlist.bar).toStrictEqual({ val: ['foo'] }); 438 // Ensure the entitlements object is merged correctly 439 const entitlements = await plist.parse( 440 fs.readFileSync(path.join(projectRoot, 'ios/HelloWorld/mycoolapp.entitlements'), 'utf8') 441 ); 442 expect(entitlements.foo).toStrictEqual('bar'); 443 444 // Ensure files are always written in the correct format 445 for (const xmlPath of [ 446 'ios/HelloWorld/Images.xcassets/AppIcon.appiconset/Contents.json', 447 'ios/HelloWorld/Images.xcassets/Contents.json', 448 'android/app/google-services.json', 449 ]) { 450 const isValid = await isValidJSONAsync(path.join(projectRoot, xmlPath)); 451 if (!isValid) throw new Error(`Invalid JSON file format at: "${xmlPath}"`); 452 } 453 454 // Ensure the Xcode project file can be read and parsed. 455 const project = xcode.project( 456 path.join(projectRoot, 'ios/HelloWorld.xcodeproj/project.pbxproj') 457 ); 458 project.parseSync(); 459 }); 460 461 it('introspects mods', async () => { 462 let config = getPrebuildConfig(); 463 464 // Apply mod 465 config = await compileModsAsync(config, { introspect: true, projectRoot: '/app' }); 466 467 // App config should have been modified 468 expect(config.name).toBe('my cool app'); 469 expect(config.ios?.infoPlist).toBeDefined(); 470 expect(config.ios?.entitlements).toBeDefined(); 471 472 // Google Sign In 473 expect( 474 config.ios?.infoPlist?.CFBundleURLTypes?.find(({ CFBundleURLSchemes }) => 475 CFBundleURLSchemes.includes('com.googleusercontent.apps.1234567890123-abcdef') 476 ) 477 ).toBeDefined(); 478 // Branch 479 expect(config.ios?.infoPlist?.branch_key?.live).toBe('MY_BRANCH_KEY'); 480 481 const mods = config.mods!; 482 // Mods should all be functions 483 expect(Object.values(mods.ios!).every((value) => typeof value === 'function')).toBe(true); 484 expect(Object.values(mods.android!).every((value) => typeof value === 'function')).toBe(true); 485 // Ensure these mods are removed 486 expect(mods.android?.dangerous).toBeUndefined(); 487 expect(mods.android?.mainActivity).toBeUndefined(); 488 expect(mods.android?.appBuildGradle).toBeUndefined(); 489 expect(mods.android?.projectBuildGradle).toBeUndefined(); 490 expect(mods.android?.settingsGradle).toBeUndefined(); 491 expect(mods.ios?.dangerous).toBeUndefined(); 492 expect(mods.ios?.xcodeproj).toBeUndefined(); 493 494 delete config.mods; 495 496 // Shape 497 expect(config).toMatchSnapshot(); 498 499 expect(config._internal?.modResults).toBeDefined(); 500 expect(config._internal?.modResults.ios.infoPlist).toBeDefined(); 501 expect(config._internal?.modResults.ios.expoPlist).toBeDefined(); 502 expect(config._internal?.modResults.ios.entitlements).toBeDefined(); 503 expect(config._internal?.modResults.android.manifest).toBeDefined(); 504 expect(Array.isArray(config._internal?.modResults.android.gradleProperties)).toBe(true); 505 expect(config._internal?.modResults.android.strings).toBeDefined(); 506 507 // Test the written files... 508 const after = getDirFromFS(vol.toJSON(), projectRoot); 509 510 expect(Object.keys(after)).toEqual([ 511 'node_modules/react-native-maps/package.json', 512 'ios/.xcode.env', 513 'ios/HelloWorld/AppDelegate.h', 514 'ios/HelloWorld/AppDelegate.mm', 515 'ios/HelloWorld/Images.xcassets/AppIcon.appiconset/Contents.json', 516 'ios/HelloWorld/Images.xcassets/Contents.json', 517 'ios/HelloWorld/Images.xcassets/SplashScreen.imageset/Contents.json', 518 'ios/HelloWorld/Images.xcassets/SplashScreenBackground.imageset/Contents.json', 519 'ios/HelloWorld/Info.plist', 520 'ios/HelloWorld/SplashScreen.storyboard', 521 'ios/HelloWorld/Supporting/Expo.plist', 522 'ios/HelloWorld/main.m', 523 'ios/HelloWorld/HelloWorld.entitlements', 524 'ios/HelloWorld.xcodeproj/project.pbxproj', 525 'ios/HelloWorld.xcodeproj/project.xcworkspace/contents.xcworkspacedata', 526 'ios/HelloWorld.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist', 527 'ios/HelloWorld.xcodeproj/xcshareddata/xcschemes/HelloWorld.xcscheme', 528 'ios/HelloWorld.xcworkspace/contents.xcworkspacedata', 529 'ios/HelloWorld.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist', 530 'ios/Podfile', 531 'ios/Podfile.properties.json', 532 'ios/gitignore', 533 'android/app/build.gradle', 534 'android/app/debug.keystore', 535 'android/app/proguard-rules.pro', 536 'android/app/src/debug/AndroidManifest.xml', 537 'android/app/src/debug/java/com/helloworld/ReactNativeFlipper.java', 538 'android/app/src/main/AndroidManifest.xml', 539 'android/app/src/main/java/com/helloworld/MainActivity.java', 540 'android/app/src/main/java/com/helloworld/MainApplication.java', 541 'android/app/src/main/res/drawable/rn_edit_text_material.xml', 542 'android/app/src/main/res/drawable/splashscreen.xml', 543 'android/app/src/main/res/values/colors.xml', 544 'android/app/src/main/res/values/strings.xml', 545 'android/app/src/main/res/values/styles.xml', 546 'android/app/src/release/java/com/helloworld/ReactNativeFlipper.java', 547 'android/build.gradle', 548 'android/gitignore', 549 'android/gradle/wrapper/gradle-wrapper.jar', 550 'android/gradle/wrapper/gradle-wrapper.properties', 551 'android/gradle.properties', 552 'android/gradlew', 553 'android/gradlew.bat', 554 'android/settings.gradle', 555 'config/GoogleService-Info.plist', 556 'config/google-services.json', 557 'locales/en-US.json', 558 ]); 559 560 // unmodified 561 expect(after['ios/HelloWorld/HelloWorld.entitlements']).not.toMatch( 562 'com.apple.developer.associated-domains' 563 ); 564 565 expect(after['ios/HelloWorld/Info.plist']).toBe(rnFixture['ios/HelloWorld/Info.plist']); 566 567 expect(after['android/app/src/main/java/com/helloworld/MainApplication.java']).toBe( 568 rnFixture['android/app/src/main/java/com/helloworld/MainApplication.java'] 569 ); 570 expect(after['android/app/src/main/java/com/helloworld/MainActivity.java']).toBe( 571 rnFixture['android/app/src/main/java/com/helloworld/MainActivity.java'] 572 ); 573 expect(after['android/app/src/main/res/values/styles.xml']).toMatch( 574 rnFixture['android/app/src/main/res/values/styles.xml'] 575 ); 576 577 // for (const [name, contents] of Object.entries(rnFixture)) { 578 // // The pbxproj seems to reformat in jest 579 // if (name.includes('pbxproj') || name.endsWith('MainApplicationTurboModuleManagerDelegate.h')) 580 // continue; 581 // expect(after[name]).toMatch(contents); 582 // } 583 // Ensure the Xcode project file can be read and parsed. 584 const project = xcode.project( 585 path.join(projectRoot, 'ios/HelloWorld.xcodeproj/project.pbxproj') 586 ); 587 project.parseSync(); 588 }); 589 590 // Tests that introspection works 591 it('introspects mods in a managed project', async () => { 592 vol.reset(); 593 vol.fromJSON( 594 { 595 // Required to link react-native-maps 596 './node_modules/react-native-maps/package.json': JSON.stringify({}), 597 // App files 598 'config/GoogleService-Info.plist': googleServiceInfoFixture, 599 'config/google-services.json': '{}', 600 'icons/foreground.png': icon, 601 'icons/background.png': icon, 602 'icons/notification-icon.png': icon, 603 'icons/ios-icon.png': icon, 604 'locales/en-US.json': JSON.stringify({ foo: 'uhh bar', fallback: 'fallback' }, null, 2), 605 }, 606 projectRoot 607 ); 608 609 let config = getPrebuildConfig(); 610 611 // Apply mod 612 config = await compileModsAsync(config, { introspect: true, projectRoot: '/app' }); 613 614 // App config should have been modified 615 expect(config.name).toBe('my cool app'); 616 expect(config.ios?.infoPlist).toBeDefined(); 617 expect(config.ios?.entitlements).toBeDefined(); 618 619 // Google Sign In 620 expect( 621 config.ios?.infoPlist?.CFBundleURLTypes?.find(({ CFBundleURLSchemes }) => 622 CFBundleURLSchemes.includes('com.googleusercontent.apps.1234567890123-abcdef') 623 ) 624 ).toBeDefined(); 625 // Branch 626 expect(config.ios?.infoPlist?.branch_key?.live).toBe('MY_BRANCH_KEY'); 627 628 const mods = config.mods!; 629 // Mods should all be functions 630 expect(Object.values(mods.ios!).every((value) => typeof value === 'function')).toBe(true); 631 expect(Object.values(mods.android!).every((value) => typeof value === 'function')).toBe(true); 632 // Ensure these mods are removed 633 expect(mods.android?.dangerous).toBeUndefined(); 634 expect(mods.android?.mainActivity).toBeUndefined(); 635 expect(mods.android?.appBuildGradle).toBeUndefined(); 636 expect(mods.android?.projectBuildGradle).toBeUndefined(); 637 expect(mods.android?.settingsGradle).toBeUndefined(); 638 expect(mods.ios?.dangerous).toBeUndefined(); 639 expect(mods.ios?.xcodeproj).toBeUndefined(); 640 641 delete config.mods; 642 643 // Shape 644 expect(config).toMatchSnapshot(); 645 646 expect(config._internal?.modResults).toBeDefined(); 647 expect(config._internal?.modResults.ios.infoPlist).toBeDefined(); 648 expect(config._internal?.modResults.ios.expoPlist).toBeDefined(); 649 expect(config._internal?.modResults.ios.entitlements).toBeDefined(); 650 expect(config._internal?.modResults.android.manifest).toBeDefined(); 651 expect(Array.isArray(config._internal?.modResults.android.gradleProperties)).toBe(true); 652 expect(config._internal?.modResults.android.strings).toBeDefined(); 653 654 // Test the written files... 655 const after = getDirFromFS(vol.toJSON(), projectRoot); 656 657 expect(Object.keys(after)).toStrictEqual([ 658 'node_modules/react-native-maps/package.json', 659 'config/GoogleService-Info.plist', 660 'config/google-services.json', 661 'locales/en-US.json', 662 ]); 663 }); 664 665 it('create Podfile.properties.json file for backward compatible', async () => { 666 const { '/app/ios/Podfile.properties.json': _, ...volWithoutPodfileProperties } = vol.toJSON(); 667 vol.reset(); 668 vol.fromJSON(volWithoutPodfileProperties); 669 670 let config = getPrebuildConfig(); 671 // change jsEngine to hermes 672 config.jsEngine = 'hermes'; 673 674 config = await compileModsAsync(config, { projectRoot: '/app' }); 675 676 const result = await JsonFile.readAsync('/app/ios/Podfile.properties.json'); 677 expect(result).toMatchObject({ 'expo.jsEngine': 'hermes' }); 678 }); 679}); 680 681async function isValidXMLAsync(filePath: string) { 682 try { 683 const res = await readXMLAsync({ path: filePath }); 684 return !!res; 685 } catch { 686 return false; 687 } 688} 689 690async function isValidJSONAsync(filePath: string) { 691 try { 692 const res = await JsonFile.readAsync(filePath); 693 return !!res; 694 } catch { 695 return false; 696 } 697} 698