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 androidStatusBar: { 72 barStyle: 'light-content', 73 backgroundColor: '#000FFF', 74 hidden: false, 75 translucent: true, 76 }, 77 androidNavigationBar: { 78 visible: 'sticky-immersive', 79 barStyle: 'dark-content', 80 81 backgroundColor: '#ff0000', 82 }, 83 developmentClient: { 84 silentLaunch: true, 85 }, 86 scheme: 'my-app-redirect', 87 packagerOpts: { 88 extraThing: true, 89 }, 90 updates: { 91 enabled: true, 92 checkAutomatically: 'ON_ERROR_RECOVERY', 93 fallbackToCacheTimeout: 650, 94 }, 95 locales: { 96 en: './locales/en-US.json', 97 es: { foo: 'el bar' }, 98 }, 99 facebookAppId: '1234567890', 100 facebookAutoInitEnabled: true, 101 facebookAutoLogAppEventsEnabled: true, 102 facebookAdvertiserIDCollectionEnabled: true, 103 facebookDisplayName: 'my-fb-test-app', 104 facebookScheme: 'fb1234567890', 105 ios: { 106 bundleIdentifier: 'com.bacon.tester.expoapp', 107 buildNumber: '6.5.0', 108 backgroundColor: '#ff0000', 109 appStoreUrl: 'https://itunes.apple.com/us/app/pillar-valley/id1336398804?ls=1&mt=8', 110 config: { 111 branch: { 112 apiKey: 'MY_BRANCH_KEY', 113 }, 114 usesNonExemptEncryption: true, 115 googleMapsApiKey: 'TEST_googleMapsApiKey', 116 googleMobileAdsAppId: 'TEST_googleMobileAdsAppId', 117 googleMobileAdsAutoInit: true, 118 }, 119 googleServicesFile: './config/GoogleService-Info.plist', 120 supportsTablet: true, 121 isTabletOnly: false, 122 requireFullScreen: true, 123 userInterfaceStyle: 'automatic', 124 infoPlist: { bar: { val: ['foo'] } }, 125 entitlements: { foo: 'bar' }, 126 associatedDomains: ['applinks:https://pillarvalley.netlify.app'], 127 usesIcloudStorage: true, 128 usesAppleSignIn: true, 129 accessesContactNotes: true, 130 }, 131 android: { 132 package: 'com.bacon.tester.expoapp', 133 versionCode: 6, 134 backgroundColor: '#ff0000', 135 userInterfaceStyle: 'light', 136 adaptiveIcon: { 137 foregroundImage: './icons/foreground.png', 138 backgroundImage: './icons/background.png', 139 }, 140 splash: { 141 backgroundColor: '#ff00ff', 142 dark: { 143 backgroundColor: '#00ffff', 144 }, 145 }, 146 blockedPermissions: [ 147 'android.permission.RECORD_AUDIO', 148 'android.permission.ACCESS_FINE_LOCATION', 149 ], 150 permissions: [ 151 'CAMERA', 152 'com.sec.android.provider.badge.permission.WRITE', 153 'android.permission.RECORD_AUDIO', 154 ], 155 googleServicesFile: './config/google-services.json', 156 config: { 157 branch: { 158 apiKey: 'MY_BRANCH_ANDROID_KEY', 159 }, 160 googleMaps: { 161 apiKey: 'MY_GOOGLE_MAPS_ANDROID_KEY', 162 }, 163 googleMobileAdsAppId: 'MY_GOOGLE_MOBILE_ADS_APP_ID', 164 googleMobileAdsAutoInit: true, 165 }, 166 intentFilters: [ 167 { 168 autoVerify: true, 169 action: 'VIEW', 170 data: { 171 scheme: 'https', 172 host: '*.expo.dev', 173 }, 174 category: ['BROWSABLE', 'DEFAULT'], 175 }, 176 ], 177 allowBackup: true, 178 softwareKeyboardLayoutMode: 'pan', 179 }, 180 _internal: { projectRoot: '/app' }, 181 mods: null, 182 }; 183} 184 185function getPrebuildConfig() { 186 let config = { ...getLargeConfig() }; 187 config = withVersionedExpoSDKPlugins(config, { expoUsername: 'bacon' }); 188 189 config = withIosExpoPlugins(config, { 190 bundleIdentifier: 'com.bacon.todo', 191 }); 192 config = withAndroidExpoPlugins(config, { 193 package: 'com.bacon.todo', 194 }); 195 return config; 196} 197describe(evalModsAsync, () => { 198 it(`runs with no core mods`, async () => { 199 let config: ExportedConfig = { 200 name: 'app', 201 slug: '', 202 }; 203 config = await evalModsAsync(config, { projectRoot: '/' }); 204 expect(config.ios).toBeUndefined(); 205 }); 206}); 207 208describe('built-in plugins', () => { 209 const projectRoot = '/app'; 210 const iconPath = path.resolve(__dirname, './fixtures/icon.png'); 211 const icon = fsReal.readFileSync(iconPath) as any; 212 const googleServiceInfoFixture = fsReal.readFileSync( 213 path.resolve(__dirname, './fixtures/GoogleService-Info.plist'), 214 'utf8' 215 ) as any; 216 217 beforeEach(async () => { 218 // Trick XDL Info.plist reading 219 Object.defineProperty(process, 'platform', { 220 value: 'not-darwin', 221 }); 222 vol.fromJSON( 223 { 224 // Required to link react-native-maps 225 './node_modules/react-native-maps/package.json': JSON.stringify({}), 226 // App files 227 ...rnFixture, 228 'ios/Podfile': PodfileBasic, 229 'config/GoogleService-Info.plist': googleServiceInfoFixture, 230 'config/google-services.json': '{}', 231 './icons/foreground.png': icon, 232 './icons/background.png': icon, 233 './icons/notification-icon.png': icon, 234 './icons/ios-icon.png': icon, 235 'locales/en-US.json': JSON.stringify({ foo: 'uhh bar', fallback: 'fallback' }, null, 2), 236 }, 237 projectRoot 238 ); 239 }); 240 241 afterEach(() => { 242 vol.reset(); 243 }); 244 245 // Ensure helpful error messages are thrown 246 it(`fails to locate the project name in an invalid project`, async () => { 247 const config = withOrientation({ 248 name: 'app', 249 slug: '', 250 ios: {}, 251 }); 252 await expect(compileModsAsync(config, { projectRoot: '/invalid' })).rejects.toThrow( 253 'Failed to locate Info.plist files relative' 254 ); 255 }); 256 257 it(`skips platforms`, async () => { 258 const config = withOrientation({ 259 name: 'app', 260 slug: '', 261 ios: {}, 262 }); 263 264 // should throw if the platform isn't skipped 265 await compileModsAsync(config, { projectRoot: '/invalid', platforms: ['android'] }); 266 }); 267 268 it('allows conflicts with info.plist overrides', async () => { 269 let config: ExportedConfig = { 270 name: 'app', 271 slug: '', 272 _internal: { projectRoot: '.' }, 273 ios: { 274 config: { 275 usesNonExemptEncryption: false, 276 }, 277 infoPlist: { 278 ITSAppUsesNonExemptEncryption: true, 279 }, 280 }, 281 }; 282 283 config = withIosExpoPlugins(config, { 284 bundleIdentifier: 'com.bacon.todo', 285 }); 286 // Apply mod 287 config = await compileModsAsync(config, { projectRoot: '/app' }); 288 // This should be false because ios.config.usesNonExemptEncryption is used in favor of ios.infoPlist.ITSAppUsesNonExemptEncryption 289 expect(config.ios?.infoPlist?.ITSAppUsesNonExemptEncryption).toBe(true); 290 }); 291 292 it('sends a valid modRequest', async () => { 293 let config = getPrebuildConfig(); 294 295 let modRequest; 296 config = withGradleProperties(config, (config) => { 297 modRequest = config.modRequest; 298 return config; 299 }); 300 // Apply mod 301 await compileModsAsync(config, { introspect: true, projectRoot: '/app' }); 302 303 expect(modRequest).toStrictEqual({ 304 introspect: true, 305 modName: 'gradleProperties', 306 platform: 'android', 307 platformProjectRoot: '/app/android', 308 projectName: undefined, 309 projectRoot: '/app', 310 }); 311 }); 312 it('compiles mods', async () => { 313 let config = getPrebuildConfig(); 314 // Apply mod 315 config = await compileModsAsync(config, { projectRoot: '/app' }); 316 317 // App config should have been modified 318 expect(config.name).toBe('my cool app'); 319 expect(config.ios?.infoPlist).toBeDefined(); 320 expect(config.ios?.entitlements).toBeDefined(); 321 322 // Google Sign In 323 expect( 324 config.ios?.infoPlist?.CFBundleURLTypes?.find(({ CFBundleURLSchemes }) => 325 CFBundleURLSchemes.includes('com.googleusercontent.apps.1234567890123-abcdef') 326 ) 327 ).toBeDefined(); 328 // Branch 329 expect(config.ios?.infoPlist?.branch_key?.live).toBe('MY_BRANCH_KEY'); 330 331 // Mods should all be functions 332 expect(Object.values(config.mods!.ios!).every((value) => typeof value === 'function')).toBe( 333 true 334 ); 335 336 delete config.mods; 337 338 // Shape 339 expect(config).toMatchSnapshot(); 340 341 // Test the written files... 342 const after = getDirFromFS(vol.toJSON(), projectRoot); 343 344 expect(Object.keys(after)).toStrictEqual([ 345 'node_modules/react-native-maps/package.json', 346 'ios/ReactNativeProject/Supporting/Expo.plist', 347 'ios/ReactNativeProject/Supporting/en.lproj/InfoPlist.strings', 348 'ios/ReactNativeProject/Supporting/es.lproj/InfoPlist.strings', 349 'ios/ReactNativeProject/Info.plist', 350 'ios/ReactNativeProject/AppDelegate.m', 351 'ios/ReactNativeProject/Base.lproj/LaunchScreen.xib', 352 'ios/ReactNativeProject/Images.xcassets/AppIcon.appiconset/Contents.json', 353 'ios/ReactNativeProject/Images.xcassets/Contents.json', 354 'ios/ReactNativeProject/Images.xcassets/SplashScreenBackground.imageset/image.png', 355 'ios/ReactNativeProject/Images.xcassets/SplashScreenBackground.imageset/Contents.json', 356 'ios/ReactNativeProject/GoogleService-Info.plist', 357 'ios/ReactNativeProject/noop-file.swift', 358 'ios/ReactNativeProject/ReactNativeProject-Bridging-Header.h', 359 'ios/ReactNativeProject/ReactNativeProject.entitlements', 360 'ios/ReactNativeProject/SplashScreen.storyboard', 361 'ios/ReactNativeProject.xcodeproj/project.pbxproj', 362 'ios/Podfile.properties.json', 363 'ios/Podfile', 364 'android/app/src/main/java/com/bacon/todo/MainActivity.java', 365 'android/app/src/main/java/com/bacon/todo/MainApplication.java', 366 'android/app/src/main/AndroidManifest.xml', 367 'android/app/src/main/res/values/styles.xml', 368 'android/app/src/main/res/values/colors.xml', 369 'android/app/src/main/res/values/strings.xml', 370 'android/app/src/main/res/drawable/splashscreen.xml', 371 'android/app/src/main/res/values-night/colors.xml', 372 'android/app/build.gradle', 373 'android/app/google-services.json', 374 'android/gradle.properties', 375 'android/settings.gradle', 376 'android/build.gradle', 377 'config/GoogleService-Info.plist', 378 'config/google-services.json', 379 'locales/en-US.json', 380 ]); 381 382 expect(after['ios/ReactNativeProject/ReactNativeProject.entitlements']).toMatch( 383 'com.apple.developer.associated-domains' 384 ); 385 386 expect(after['ios/ReactNativeProject/Info.plist']).toMatch(/com.bacon.todo/); 387 expect(after['ios/ReactNativeProject/Supporting/en.lproj/InfoPlist.strings']).toMatch( 388 /foo = "uhh bar"/ 389 ); 390 expect(after['ios/ReactNativeProject/GoogleService-Info.plist']).toBe(googleServiceInfoFixture); 391 392 expect(after['android/app/src/main/java/com/bacon/todo/MainApplication.java']).toMatch( 393 'package com.bacon.todo;' 394 ); 395 396 expect(after['android/app/src/main/res/values/strings.xml']).toMatch( 397 '<string name="app_name">my cool app</string>' 398 ); 399 400 // Ensure files are always written in the correct format 401 for (const xmlPath of [ 402 'android/app/src/main/AndroidManifest.xml', 403 'android/app/src/main/res/values/styles.xml', 404 'android/app/src/main/res/values/strings.xml', 405 'android/app/src/main/res/values/colors.xml', 406 'ios/ReactNativeProject/Info.plist', 407 'ios/ReactNativeProject/Base.lproj/LaunchScreen.xib', 408 ]) { 409 const isValid = await isValidXMLAsync(path.join(projectRoot, xmlPath)); 410 if (!isValid) throw new Error(`Invalid XML file format at: "${xmlPath}"`); 411 } 412 413 // Ensure the infoPlist object is merged correctly 414 const infoPlist = await plist.parse( 415 fs.readFileSync(path.join(projectRoot, 'ios/ReactNativeProject/Info.plist'), 'utf8') 416 ); 417 expect(infoPlist.bar).toStrictEqual({ val: ['foo'] }); 418 // Ensure the entitlements object is merged correctly 419 const entitlements = await plist.parse( 420 fs.readFileSync( 421 path.join(projectRoot, 'ios/ReactNativeProject/ReactNativeProject.entitlements'), 422 'utf8' 423 ) 424 ); 425 expect(entitlements.foo).toStrictEqual('bar'); 426 427 // Ensure files are always written in the correct format 428 for (const xmlPath of [ 429 'ios/ReactNativeProject/Images.xcassets/AppIcon.appiconset/Contents.json', 430 'ios/ReactNativeProject/Images.xcassets/Contents.json', 431 'android/app/google-services.json', 432 ]) { 433 const isValid = await isValidJSONAsync(path.join(projectRoot, xmlPath)); 434 if (!isValid) throw new Error(`Invalid JSON file format at: "${xmlPath}"`); 435 } 436 437 // Ensure the Xcode project file can be read and parsed. 438 const project = xcode.project( 439 path.join(projectRoot, 'ios/ReactNativeProject.xcodeproj/project.pbxproj') 440 ); 441 project.parseSync(); 442 }); 443 444 it('introspects mods', async () => { 445 let config = getPrebuildConfig(); 446 447 // Apply mod 448 config = await compileModsAsync(config, { introspect: true, projectRoot: '/app' }); 449 450 // App config should have been modified 451 expect(config.name).toBe('my cool app'); 452 expect(config.ios?.infoPlist).toBeDefined(); 453 expect(config.ios?.entitlements).toBeDefined(); 454 455 // Google Sign In 456 expect( 457 config.ios?.infoPlist?.CFBundleURLTypes?.find(({ CFBundleURLSchemes }) => 458 CFBundleURLSchemes.includes('com.googleusercontent.apps.1234567890123-abcdef') 459 ) 460 ).toBeDefined(); 461 // Branch 462 expect(config.ios?.infoPlist?.branch_key?.live).toBe('MY_BRANCH_KEY'); 463 464 const mods = config.mods!; 465 // Mods should all be functions 466 expect(Object.values(mods.ios!).every((value) => typeof value === 'function')).toBe(true); 467 expect(Object.values(mods.android!).every((value) => typeof value === 'function')).toBe(true); 468 // Ensure these mods are removed 469 expect(mods.android?.dangerous).toBeUndefined(); 470 expect(mods.android?.mainActivity).toBeUndefined(); 471 expect(mods.android?.appBuildGradle).toBeUndefined(); 472 expect(mods.android?.projectBuildGradle).toBeUndefined(); 473 expect(mods.android?.settingsGradle).toBeUndefined(); 474 expect(mods.ios?.dangerous).toBeUndefined(); 475 expect(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': googleServiceInfoFixture, 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('com.googleusercontent.apps.1234567890123-abcdef') 582 ) 583 ).toBeDefined(); 584 // Branch 585 expect(config.ios?.infoPlist?.branch_key?.live).toBe('MY_BRANCH_KEY'); 586 587 const mods = config.mods!; 588 // Mods should all be functions 589 expect(Object.values(mods.ios!).every((value) => typeof value === 'function')).toBe(true); 590 expect(Object.values(mods.android!).every((value) => typeof value === 'function')).toBe(true); 591 // Ensure these mods are removed 592 expect(mods.android?.dangerous).toBeUndefined(); 593 expect(mods.android?.mainActivity).toBeUndefined(); 594 expect(mods.android?.appBuildGradle).toBeUndefined(); 595 expect(mods.android?.projectBuildGradle).toBeUndefined(); 596 expect(mods.android?.settingsGradle).toBeUndefined(); 597 expect(mods.ios?.dangerous).toBeUndefined(); 598 expect(mods.ios?.xcodeproj).toBeUndefined(); 599 600 delete config.mods; 601 602 // Shape 603 expect(config).toMatchSnapshot(); 604 605 expect(config._internal?.modResults).toBeDefined(); 606 expect(config._internal?.modResults.ios.infoPlist).toBeDefined(); 607 expect(config._internal?.modResults.ios.expoPlist).toBeDefined(); 608 expect(config._internal?.modResults.ios.entitlements).toBeDefined(); 609 expect(config._internal?.modResults.android.manifest).toBeDefined(); 610 expect(Array.isArray(config._internal?.modResults.android.gradleProperties)).toBe(true); 611 expect(config._internal?.modResults.android.strings).toBeDefined(); 612 613 // Test the written files... 614 const after = getDirFromFS(vol.toJSON(), projectRoot); 615 616 expect(Object.keys(after)).toStrictEqual([ 617 'node_modules/react-native-maps/package.json', 618 'config/GoogleService-Info.plist', 619 'config/google-services.json', 620 'locales/en-US.json', 621 ]); 622 }); 623 624 it('create Podfile.properties.json file for backward compatible', async () => { 625 const { '/app/ios/Podfile.properties.json': _, ...volWithoutPodfileProperties } = vol.toJSON(); 626 vol.reset(); 627 vol.fromJSON(volWithoutPodfileProperties); 628 629 let config = getPrebuildConfig(); 630 // change jsEngine to hermes 631 config.jsEngine = 'hermes'; 632 633 config = await compileModsAsync(config, { projectRoot: '/app' }); 634 635 const result = await JsonFile.readAsync('/app/ios/Podfile.properties.json'); 636 expect(result).toMatchObject({ 'expo.jsEngine': 'hermes' }); 637 }); 638}); 639 640async function isValidXMLAsync(filePath: string) { 641 try { 642 const res = await readXMLAsync({ path: filePath }); 643 return !!res; 644 } catch { 645 return false; 646 } 647} 648 649async function isValidJSONAsync(filePath: string) { 650 try { 651 const res = await JsonFile.readAsync(filePath); 652 return !!res; 653 } catch { 654 return false; 655 } 656} 657