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