1import chalk from 'chalk'; 2import fs from 'fs-extra'; 3import glob from 'glob-promise'; 4import once from 'lodash/once'; 5import ncp from 'ncp'; 6import path from 'path'; 7import xcode from 'xcode'; 8 9import { EXPO_DIR } from '../Constants'; 10import * as Directories from '../Directories'; 11 12interface VendoredModuleUpdateStep { 13 iosPrefix?: string; 14 sourceIosPath?: string; 15 targetIosPath?: string; 16 sourceAndroidPath?: string; 17 targetAndroidPath?: string; 18 sourceAndroidPackage?: string; 19 targetAndroidPackage?: string; 20 recursive?: boolean; 21 updatePbxproj?: boolean; 22 23 // should cleanup target path before vendoring 24 cleanupTargetPath?: boolean; 25 26 /** 27 * Hook that is fired by the end of vendoring an Android file. 28 * You should use it to perform some extra operations that are not covered by the main flow. 29 * @deprecated Use {@link VendoredModuleConfig.moduleModifier} instead. 30 */ 31 onDidVendorAndroidFile?: (file: string) => Promise<void>; 32} 33 34type ModuleModifier = ( 35 moduleConfig: VendoredModuleConfig, 36 clonedProjectPath: string 37) => Promise<void>; 38 39interface VendoredModuleConfig { 40 repoUrl: string; 41 packageName?: string; 42 packageJsonPath?: string; 43 installableInManagedApps?: boolean; 44 semverPrefix?: '~' | '^'; 45 skipCleanup?: boolean; 46 steps: VendoredModuleUpdateStep[]; 47 /** 48 * These modifiers are run before files are copied to the target directory. 49 */ 50 moduleModifier?: ModuleModifier; 51 warnings?: string[]; 52} 53 54const IOS_DIR = Directories.getIosDir(); 55const ANDROID_DIR = Directories.getAndroidDir(); 56 57const SvgModifier: ModuleModifier = async function ( 58 moduleConfig: VendoredModuleConfig, 59 clonedProjectPath: string 60): Promise<void> { 61 const removeMacFiles = async () => { 62 const macPattern = path.join(clonedProjectPath, 'apple', '**', '*.macos.@(h|m)'); 63 const macFiles = await glob(macPattern); 64 for (const file of macFiles) { 65 await fs.remove(file); 66 } 67 }; 68 69 const addHeaderImport = async () => { 70 const targetPath = path.join(clonedProjectPath, 'apple', 'Text', 'RNSVGTopAlignedLabel.h'); 71 const content = await fs.readFile(targetPath, 'utf8'); 72 const transformedContent = `#import "RNSVGUIKit.h"\n${content}`; 73 await fs.writeFile(targetPath, transformedContent, 'utf8'); 74 }; 75 76 await removeMacFiles(); 77 await addHeaderImport(); 78}; 79 80const MapsModifier: ModuleModifier = async function ( 81 moduleConfig: VendoredModuleConfig, 82 clonedProjectPath: string 83): Promise<void> { 84 const fixGoogleMapsImports = async () => { 85 const targetPath = path.join(clonedProjectPath, 'ios', 'AirGoogleMaps', 'AIRGoogleMap.m'); 86 let content = await fs.readFile(targetPath, 'utf8'); 87 content = content.replace(/^#import "(GMU.+?\.h)"$/gm, '#import <Google-Maps-iOS-Utils/$1>'); 88 await fs.writeFile(targetPath, content, 'utf8'); 89 }; 90 91 await fixGoogleMapsImports(); 92}; 93 94const ReanimatedModifier: ModuleModifier = async function ( 95 moduleConfig: VendoredModuleConfig, 96 clonedProjectPath: string 97): Promise<void> { 98 const firstStep = moduleConfig.steps[0]; 99 const androidMainPathReanimated = path.join(clonedProjectPath, 'android', 'src', 'main'); 100 const androidMainPathExpoview = path.join(ANDROID_DIR, 'expoview', 'src', 'main'); 101 const JNIOldPackagePrefix = firstStep.sourceAndroidPackage!.split('.').join('/'); 102 const JNINewPackagePrefix = firstStep.targetAndroidPackage!.split('.').join('/'); 103 104 const replaceJNIPackages = async () => { 105 const cppPattern = path.join(androidMainPathReanimated, 'cpp', '**', '*.@(h|cpp)'); 106 const androidCpp = await glob(cppPattern); 107 for (const file of androidCpp) { 108 const content = await fs.readFile(file, 'utf8'); 109 const transformedContent = content.split(JNIOldPackagePrefix).join(JNINewPackagePrefix); 110 await fs.writeFile(file, transformedContent, 'utf8'); 111 } 112 }; 113 114 const copyCPP = async () => { 115 const dirs = ['Common', 'cpp']; 116 for (const dir of dirs) { 117 await fs.remove(path.join(androidMainPathExpoview, dir)); // clean 118 // copy 119 await new Promise<void>((res, rej) => { 120 ncp( 121 path.join(androidMainPathReanimated, dir), 122 path.join(androidMainPathExpoview, dir), 123 { dereference: true }, 124 () => { 125 res(); 126 } 127 ); 128 }); 129 } 130 }; 131 132 const prepareIOSNativeFiles = async () => { 133 const patternCommon = path.join(clonedProjectPath, 'Common', '**', '*.@(h|mm|cpp)'); 134 const patternNative = path.join(clonedProjectPath, 'ios', 'native', '**', '*.@(h|mm|cpp)'); 135 const commonFiles = await glob(patternCommon); 136 const iosOnlyFiles = await glob(patternNative); 137 const files = [...commonFiles, ...iosOnlyFiles]; 138 for (const file of files) { 139 console.log(file); 140 const fileName = file.split(path.sep).slice(-1)[0]; 141 await fs.copy(file, path.join(clonedProjectPath, 'ios', fileName)); 142 } 143 144 await fs.remove(path.join(clonedProjectPath, 'ios', 'native')); 145 }; 146 147 const transformGestureHandlerImports = async () => { 148 const javaFiles = await glob(path.join(clonedProjectPath, 'android', '**', '*.java')); 149 await Promise.all( 150 javaFiles.map(async (file) => { 151 let content = await fs.readFile(file, 'utf8'); 152 content = content.replace( 153 /^import com\.swmansion\.common\./gm, 154 'import versioned.host.exp.exponent.modules.api.components.gesturehandler.' 155 ); 156 await fs.writeFile(file, content); 157 }) 158 ); 159 }; 160 161 const applyRNVersionPatches = async () => { 162 const rnVersion = '0.67.2'; 163 const patchVersion = rnVersion.split('.')[1]; 164 const patchSourceDir = path.join(clonedProjectPath, 'android', 'rnVersionPatch', patchVersion); 165 const javaFiles = await glob('**/*.java', { 166 cwd: patchSourceDir, 167 }); 168 await Promise.all( 169 javaFiles.map(async (file) => { 170 const srcPath = path.join(patchSourceDir, file); 171 const dstPath = path.join( 172 clonedProjectPath, 173 'android', 174 'src', 175 'main', 176 'java', 177 'com', 178 'swmansion', 179 'reanimated', 180 file 181 ); 182 await fs.copy(srcPath, dstPath); 183 }) 184 ); 185 }; 186 187 await applyRNVersionPatches(); 188 await replaceJNIPackages(); 189 await copyCPP(); 190 await prepareIOSNativeFiles(); 191 await transformGestureHandlerImports(); 192}; 193 194const PickerModifier: ModuleModifier = once(async function (moduleConfig, clonedProjectPath) { 195 const addResourceImportAsync = async () => { 196 const files = [ 197 `${clonedProjectPath}/android/src/main/java/com/reactnativecommunity/picker/ReactPicker.java`, 198 `${clonedProjectPath}/android/src/main/java/com/reactnativecommunity/picker/ReactPickerManager.java`, 199 ]; 200 await Promise.all( 201 files 202 .map((file) => path.resolve(file)) 203 .map(async (file) => { 204 let content = await fs.readFile(file, 'utf8'); 205 content = content.replace(/^(package .+)$/gm, '$1\n\nimport host.exp.expoview.R;'); 206 await fs.writeFile(file, content, 'utf8'); 207 }) 208 ); 209 }; 210 211 await addResourceImportAsync(); 212}); 213 214const GestureHandlerModifier: ModuleModifier = async function ( 215 moduleConfig: VendoredModuleConfig, 216 clonedProjectPath: string 217): Promise<void> { 218 const addResourceImportAsync = async () => { 219 const files = [ 220 `${clonedProjectPath}/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt`, 221 ]; 222 await Promise.all( 223 files 224 .map((file) => path.resolve(file)) 225 .map(async (file) => { 226 let content = await fs.readFile(file, 'utf8'); 227 content = content.replace(/^(package .+)$/gm, '$1\nimport host.exp.expoview.R'); 228 await fs.writeFile(file, content, 'utf8'); 229 }) 230 ); 231 }; 232 233 const replaceOrAddBuildConfigImportAsync = async () => { 234 const files = [ 235 `${clonedProjectPath}/android/lib/src/main/java/com/swmansion/gesturehandler/GestureHandler.kt`, 236 `${clonedProjectPath}/android/src/main/java/com/swmansion/gesturehandler/RNGestureHandlerPackage.kt`, 237 `${clonedProjectPath}/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerModule.kt`, 238 ]; 239 await Promise.all( 240 files 241 .map((file) => path.resolve(file)) 242 .map(async (file) => { 243 let content = await fs.readFile(file, 'utf8'); 244 content = content 245 .replace(/^.*\.BuildConfig$/gm, '') 246 .replace(/^(package .+)$/gm, '$1\nimport host.exp.expoview.BuildConfig'); 247 await fs.writeFile(file, content, 'utf8'); 248 }) 249 ); 250 }; 251 252 const transformImportsAsync = async () => { 253 const files = [ 254 `${clonedProjectPath}/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerModule.kt`, 255 ]; 256 await Promise.all( 257 files 258 .map((file) => path.resolve(file)) 259 .map(async (file) => { 260 let content = await fs.readFile(file, 'utf8'); 261 content = content.replace( 262 /^import com\.swmansion\.common\./gm, 263 'import versioned.host.exp.exponent.modules.api.components.gesturehandler.' 264 ); 265 await fs.writeFile(file, content, 'utf8'); 266 }) 267 ); 268 }; 269 270 const commentOurReanimatedCode = async () => { 271 const files = [ 272 `${clonedProjectPath}/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerModule.kt`, 273 ]; 274 await Promise.all( 275 files 276 .map((file) => path.resolve(file)) 277 .map(async (file) => { 278 let content = await fs.readFile(file, 'utf8'); 279 content = content.replace( 280 'ReanimatedEventDispatcher.sendEvent(event, reactApplicationContext)', 281 '// $& // COMMENTED OUT BY VENDORING SCRIPT' 282 ); 283 await fs.writeFile(file, content, 'utf8'); 284 }) 285 ); 286 }; 287 288 await addResourceImportAsync(); 289 await replaceOrAddBuildConfigImportAsync(); 290 await transformImportsAsync(); 291 await commentOurReanimatedCode(); 292}; 293 294const ScreensModifier: ModuleModifier = async function ( 295 moduleConfig: VendoredModuleConfig, 296 clonedProjectPath: string 297): Promise<void> { 298 const viewmanagersExpoviewDir = path.join( 299 ANDROID_DIR, 300 'expoview', 301 'src', 302 'main', 303 'java', 304 'com', 305 'facebook', 306 'react', 307 'viewmanagers' 308 ); 309 310 const copyPaperViewManager = async () => { 311 await fs.remove(viewmanagersExpoviewDir); // clean 312 // copy 313 await new Promise<void>((res, rej) => { 314 ncp( 315 path.join( 316 clonedProjectPath, 317 'android', 318 'src', 319 'paper', 320 'java', 321 'com', 322 'facebook', 323 'react', 324 'viewmanagers' 325 ), 326 viewmanagersExpoviewDir, 327 { dereference: true }, 328 () => { 329 res(); 330 } 331 ); 332 }); 333 }; 334 335 await copyPaperViewManager(); 336}; 337 338const vendoredModulesConfig: { [key: string]: VendoredModuleConfig } = { 339 'react-native-gesture-handler': { 340 repoUrl: 'https://github.com/software-mansion/react-native-gesture-handler.git', 341 installableInManagedApps: true, 342 semverPrefix: '~', 343 moduleModifier: GestureHandlerModifier, 344 steps: [ 345 { 346 sourceAndroidPath: 'android/src/main/java/com/swmansion/gesturehandler', 347 targetAndroidPath: 'modules/api/components/gesturehandler', 348 sourceAndroidPackage: 'com.swmansion.gesturehandler', 349 targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.components.gesturehandler', 350 }, 351 { 352 sourceAndroidPath: 'android/lib/src/main/java/com/swmansion/gesturehandler', 353 targetAndroidPath: 'modules/api/components/gesturehandler', 354 sourceAndroidPackage: 'com.swmansion.gesturehandler', 355 targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.components.gesturehandler', 356 cleanupTargetPath: false, // first step cleans parent directory 357 }, 358 { 359 sourceAndroidPath: 'android/common/src/main/java/com/swmansion/common', 360 targetAndroidPath: 'modules/api/components/gesturehandler/common', 361 sourceAndroidPackage: 'com.swmansion.common', 362 targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.components.gesturehandler', 363 cleanupTargetPath: false, // first steps cleans parent directory 364 }, 365 { 366 sourceAndroidPath: 'android/src/paper/java/com/facebook/react/viewmanagers', 367 targetAndroidPath: '../../../../com/facebook/react/viewmanagers', 368 sourceAndroidPackage: 'com.facebook.react.viewmanagers', 369 targetAndroidPackage: 'com.facebook.react.viewmanagers', 370 cleanupTargetPath: false, 371 }, 372 { 373 sourceAndroidPath: 'android/src/paper/java/com/swmansion/gesturehandler', 374 targetAndroidPath: 'modules/api/components/gesturehandler', 375 sourceAndroidPackage: 'com.swmansion.gesturehandler', 376 targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.components.gesturehandler', 377 cleanupTargetPath: false, 378 }, 379 ], 380 }, 381 'react-native-reanimated': { 382 repoUrl: 'https://github.com/software-mansion/react-native-reanimated.git', 383 installableInManagedApps: true, 384 semverPrefix: '~', 385 moduleModifier: ReanimatedModifier, 386 steps: [ 387 { 388 recursive: true, 389 sourceIosPath: 'ios', 390 targetIosPath: 'Api/Reanimated', 391 sourceAndroidPath: 'android/src/main/java/com/swmansion/reanimated', 392 targetAndroidPath: 'modules/api/reanimated', 393 sourceAndroidPackage: 'com.swmansion.reanimated', 394 targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.reanimated', 395 onDidVendorAndroidFile: async (file: string) => { 396 const fileName = path.basename(file); 397 if (fileName === 'ReanimatedUIManager.java') { 398 // reanimated tries to override react native `UIManager` implementation. 399 // this file is placed inside `com/swmansion/reanimated/layoutReanimation/ReanimatedUIManager.java` 400 // but its package name is `package com.facebook.react.uimanager;`. 401 // we should put this into correct folder structure so that other files can 402 // `import com.facebook.react.uimanager.ReanimatedUIManager` 403 await fs.move( 404 file, 405 path.join( 406 ANDROID_DIR, 407 'expoview', 408 'src', 409 'main', 410 'java', 411 'com', 412 'facebook', 413 'react', 414 'uimanager', 415 fileName 416 ), 417 { overwrite: true } 418 ); 419 } 420 }, 421 }, 422 ], 423 warnings: [ 424 `NOTE: Any files in ${chalk.magenta( 425 'com.facebook.react' 426 )} will not be updated -- you'll need to add these to expoview manually!`, 427 `NOTE: Some imports have to be changed from ${chalk.magenta('<>')} form to 428 ${chalk.magenta('""')}`, 429 ], 430 }, 431 'react-native-screens': { 432 repoUrl: 'https://github.com/software-mansion/react-native-screens.git', 433 installableInManagedApps: true, 434 semverPrefix: '~', 435 moduleModifier: ScreensModifier, 436 steps: [ 437 { 438 sourceIosPath: 'ios', 439 targetIosPath: 'Api/Screens', 440 sourceAndroidPath: 'android/src/main/java/com/swmansion/rnscreens', 441 targetAndroidPath: 'modules/api/screens', 442 sourceAndroidPackage: 'com.swmansion.rnscreens', 443 targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.screens', 444 onDidVendorAndroidFile: async (file: string) => { 445 const filename = path.basename(file); 446 const CHANGES = { 447 'ScreenStack.kt': { 448 find: /(?=^class ScreenStack\()/m, 449 replaceWith: `import host.exp.expoview.R\n\n`, 450 }, 451 'ScreenStackHeaderConfig.kt': { 452 find: /(?=^class ScreenStackHeaderConfig\()/m, 453 replaceWith: `import host.exp.expoview.BuildConfig\nimport host.exp.expoview.R\n\n`, 454 }, 455 'RNScreensPackage.kt': { 456 find: /(?=^class RNScreensPackage\ :)/m, 457 replaceWith: `import host.exp.expoview.BuildConfig\n\n`, 458 }, 459 'Screen.kt': { 460 find: /(?=^@SuppressLint\(\"ViewConstructor\"\)\nclass Screen)/m, 461 replaceWith: `import host.exp.expoview.BuildConfig\n\n`, 462 }, 463 }; 464 465 const fileConfig = CHANGES[filename]; 466 if (!fileConfig) { 467 return; 468 } 469 470 const originalFileContent = await fs.readFile(file, 'utf8'); 471 const newFileContent = originalFileContent.replace( 472 fileConfig.find, 473 fileConfig.replaceWith 474 ); 475 await fs.writeFile(file, newFileContent, 'utf8'); 476 }, 477 }, 478 { 479 cleanupTargetPath: false, 480 sourceAndroidPath: 'android/src/paper/java/com/swmansion/rnscreens', 481 targetAndroidPath: 'modules/api/screens', 482 sourceAndroidPackage: 'com.swmansion.rnscreens', 483 targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.screens', 484 }, 485 ], 486 }, 487 'amazon-cognito-identity-js': { 488 repoUrl: 'https://github.com/aws-amplify/amplify-js.git', 489 installableInManagedApps: false, 490 steps: [ 491 { 492 sourceIosPath: 'packages/amazon-cognito-identity-js/ios', 493 targetIosPath: 'Api/Cognito', 494 sourceAndroidPath: 495 'packages/amazon-cognito-identity-js/android/src/main/java/com/amazonaws', 496 targetAndroidPath: 'modules/api/cognito', 497 sourceAndroidPackage: 'com.amazonaws', 498 targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.cognito', 499 }, 500 ], 501 }, 502 'react-native-view-shot': { 503 repoUrl: 'https://github.com/gre/react-native-view-shot.git', 504 steps: [ 505 { 506 sourceIosPath: 'ios', 507 targetIosPath: 'Api/ViewShot', 508 sourceAndroidPath: 'android/src/main/java/fr/greweb/reactnativeviewshot', 509 targetAndroidPath: 'modules/api/viewshot', 510 sourceAndroidPackage: 'fr.greweb.reactnativeviewshot', 511 targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.viewshot', 512 }, 513 ], 514 }, 515 'react-native-branch': { 516 repoUrl: 'https://github.com/BranchMetrics/react-native-branch-deep-linking.git', 517 steps: [ 518 { 519 sourceIosPath: 'ios', 520 targetIosPath: '../../../../packages/expo-branch/ios/EXBranch/RNBranch', 521 sourceAndroidPath: 'android/src/main/java/io/branch/rnbranch', 522 targetAndroidPath: 523 '../../../../../../../../../packages/expo-branch/android/src/main/java/io/branch/rnbranch', 524 sourceAndroidPackage: 'io.branch.rnbranch', 525 targetAndroidPackage: 'io.branch.rnbranch', 526 recursive: false, 527 updatePbxproj: false, 528 }, 529 ], 530 }, 531 'lottie-react-native': { 532 repoUrl: 'https://github.com/react-native-community/lottie-react-native.git', 533 installableInManagedApps: true, 534 steps: [ 535 { 536 iosPrefix: 'LRN', 537 sourceIosPath: 'src/ios/LottieReactNative', 538 targetIosPath: 'Api/Components/Lottie', 539 sourceAndroidPath: 'src/android/src/main/java/com/airbnb/android/react/lottie', 540 targetAndroidPath: 'modules/api/components/lottie', 541 sourceAndroidPackage: 'com.airbnb.android.react.lottie', 542 targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.components.lottie', 543 }, 544 ], 545 }, 546 'react-native-svg': { 547 repoUrl: 'https://github.com/react-native-community/react-native-svg.git', 548 installableInManagedApps: true, 549 moduleModifier: SvgModifier, 550 steps: [ 551 { 552 recursive: true, 553 sourceIosPath: 'apple', 554 targetIosPath: 'Api/Components/Svg', 555 sourceAndroidPath: 'android/src/main/java/com/horcrux/svg', 556 targetAndroidPath: 'modules/api/components/svg', 557 sourceAndroidPackage: 'com.horcrux.svg', 558 targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.components.svg', 559 }, 560 ], 561 }, 562 'react-native-maps': { 563 repoUrl: 'https://github.com/react-native-community/react-native-maps.git', 564 installableInManagedApps: true, 565 moduleModifier: MapsModifier, 566 steps: [ 567 { 568 sourceIosPath: 'ios/AirGoogleMaps', 569 targetIosPath: 'Api/Components/GoogleMaps', 570 }, 571 { 572 recursive: true, 573 sourceIosPath: 'ios/AirMaps', 574 targetIosPath: 'Api/Components/Maps', 575 sourceAndroidPath: 'android/src/main/java/com/rnmaps/maps', 576 targetAndroidPath: 'modules/api/components/maps', 577 sourceAndroidPackage: 'com.rnmaps.maps', 578 targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.components.maps', 579 }, 580 ], 581 }, 582 '@react-native-community/netinfo': { 583 repoUrl: 'https://github.com/react-native-community/react-native-netinfo.git', 584 installableInManagedApps: true, 585 steps: [ 586 { 587 sourceIosPath: 'ios', 588 targetIosPath: 'Api/NetInfo', 589 sourceAndroidPath: 'android/src/main/java/com/reactnativecommunity/netinfo', 590 targetAndroidPath: 'modules/api/netinfo', 591 sourceAndroidPackage: 'com.reactnativecommunity.netinfo', 592 targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.netinfo', 593 }, 594 ], 595 }, 596 'react-native-webview': { 597 repoUrl: 'https://github.com/react-native-community/react-native-webview.git', 598 installableInManagedApps: true, 599 steps: [ 600 { 601 sourceIosPath: 'apple', 602 targetIosPath: 'Api/Components/WebView', 603 sourceAndroidPath: 'android/src/main/java/com/reactnativecommunity/webview', 604 targetAndroidPath: 'modules/api/components/webview', 605 sourceAndroidPackage: 'com.reactnativecommunity.webview', 606 targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.components.webview', 607 }, 608 { 609 sourceAndroidPath: 'android/src/oldarch/com/reactnativecommunity/webview', 610 cleanupTargetPath: false, 611 targetAndroidPath: 'modules/api/components/webview', 612 sourceAndroidPackage: 'com.reactnativecommunity.webview', 613 targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.components.webview', 614 onDidVendorAndroidFile: async (file: string) => { 615 const fileName = path.basename(file); 616 if (fileName === 'RNCWebViewPackage.java') { 617 let content = await fs.readFile(file, 'utf8'); 618 content = content.replace( 619 /^(package .+)$/gm, 620 '$1\nimport host.exp.expoview.BuildConfig;' 621 ); 622 await fs.writeFile(file, content, 'utf8'); 623 } 624 }, 625 }, 626 ], 627 }, 628 'react-native-safe-area-context': { 629 repoUrl: 'https://github.com/th3rdwave/react-native-safe-area-context', 630 steps: [ 631 { 632 sourceIosPath: 'ios', 633 targetIosPath: 'Api/SafeAreaContext', 634 sourceAndroidPath: 'android/src/main/java/com/th3rdwave/safeareacontext', 635 targetAndroidPath: 'modules/api/safeareacontext', 636 sourceAndroidPackage: 'com.th3rdwave.safeareacontext', 637 targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.safeareacontext', 638 onDidVendorAndroidFile: async (file: string) => { 639 const fileName = path.basename(file); 640 if (fileName === 'SafeAreaContextPackage.kt') { 641 let content = await fs.readFile(file, 'utf8'); 642 content = content.replace( 643 /^(package .+)$/gm, 644 '$1\nimport host.exp.expoview.BuildConfig' 645 ); 646 await fs.writeFile(file, content, 'utf8'); 647 } 648 }, 649 }, 650 { 651 sourceIosPath: 'ios/SafeAreaContextSpec', 652 targetIosPath: 'Api/SafeAreaContext', 653 cleanupTargetPath: false, 654 }, 655 { 656 sourceAndroidPath: 'android/src/paper/java/com/th3rdwave/safeareacontext', 657 targetAndroidPath: 'modules/api/safeareacontext', 658 sourceAndroidPackage: 'com.th3rdwave.safeareacontext', 659 targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.safeareacontext', 660 cleanupTargetPath: false, 661 }, 662 { 663 sourceAndroidPath: 'android/src/paper/java/com/facebook/react/viewmanagers', 664 targetAndroidPath: 'modules/api/safeareacontext', 665 sourceAndroidPackage: 'com.facebook.react.viewmanagers', 666 targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.safeareacontext', 667 cleanupTargetPath: false, 668 }, 669 ], 670 }, 671 '@react-native-community/datetimepicker': { 672 repoUrl: 'https://github.com/react-native-community/react-native-datetimepicker.git', 673 installableInManagedApps: true, 674 steps: [ 675 { 676 sourceIosPath: 'ios', 677 targetIosPath: 'Api/Components/DateTimePicker', 678 sourceAndroidPath: 'android/src/main/java/com/reactcommunity/rndatetimepicker', 679 targetAndroidPath: 'modules/api/components/datetimepicker', 680 sourceAndroidPackage: 'com.reactcommunity.rndatetimepicker', 681 targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.components.datetimepicker', 682 }, 683 { 684 sourceAndroidPath: 'android/src/paper/java/com/reactcommunity/rndatetimepicker', 685 targetAndroidPath: 'modules/api/components/datetimepicker', 686 sourceAndroidPackage: 'com.reactcommunity.rndatetimepicker', 687 targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.components.datetimepicker', 688 cleanupTargetPath: false, 689 }, 690 ], 691 warnings: [ 692 `NOTE: In Expo, native Android styles are prefixed with ${chalk.magenta( 693 'ReactAndroid' 694 )}. Please ensure that ${chalk.magenta( 695 'resourceName' 696 )}s used for grabbing style of dialogs are being resolved properly.`, 697 ], 698 }, 699 '@react-native-masked-view/masked-view': { 700 repoUrl: 'https://github.com/react-native-masked-view/masked-view', 701 installableInManagedApps: true, 702 steps: [ 703 { 704 sourceIosPath: 'ios', 705 targetIosPath: 'Api/Components/MaskedView', 706 sourceAndroidPath: 'android/src/main/java/org/reactnative/maskedview', 707 targetAndroidPath: 'modules/api/components/maskedview', 708 sourceAndroidPackage: 'org.reactnative.maskedview', 709 targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.components.maskedview', 710 }, 711 ], 712 }, 713 '@react-native-segmented-control/segmented-control': { 714 repoUrl: 'https://github.com/react-native-segmented-control/segmented-control', 715 installableInManagedApps: true, 716 steps: [ 717 { 718 sourceIosPath: 'ios', 719 targetIosPath: 'Api/Components/SegmentedControl', 720 }, 721 ], 722 }, 723 '@react-native-picker/picker': { 724 repoUrl: 'https://github.com/react-native-picker/picker', 725 installableInManagedApps: true, 726 moduleModifier: PickerModifier, 727 steps: [ 728 { 729 sourceIosPath: 'ios', 730 targetIosPath: 'Api/Components/Picker', 731 sourceAndroidPath: 'android/src/main/java/com/reactnativecommunity/picker', 732 targetAndroidPath: 'modules/api/components/picker', 733 sourceAndroidPackage: 'com.reactnativecommunity.picker', 734 targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.components.picker', 735 }, 736 ], 737 }, 738 '@stripe/stripe-react-native': { 739 repoUrl: 'https://github.com/stripe/stripe-react-native', 740 installableInManagedApps: true, 741 steps: [ 742 { 743 sourceAndroidPath: 'android/src/main/java/com/reactnativestripesdk', 744 targetAndroidPath: 'modules/api/components/reactnativestripesdk', 745 sourceAndroidPackage: 'com.reactnativestripesdk', 746 targetAndroidPackage: 747 'versioned.host.exp.exponent.modules.api.components.reactnativestripesdk', 748 }, 749 ], 750 }, 751}; 752 753async function renameIOSSymbolsAsync(file: string, iosPrefix: string) { 754 const content = await fs.readFile(file, 'utf8'); 755 756 // Do something more sophisticated if this causes issues with more complex modules. 757 const transformedContent = content.replace(new RegExp(iosPrefix, 'g'), 'EX'); 758 const newFileName = file.replace(iosPrefix, 'EX'); 759 760 await fs.writeFile(newFileName, transformedContent, 'utf8'); 761 await fs.remove(file); 762} 763 764async function findObjcFilesAsync(dir: string, recursive: boolean): Promise<string[]> { 765 const pattern = path.join(dir, recursive ? '**' : '', '*.@(h|m|c|mm|cpp|swift)'); 766 return await glob(pattern); 767} 768 769async function renamePackageAndroidAsync( 770 file: string, 771 sourceAndroidPackage: string, 772 targetAndroidPackage: string 773) { 774 const content = await fs.readFile(file, 'utf8'); 775 776 // Note: this only works for a single package. If react-native-svg separates 777 // its code into multiple packages we will have to do something more 778 // sophisticated here. 779 const transformedContent = content.replace( 780 new RegExp(sourceAndroidPackage, 'g'), 781 targetAndroidPackage 782 ); 783 784 await fs.writeFile(file, transformedContent, 'utf8'); 785} 786 787async function findAndroidFilesAsync(dir: string): Promise<string[]> { 788 const pattern = path.join(dir, '**', '*.@(java|kt)'); 789 return await glob(pattern); 790} 791 792async function loadXcodeprojFileAsync(file: string): Promise<any> { 793 return new Promise((resolve, reject) => { 794 const pbxproj = xcode.project(file); 795 pbxproj.parse((err) => (err ? reject(err) : resolve(pbxproj))); 796 }); 797} 798 799function pbxGroupChild(file) { 800 const obj = Object.create(null); 801 obj.value = file.fileRef; 802 obj.comment = file.basename; 803 return obj; 804} 805 806function pbxGroupHasChildWithRef(group: any, ref: string): boolean { 807 return group.children.some((child) => child.value === ref); 808} 809 810async function addFileToPbxprojAsync( 811 filePath: string, 812 targetDir: string, 813 pbxproj: any 814): Promise<void> { 815 const fileName = path.basename(filePath); 816 817 // The parent group of the target directory that should already be created in the project, e.g. `Components` or `Api`. 818 const targetGroup = pbxproj.pbxGroupByName(path.basename(path.dirname(targetDir))); 819 820 if (!pbxproj.hasFile(fileName)) { 821 console.log(`Adding ${chalk.magenta(fileName)} to pbxproj configuration ...`); 822 823 const fileOptions = { 824 // Mute warnings from 3rd party modules. 825 compilerFlags: '-w', 826 }; 827 828 // The group name is mostly just a basename of the path. 829 const groupName = path.basename(path.dirname(filePath)); 830 831 // Add a file to pbxproj tree. 832 const file = 833 path.extname(fileName) === '.h' 834 ? pbxproj.addHeaderFile(fileName, fileOptions, groupName) 835 : pbxproj.addSourceFile(fileName, fileOptions, groupName); 836 837 // Search for the group where the file should be placed. 838 const group = pbxproj.pbxGroupByName(groupName); 839 840 // Our files has `includeInIndex` set to 1, so let's continue doing that. 841 file.includeInIndex = 1; 842 843 if (group) { 844 // Add a file if it is not there already. 845 if (!pbxGroupHasChildWithRef(group, file.fileRef)) { 846 group.children.push(pbxGroupChild(file)); 847 } 848 } else { 849 // Create a pbx group with this file. 850 const { uuid } = pbxproj.addPbxGroup([file.path], groupName, groupName); 851 852 // Add newly created group to the parent group. 853 if (!pbxGroupHasChildWithRef(targetGroup, uuid)) { 854 targetGroup.children.push(pbxGroupChild({ fileRef: uuid, basename: groupName })); 855 } 856 } 857 } 858} 859 860async function copyFilesAsync( 861 files: string[], 862 sourceDir: string, 863 targetDir: string 864): Promise<void> { 865 for (const file of files) { 866 const fileRelativePath = path.relative(sourceDir, file); 867 const fileTargetPath = path.join(targetDir, fileRelativePath); 868 869 await fs.mkdirs(path.dirname(fileTargetPath)); 870 await fs.copy(file, fileTargetPath); 871 872 console.log(chalk.yellow('>'), chalk.magenta(path.relative(EXPO_DIR, fileTargetPath))); 873 } 874} 875 876export async function legacyVendorModuleAsync( 877 moduleName: string, 878 platform: string, 879 tmpDir: string 880) { 881 const moduleConfig = vendoredModulesConfig[moduleName]; 882 883 if (!moduleConfig) { 884 throw new Error( 885 `Module \`${chalk.green( 886 moduleName 887 )}\` doesn't match any of currently supported 3rd party modules. Run with \`--list\` to show a list of modules.` 888 ); 889 } 890 891 moduleConfig.installableInManagedApps = 892 moduleConfig.installableInManagedApps == null ? true : moduleConfig.installableInManagedApps; 893 894 if (moduleConfig.warnings) { 895 moduleConfig.warnings.forEach((warning) => console.warn(warning)); 896 } 897 898 if (moduleConfig.moduleModifier) { 899 await moduleConfig.moduleModifier(moduleConfig, tmpDir); 900 } 901 902 for (const step of moduleConfig.steps) { 903 const executeAndroid = ['all', 'android'].includes(platform); 904 const executeIOS = ['all', 'ios'].includes(platform); 905 906 step.recursive = step.recursive === true; 907 step.updatePbxproj = !(step.updatePbxproj === false); 908 const cleanupTargetPath = step.cleanupTargetPath ?? true; 909 910 // iOS 911 if (executeIOS && step.sourceIosPath && step.targetIosPath) { 912 const sourceDir = path.join(tmpDir, step.sourceIosPath); 913 const targetDir = path.join(IOS_DIR, 'Exponent', 'Versioned', 'Core', step.targetIosPath); 914 915 if (cleanupTargetPath) { 916 console.log( 917 `\nCleaning up iOS files at ${chalk.magenta(path.relative(IOS_DIR, targetDir))} ...` 918 ); 919 920 await fs.remove(targetDir); 921 } 922 await fs.mkdirs(targetDir); 923 924 console.log('\nCopying iOS files ...'); 925 926 const objcFiles = await findObjcFilesAsync(sourceDir, step.recursive); 927 const pbxprojPath = path.join(IOS_DIR, 'Exponent.xcodeproj', 'project.pbxproj'); 928 const pbxproj = await loadXcodeprojFileAsync(pbxprojPath); 929 930 await copyFilesAsync(objcFiles, sourceDir, targetDir); 931 932 if (step.updatePbxproj) { 933 console.log(`\nUpdating pbxproj configuration ...`); 934 935 for (const file of objcFiles) { 936 const fileRelativePath = path.relative(sourceDir, file); 937 const fileTargetPath = path.join(targetDir, fileRelativePath); 938 939 await addFileToPbxprojAsync(fileTargetPath, targetDir, pbxproj); 940 } 941 942 console.log( 943 `Saving updated pbxproj structure to the file ${chalk.magenta( 944 path.relative(IOS_DIR, pbxprojPath) 945 )} ...` 946 ); 947 await fs.writeFile(pbxprojPath, pbxproj.writeSync()); 948 } 949 950 if (step.iosPrefix) { 951 console.log(`\nUpdating classes prefix to ${chalk.yellow(step.iosPrefix)} ...`); 952 953 const files = await findObjcFilesAsync(targetDir, step.recursive); 954 955 for (const file of files) { 956 await renameIOSSymbolsAsync(file, step.iosPrefix); 957 } 958 } 959 960 console.log( 961 chalk.yellow( 962 `\nSuccessfully updated iOS files, but please make sure Xcode project files are setup correctly in ${chalk.magenta( 963 `Exponent/Versioned/Core/${step.targetIosPath}` 964 )}` 965 ) 966 ); 967 } 968 969 // Android 970 if ( 971 executeAndroid && 972 step.sourceAndroidPath && 973 step.targetAndroidPath && 974 step.sourceAndroidPackage && 975 step.targetAndroidPackage 976 ) { 977 const sourceDir = path.join(tmpDir, step.sourceAndroidPath); 978 const targetDir = path.join( 979 ANDROID_DIR, 980 'expoview', 981 'src', 982 'main', 983 'java', 984 'versioned', 985 'host', 986 'exp', 987 'exponent', 988 step.targetAndroidPath 989 ); 990 991 if (cleanupTargetPath) { 992 console.log( 993 `\nCleaning up Android files at ${chalk.magenta( 994 path.relative(ANDROID_DIR, targetDir) 995 )} ...` 996 ); 997 998 await fs.remove(targetDir); 999 } 1000 await fs.mkdirs(targetDir); 1001 1002 console.log('\nCopying Android files ...'); 1003 1004 const javaFiles = await findAndroidFilesAsync(sourceDir); 1005 1006 await copyFilesAsync(javaFiles, sourceDir, targetDir); 1007 1008 const files = await findAndroidFilesAsync(targetDir); 1009 1010 for (const file of files) { 1011 await renamePackageAndroidAsync(file, step.sourceAndroidPackage, step.targetAndroidPackage); 1012 await step.onDidVendorAndroidFile?.(file); 1013 } 1014 } 1015 } 1016} 1017