1import assert from 'assert'; 2import fs from 'fs-extra'; 3import minimatch from 'minimatch'; 4import path from 'path'; 5 6import { Podspec } from '../../CocoaPods'; 7import { EXPO_DIR, EXPOTOOLS_DIR, REACT_NATIVE_SUBMODULE_DIR } from '../../Constants'; 8import logger from '../../Logger'; 9import { transformFileAsync } from '../../Transforms'; 10import { applyPatchAsync } from '../../Utils'; 11import { VendoringTargetConfig } from '../types'; 12 13const config: VendoringTargetConfig = { 14 name: 'Expo Go', 15 platforms: { 16 ios: { 17 targetDirectory: 'ios/vendored/unversioned', 18 }, 19 android: { 20 targetDirectory: 'android/vendored/unversioned', 21 }, 22 }, 23 modules: { 24 '@stripe/stripe-react-native': { 25 source: 'https://github.com/stripe/stripe-react-native.git', 26 ios: { 27 async mutatePodspec(podspec: Podspec) { 28 if (!podspec.pod_target_xcconfig) { 29 podspec.pod_target_xcconfig = {}; 30 } 31 podspec.pod_target_xcconfig['HEADER_SEARCH_PATHS'] = 32 '"${PODS_ROOT}/Stripe/Stripe3DS2" "${PODS_ROOT}/Headers/Public/Stripe"'; 33 }, 34 }, 35 // android: { 36 // excludeFiles: [ 37 // 'android/src/main/java/com/reactnativestripesdk/GooglePayButtonManager.kt', 38 // 'android/src/main/java/com/reactnativestripesdk/GooglePayButtonView.kt', 39 // ], 40 // transforms: { 41 // content: [ 42 // { 43 // paths: 'StripeSdkPackage.kt', 44 // find: /, GooglePayButtonManager\(\)/, 45 // replaceWith: '', 46 // }, 47 // ], 48 // }, 49 // }, 50 }, 51 'lottie-react-native': { 52 source: 'lottie-react-native', 53 sourceType: 'npm', 54 ios: {}, 55 android: { 56 includeFiles: ['android/**'], 57 excludeFiles: ['src/android/gradle.properties', 'src/android/gradle-maven-push.gradle'], 58 }, 59 }, 60 'react-native-gesture-handler': { 61 source: 'https://github.com/software-mansion/react-native-gesture-handler.git', 62 semverPrefix: '~', 63 ios: {}, 64 android: { 65 async postCopyFilesHookAsync(sourceDirectory: string, targetDirectory: string) { 66 const buildGradlePath = path.join(targetDirectory, 'android', 'build.gradle'); 67 let buildGradle = await fs.readFile(buildGradlePath, 'utf-8'); 68 buildGradle = buildGradle.replace( 69 'def shouldUseCommonInterfaceFromReanimated() {', 70 'def shouldUseCommonInterfaceFromReanimated() {\n return true\n' 71 ); 72 buildGradle = buildGradle.replace( 73 /react-native-reanimated/g, 74 'vendored_unversioned_react-native-reanimated' 75 ); 76 await fs.writeFile(buildGradlePath, buildGradle); 77 }, 78 excludeFiles: [ 79 'android/gradle{/**,**}', 80 'android/settings.gradle', 81 'android/spotless.gradle', 82 ], 83 }, 84 }, 85 'react-native-reanimated': { 86 source: 'https://github.com/software-mansion/react-native-reanimated.git', 87 semverPrefix: '~', 88 ios: { 89 async preReadPodspecHookAsync(podspecPath: string): Promise<string> { 90 const reaUtilsPath = path.join(podspecPath, '..', 'scripts', 'reanimated_utils.rb'); 91 assert(fs.existsSync(reaUtilsPath), 'Cannot find `reanimated_utils`.'); 92 const rnForkPath = path.join(REACT_NATIVE_SUBMODULE_DIR, '..'); 93 let content = await fs.readFile(reaUtilsPath, 'utf-8'); 94 content = content.replace( 95 'react_native_node_modules_dir = ', 96 `react_native_node_modules_dir = "${rnForkPath}" #` 97 ); 98 await fs.writeFile(reaUtilsPath, content); 99 return podspecPath; 100 }, 101 async mutatePodspec(podspec: Podspec) { 102 const reactCommonDir = path.relative( 103 EXPO_DIR, 104 path.join(REACT_NATIVE_SUBMODULE_DIR, 'packages', 'react-native', 'ReactCommon') 105 ); 106 // `reanimated_utils.rb` generates wrong and confusing paths to ReactCommon headers, so we need to fix them. 107 podspec.xcconfig['HEADER_SEARCH_PATHS'] = podspec.xcconfig[ 108 'HEADER_SEARCH_PATHS' 109 ]?.replace(/"\$\(PODS_ROOT\)\/\.\.\/.+?"/g, `"\${PODS_ROOT}/../../${reactCommonDir}"`); 110 }, 111 transforms: { 112 content: [ 113 { 114 paths: 'REAUIManager.mm', 115 find: /^#import "RCT(.*).h"$/gm, 116 replaceWith: '#import <React/RCT$1.h>', 117 }, 118 { 119 paths: 'REAPropsNode.m', 120 find: /^#import "React\/RCT(.*).h"$/gm, 121 replaceWith: '#import <React/RCT$1.h>', 122 }, 123 { 124 // remove the `#elif __has_include(<hermes/hermes.h>)` code block 125 paths: 'NativeProxy.mm', 126 find: /#elif __has_include\(<hermes\/hermes.h>\)\n.*(#import|makeHermesRuntime).*\n/gm, 127 replaceWith: '', 128 }, 129 ], 130 }, 131 }, 132 android: { 133 excludeFiles: [ 134 'android/gradle{/**,**}', 135 'android/settings.gradle', 136 'android/spotless.gradle', 137 'android/README.md', 138 'android/rnVersionPatch/**', 139 ], 140 async postCopyFilesHookAsync(sourceDirectory: string, targetDirectory: string) { 141 const excludedBlobs = ['**/*.md']; 142 143 await fs.copy( 144 path.join(sourceDirectory, 'Common'), 145 path.join(targetDirectory, 'Common'), 146 { 147 filter: (src) => { 148 const isExcluded = excludedBlobs.some((blob) => minimatch(src, blob)); 149 return !isExcluded; 150 }, 151 overwrite: true, 152 errorOnExist: false, 153 } 154 ); 155 const reanimatedVersion = require(path.join(sourceDirectory, 'package.json')).version; 156 await transformFileAsync(path.join(targetDirectory, 'android', 'build.gradle'), [ 157 // set reanimated version 158 { 159 find: 'def REANIMATED_VERSION = getReanimatedVersion()', 160 replaceWith: `def REANIMATED_VERSION = "${reanimatedVersion}"`, 161 }, 162 { 163 find: 'def REANIMATED_MAJOR_VERSION = getReanimatedMajorVersion()', 164 replaceWith: `def REANIMATED_MAJOR_VERSION = ${reanimatedVersion.split('.')[0]}`, 165 }, 166 ]); 167 }, 168 transforms: { 169 content: [ 170 { 171 // Always uses hermes as reanimated worklet runtime on Expo Go 172 paths: 'build.gradle', 173 find: /\b(def JS_RUNTIME = \{)/g, 174 replaceWith: '$1\n return "hermes" // Expo Go always uses hermes\n', 175 }, 176 { 177 // react-native root dir is in react-native-lab/react-native 178 paths: 'build.gradle', 179 find: /\b(def reactNativeRootDir)\s*=.+$/gm, 180 replaceWith: `$1 = Paths.get(projectDir.getPath(), '../../../../../react-native-lab/react-native/packages/react-native').toFile()`, 181 }, 182 { 183 // no-op for extracting tasks 184 paths: 'build.gradle', 185 find: /\b(task (prepareHermes|unpackReactNativeAAR).*\{)$/gm, 186 replaceWith: `$1\n return`, 187 }, 188 { 189 // project `:ReactAndroid` to `:packages:react-native:ReactAndroid` 190 paths: 'build.gradle', 191 find: /(:ReactAndroid)/g, 192 replaceWith: ':packages:react-native:$1', 193 }, 194 { 195 // compileOnly hermes-engine 196 paths: 'build.gradle', 197 find: /implementation "com\.facebook\.react:hermes-android:?"\s*\/\/ version substituted by RNGP/g, 198 replaceWith: 199 'compileOnly "com.facebook.react:hermes-android:${REACT_NATIVE_VERSION}"', 200 }, 201 { 202 // expose `ReanimatedUIManagerFactory.create` publicly 203 paths: 'ReanimatedUIManagerFactory.java', 204 find: /((?<!public )static UIManagerModule create\()/g, 205 replaceWith: 'public $1', 206 }, 207 ], 208 }, 209 }, 210 }, 211 'react-native-screens': { 212 source: 'https://github.com/software-mansion/react-native-screens.git', 213 semverPrefix: '~', 214 ios: {}, 215 android: { 216 excludeFiles: [ 217 'android/gradle{/**,**}', 218 'android/settings.gradle', 219 'android/spotless.gradle', 220 ], 221 }, 222 }, 223 'amazon-cognito-identity-js': { 224 source: 'https://github.com/aws-amplify/amplify-js.git', 225 }, 226 'react-native-view-shot': { 227 source: 'https://github.com/gre/react-native-view-shot.git', 228 }, 229 'react-native-svg': { 230 source: 'https://github.com/react-native-svg/react-native-svg', 231 ios: {}, 232 android: { 233 excludeFiles: [ 234 'android/gradle{/**,**}', 235 'android/settings.gradle', 236 'android/spotless.gradle', 237 'android/src/fabric/**', 238 'android/src/main/jni/**', 239 ], 240 }, 241 }, 242 'react-native-maps': { 243 source: 'https://github.com/react-native-maps/react-native-maps', 244 }, 245 '@react-native-community/netinfo': { 246 source: 'https://github.com/react-native-netinfo/react-native-netinfo', 247 ios: {}, 248 }, 249 'react-native-webview': { 250 source: 'https://github.com/react-native-webview/react-native-webview.git', 251 ios: { 252 transforms: { 253 // react-native-webview exposes `useSharedProcessPool` property which has to be handled differently in Expo Go. 254 // After upgrading this library, please ensure that proper patch is in place. 255 // See commit https://github.com/expo/expo/commit/3aeb66e33dc391399ea1c90fd166425130d17a12 256 content: [ 257 { 258 paths: 'RNCWKProcessPoolManager.h', 259 find: '- (WKProcessPool *)sharedProcessPool;', 260 replaceWith: '- (WKProcessPool *)sharedProcessPoolForScopeKey:(NSString *)scopeKey;', 261 }, 262 { 263 paths: 'RNCWKProcessPoolManager.m', 264 find: 'WKProcessPool *_sharedProcessPool;', 265 replaceWith: 266 'WKProcessPool *_sharedProcessPool;\n NSMutableDictionary<NSString *, WKProcessPool *> *_pools;', 267 }, 268 { 269 paths: 'RNCWKProcessPoolManager.m', 270 find: '@implementation RNCWKProcessPoolManager', 271 replaceWith: `@implementation RNCWKProcessPoolManager 272 273- (instancetype)init 274{ 275 if (self = [super init]) { 276 _pools = [NSMutableDictionary new]; 277 } 278 return self; 279} 280 281- (WKProcessPool *)sharedProcessPoolForScopeKey:(NSString *)scopeKey 282{ 283 if (!scopeKey) { 284 return [self sharedProcessPool]; 285 } 286 if (!_pools[scopeKey]) { 287 _pools[scopeKey] = [[WKProcessPool alloc] init]; 288 } 289 return _pools[scopeKey]; 290} 291`, 292 }, 293 { 294 paths: 'RNCWebViewImpl.h', 295 find: /@interface RNCWebViewImpl : RCTView/, 296 replaceWith: '$&\n@property (nonatomic, strong) NSString *scopeKey;', 297 }, 298 { 299 paths: 'RNCWebViewImpl.m', 300 find: /(\[\[RNCWKProcessPoolManager sharedManager\] sharedProcessPool)]/, 301 replaceWith: '$1ForScopeKey:self.scopeKey]', 302 }, 303 { 304 paths: 'RNCWebViewManager.mm', 305 find: /@implementation RNCWebViewManager\s*{/, 306 replaceWith: '$&\n NSString *_scopeKey;', 307 }, 308 { 309 paths: 'RNCWebViewManager.mm', 310 find: 'return [[RNCWebViewImpl alloc] init];', 311 replaceWith: 312 'RNCWebViewImpl *webview = [[RNCWebViewImpl alloc] init];\n webview.scopeKey = _scopeKey;\n return webview;', 313 }, 314 { 315 paths: 'RNCWebViewManager.mm', 316 find: /RCT_EXPORT_MODULE\(RNCWebView\)/, 317 replaceWith: `RCT_EXPORT_MODULE(RNCWebView) 318 319- (instancetype)initWithExperienceStableLegacyId:(NSString *)experienceStableLegacyId 320 scopeKey:(NSString *)scopeKey 321 easProjectId:(NSString *)easProjectId 322 kernelServiceDelegate:(id)kernelServiceInstance 323 params:(NSDictionary *)params 324{ 325 if (self = [super init]) { 326 _scopeKey = scopeKey; 327 } 328 return self; 329}`, 330 }, 331 ], 332 }, 333 }, 334 }, 335 'react-native-safe-area-context': { 336 source: 'https://github.com/th3rdwave/react-native-safe-area-context', 337 ios: {}, 338 }, 339 '@react-native-community/datetimepicker': { 340 source: 'https://github.com/react-native-community/react-native-datetimepicker.git', 341 // TODO: Uncomment the following once the new vendoring scripts support Android 342 // android: { 343 // transforms: { 344 // content: [ 345 // { 346 // paths: 'RNTimePickerDialogFragment.java', 347 // find: /"ClockTimePickerDialog"/, 348 // replaceWith: '"ReactAndroidClockTimePickerDialog"', 349 // }, 350 // { 351 // paths: 'RNTimePickerDialogFragment.java', 352 // find: /"SpinnerTimePickerDialog"/, 353 // replaceWith: '"ReactAndroidSpinnerTimePickerDialog"', 354 // }, 355 // { 356 // paths: 'RNDatePickerDialogFragment.java', 357 // find: /"CalendarDatePickerDialog"/, 358 // replaceWith: '"ReactAndroidCalendarDatePickerDialog"', 359 // }, 360 // { 361 // paths: 'RNDatePickerDialogFragment.java', 362 // find: /"SpinnerDatePickerDialog"/, 363 // replaceWith: '"ReactAndroidSpinnerDatePickerDialog"', 364 // }, 365 // ], 366 // }, 367 // }, 368 }, 369 // NOTE(brentvatne): masked-view has been renamed to 370 // @react-native-masked-view/masked-view but we should synchronize moving 371 // over to the new package name along with React Navigation 372 '@react-native-masked-view/masked-view': { 373 source: 'https://github.com/react-native-masked-view/masked-view', 374 }, 375 'react-native-pager-view': { 376 source: 'https://github.com/callstack/react-native-viewpager', 377 ios: {}, 378 android: { 379 excludeFiles: ['android/gradle{/**,**}', 'android/settings.gradle'], 380 }, 381 }, 382 '@react-native-segmented-control/segmented-control': { 383 source: 'https://github.com/react-native-segmented-control/segmented-control', 384 ios: {}, 385 }, 386 '@react-native-picker/picker': { 387 source: 'https://github.com/react-native-picker/picker', 388 }, 389 '@react-native-community/slider': { 390 source: 'https://github.com/callstack/react-native-slider', 391 rootDir: 'package', 392 ios: {}, 393 android: { 394 includeFiles: 'android/**', 395 excludeFiles: ['android/gradle{/**,**}'], 396 }, 397 }, 398 '@shopify/react-native-skia': { 399 source: '@shopify/react-native-skia', 400 sourceType: 'npm', 401 ios: { 402 async mutatePodspec(podspec: Podspec, sourceDirectory: string, targetDirectory: string) { 403 const vendoredRootDir = path.dirname(path.dirname(path.dirname(targetDirectory))); 404 assert(path.basename(vendoredRootDir) === 'vendored'); 405 const vendoredCommonDir = path.join(vendoredRootDir, 'common'); 406 const vendoredFrameworks = podspec.ios?.vendored_frameworks ?? []; 407 for (const framework of vendoredFrameworks) { 408 // create symlink from node_modules/@shopify/react-native-skia to common lib dir 409 const sourceFrameworkPath = path.join( 410 EXPO_DIR, 411 'node_modules/@shopify/react-native-skia', 412 framework 413 ); 414 const sharedFrameworkPath = path.join(vendoredCommonDir, path.basename(framework)); 415 await fs.unlink(sharedFrameworkPath); 416 await fs.symlink( 417 path.relative(path.dirname(sharedFrameworkPath), sourceFrameworkPath), 418 sharedFrameworkPath 419 ); 420 421 // create symlink from common lib dir to module dir, because podspec cannot specify files out of its dir. 422 const symlinkFrameworkPath = path.join(targetDirectory, framework); 423 await fs.ensureDir(path.dirname(symlinkFrameworkPath)); 424 await fs.symlink( 425 path.relative(path.dirname(symlinkFrameworkPath), sharedFrameworkPath), 426 symlinkFrameworkPath 427 ); 428 } 429 430 // Workaround React-bridging header search path for react-native 0.69 with `generate_multiple_pod_projects=true` 431 if (!podspec.pod_target_xcconfig) { 432 podspec.pod_target_xcconfig = {}; 433 } 434 podspec.pod_target_xcconfig['HEADER_SEARCH_PATHS'] = 435 '"$(PODS_TARGET_SRCROOT)/cpp/"/** "$(PODS_ROOT)/Headers/Private/React-bridging/react/bridging" "$(PODS_CONFIGURATION_BUILD_DIR)/React-bridging/react_bridging.framework/Headers"'; 436 }, 437 }, 438 android: { 439 includeFiles: ['android/**', 'cpp/**'], 440 async postCopyFilesHookAsync(sourceDirectory, targetDirectory) { 441 // create symlink from node_modules/@shopify/react-native-skia to common lib dir 442 const libs = ['libskia.a', 'libskshaper.a', 'libsvg.a']; 443 const archs = ['armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64']; 444 for (const lib of libs) { 445 for (const arch of archs) { 446 const sourceLibPath = path.join( 447 EXPO_DIR, 448 'node_modules/@shopify/react-native-skia/libs/android', 449 arch, 450 lib 451 ); 452 const commonLibPath = path.join(targetDirectory, '../../../common/libs', arch, lib); 453 await fs.ensureDir(path.dirname(commonLibPath)); 454 await fs.unlink(commonLibPath); 455 await fs.symlink( 456 path.relative(path.dirname(commonLibPath), sourceLibPath), 457 commonLibPath 458 ); 459 } 460 } 461 462 // patch gradle and cmake files 463 const patchFile = path.join( 464 EXPOTOOLS_DIR, 465 'src/vendoring/config/react-native-skia.patch' 466 ); 467 const patchContent = await fs.readFile(patchFile, 'utf8'); 468 try { 469 await applyPatchAsync({ 470 patchContent, 471 cwd: targetDirectory, 472 stripPrefixNum: 0, 473 }); 474 } catch (e) { 475 logger.error( 476 `Failed to apply patch: \`patch -p0 -d '${targetDirectory}' < ${patchFile}\`` 477 ); 478 throw e; 479 } 480 }, 481 }, 482 }, 483 '@shopify/flash-list': { 484 source: 'https://github.com/Shopify/flash-list', 485 ios: {}, 486 android: { 487 excludeFiles: ['**/src/test/**'], 488 }, 489 }, 490 '@react-native-async-storage/async-storage': { 491 source: 'https://github.com/react-native-async-storage/async-storage.git', 492 ios: { 493 excludeFiles: 'example/**/*', 494 async mutatePodspec(podspec: Podspec, sourceDirectory: string, targetDirectory: string) { 495 // patch for scoped async storage 496 const patchFile = path.join( 497 EXPOTOOLS_DIR, 498 'src/vendoring/config/react-native-async-storage-scoped-storage-ios.patch' 499 ); 500 const patchContent = await fs.readFile(patchFile, 'utf8'); 501 try { 502 await applyPatchAsync({ 503 patchContent, 504 cwd: targetDirectory, 505 stripPrefixNum: 0, 506 }); 507 } catch (e) { 508 logger.error( 509 `Failed to apply patch: \`patch -p0 -d '${targetDirectory}' < ${patchFile}\`` 510 ); 511 throw e; 512 } 513 }, 514 }, 515 android: { 516 excludeFiles: 'example/**/*', 517 async postCopyFilesHookAsync(sourceDirectory, targetDirectory) { 518 // patch for scoped async storage 519 const patchFile = path.join( 520 EXPOTOOLS_DIR, 521 'src/vendoring/config/react-native-async-storage-scoped-storage-android.patch' 522 ); 523 const patchContent = await fs.readFile(patchFile, 'utf8'); 524 try { 525 await applyPatchAsync({ 526 patchContent, 527 cwd: targetDirectory, 528 stripPrefixNum: 0, 529 }); 530 } catch (e) { 531 logger.error( 532 `Failed to apply patch: \`patch -p0 -d '${targetDirectory}' < ${patchFile}\`` 533 ); 534 throw e; 535 } 536 }, 537 }, 538 }, 539 }, 540}; 541 542export default config; 543