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