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