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