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