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