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