import assert from 'assert'; import fs from 'fs-extra'; import minimatch from 'minimatch'; import path from 'path'; import { Podspec } from '../../CocoaPods'; import { EXPO_DIR, EXPOTOOLS_DIR, REACT_NATIVE_SUBMODULE_DIR } from '../../Constants'; import logger from '../../Logger'; import { transformFileAsync } from '../../Transforms'; import { applyPatchAsync } from '../../Utils'; import { VendoringTargetConfig } from '../types'; const config: VendoringTargetConfig = { name: 'Expo Go', platforms: { ios: { targetDirectory: 'ios/vendored/unversioned', }, android: { targetDirectory: 'android/vendored/unversioned', }, }, modules: { '@stripe/stripe-react-native': { source: 'https://github.com/stripe/stripe-react-native.git', ios: { async mutatePodspec(podspec: Podspec) { if (!podspec.pod_target_xcconfig) { podspec.pod_target_xcconfig = {}; } podspec.pod_target_xcconfig['HEADER_SEARCH_PATHS'] = '"${PODS_ROOT}/Stripe/Stripe3DS2" "${PODS_ROOT}/Headers/Public/Stripe"'; }, }, // android: { // excludeFiles: [ // 'android/src/main/java/com/reactnativestripesdk/GooglePayButtonManager.kt', // 'android/src/main/java/com/reactnativestripesdk/GooglePayButtonView.kt', // ], // transforms: { // content: [ // { // paths: 'StripeSdkPackage.kt', // find: /, GooglePayButtonManager\(\)/, // replaceWith: '', // }, // ], // }, // }, }, 'lottie-react-native': { source: 'lottie-react-native', sourceType: 'npm', ios: {}, android: { includeFiles: ['android/**'], excludeFiles: ['src/android/gradle.properties', 'src/android/gradle-maven-push.gradle'], }, }, 'react-native-gesture-handler': { source: 'https://github.com/software-mansion/react-native-gesture-handler.git', semverPrefix: '~', ios: {}, android: { async postCopyFilesHookAsync(sourceDirectory: string, targetDirectory: string) { const buildGradlePath = path.join(targetDirectory, 'android', 'build.gradle'); let buildGradle = await fs.readFile(buildGradlePath, 'utf-8'); buildGradle = buildGradle.replace( 'def shouldUseCommonInterfaceFromReanimated() {', 'def shouldUseCommonInterfaceFromReanimated() {\n return true\n' ); buildGradle = buildGradle.replace( /react-native-reanimated/g, 'vendored_unversioned_react-native-reanimated' ); await fs.writeFile(buildGradlePath, buildGradle); }, excludeFiles: [ 'android/gradle{/**,**}', 'android/settings.gradle', 'android/spotless.gradle', ], }, }, 'react-native-reanimated': { source: 'https://github.com/software-mansion/react-native-reanimated.git', semverPrefix: '~', ios: { async preReadPodspecHookAsync(podspecPath: string): Promise { const reaUtilsPath = path.join(podspecPath, '..', 'scripts', 'reanimated_utils.rb'); assert(fs.existsSync(reaUtilsPath), 'Cannot find `reanimated_utils`.'); const rnForkPath = path.join(REACT_NATIVE_SUBMODULE_DIR, '..'); let content = await fs.readFile(reaUtilsPath, 'utf-8'); content = content.replace( 'react_native_node_modules_dir = ', `react_native_node_modules_dir = "${rnForkPath}" #` ); await fs.writeFile(reaUtilsPath, content); return podspecPath; }, async mutatePodspec(podspec: Podspec) { const reactCommonDir = path.relative( EXPO_DIR, path.join(REACT_NATIVE_SUBMODULE_DIR, 'packages', 'react-native', 'ReactCommon') ); // `reanimated_utils.rb` generates wrong and confusing paths to ReactCommon headers, so we need to fix them. podspec.xcconfig['HEADER_SEARCH_PATHS'] = podspec.xcconfig[ 'HEADER_SEARCH_PATHS' ]?.replace(/"\$\(PODS_ROOT\)\/\.\.\/.+?"/g, `"\${PODS_ROOT}/../../${reactCommonDir}"`); }, transforms: { content: [ { paths: 'REAUIManager.mm', find: /^#import "RCT(.*).h"$/gm, replaceWith: '#import ', }, { paths: 'REAPropsNode.m', find: /^#import "React\/RCT(.*).h"$/gm, replaceWith: '#import ', }, { // remove the `#elif __has_include()` code block paths: 'NativeProxy.mm', find: /#elif __has_include\(\)\n.*(#import|makeHermesRuntime).*\n/gm, replaceWith: '', }, ], }, }, android: { excludeFiles: [ 'android/gradle{/**,**}', 'android/settings.gradle', 'android/spotless.gradle', 'android/README.md', 'android/rnVersionPatch/**', ], async postCopyFilesHookAsync(sourceDirectory: string, targetDirectory: string) { const excludedBlobs = ['**/*.md']; await fs.copy( path.join(sourceDirectory, 'Common'), path.join(targetDirectory, 'Common'), { filter: (src) => { const isExcluded = excludedBlobs.some((blob) => minimatch(src, blob)); return !isExcluded; }, overwrite: true, errorOnExist: false, } ); const reanimatedVersion = require(path.join(sourceDirectory, 'package.json')).version; await transformFileAsync(path.join(targetDirectory, 'android', 'build.gradle'), [ // set reanimated version { find: 'def REANIMATED_VERSION = getReanimatedVersion()', replaceWith: `def REANIMATED_VERSION = "${reanimatedVersion}"`, }, { find: 'def REANIMATED_MAJOR_VERSION = getReanimatedMajorVersion()', replaceWith: `def REANIMATED_MAJOR_VERSION = ${reanimatedVersion.split('.')[0]}`, }, ]); }, transforms: { content: [ { // Always uses hermes as reanimated worklet runtime on Expo Go paths: 'build.gradle', find: /\b(def JS_RUNTIME = \{)/g, replaceWith: '$1\n return "hermes" // Expo Go always uses hermes\n', }, { // react-native root dir is in react-native-lab/react-native paths: 'build.gradle', find: /\b(def reactNativeRootDir)\s*=.+$/gm, replaceWith: `$1 = Paths.get(projectDir.getPath(), '../../../../../react-native-lab/react-native/packages/react-native').toFile()`, }, { // no-op for extracting tasks paths: 'build.gradle', find: /\b(task (prepareHermes|unpackReactNativeAAR).*\{)$/gm, replaceWith: `$1\n return`, }, { // project `:ReactAndroid` to `:packages:react-native:ReactAndroid` paths: 'build.gradle', find: /(:ReactAndroid)/g, replaceWith: ':packages:react-native:$1', }, { // compileOnly hermes-engine paths: 'build.gradle', find: /implementation "com\.facebook\.react:hermes-android:?"\s*\/\/ version substituted by RNGP/g, replaceWith: 'compileOnly "com.facebook.react:hermes-android:${REACT_NATIVE_VERSION}"', }, { // expose `ReanimatedUIManagerFactory.create` publicly paths: 'ReanimatedUIManagerFactory.java', find: /((? *_pools;', }, { paths: 'RNCWKProcessPoolManager.m', find: '@implementation RNCWKProcessPoolManager', replaceWith: `@implementation RNCWKProcessPoolManager - (instancetype)init { if (self = [super init]) { _pools = [NSMutableDictionary new]; } return self; } - (WKProcessPool *)sharedProcessPoolForScopeKey:(NSString *)scopeKey { if (!scopeKey) { return [self sharedProcessPool]; } if (!_pools[scopeKey]) { _pools[scopeKey] = [[WKProcessPool alloc] init]; } return _pools[scopeKey]; } `, }, { paths: 'RNCWebViewImpl.h', find: /@interface RNCWebViewImpl : RCTView/, replaceWith: '$&\n@property (nonatomic, strong) NSString *scopeKey;', }, { paths: 'RNCWebViewImpl.m', find: /(\[\[RNCWKProcessPoolManager sharedManager\] sharedProcessPool)]/, replaceWith: '$1ForScopeKey:self.scopeKey]', }, { paths: 'RNCWebViewManager.mm', find: /@implementation RNCWebViewManager\s*{/, replaceWith: '$&\n NSString *_scopeKey;', }, { paths: 'RNCWebViewManager.mm', find: 'return [[RNCWebViewImpl alloc] init];', replaceWith: 'RNCWebViewImpl *webview = [[RNCWebViewImpl alloc] init];\n webview.scopeKey = _scopeKey;\n return webview;', }, { paths: 'RNCWebViewManager.mm', find: /RCT_EXPORT_MODULE\(RNCWebView\)/, replaceWith: `RCT_EXPORT_MODULE(RNCWebView) - (instancetype)initWithExperienceStableLegacyId:(NSString *)experienceStableLegacyId scopeKey:(NSString *)scopeKey easProjectId:(NSString *)easProjectId kernelServiceDelegate:(id)kernelServiceInstance params:(NSDictionary *)params { if (self = [super init]) { _scopeKey = scopeKey; } return self; }`, }, ], }, }, }, 'react-native-safe-area-context': { source: 'https://github.com/th3rdwave/react-native-safe-area-context', ios: {}, }, '@react-native-community/datetimepicker': { source: 'https://github.com/react-native-community/react-native-datetimepicker.git', // TODO: Uncomment the following once the new vendoring scripts support Android // android: { // transforms: { // content: [ // { // paths: 'RNTimePickerDialogFragment.java', // find: /"ClockTimePickerDialog"/, // replaceWith: '"ReactAndroidClockTimePickerDialog"', // }, // { // paths: 'RNTimePickerDialogFragment.java', // find: /"SpinnerTimePickerDialog"/, // replaceWith: '"ReactAndroidSpinnerTimePickerDialog"', // }, // { // paths: 'RNDatePickerDialogFragment.java', // find: /"CalendarDatePickerDialog"/, // replaceWith: '"ReactAndroidCalendarDatePickerDialog"', // }, // { // paths: 'RNDatePickerDialogFragment.java', // find: /"SpinnerDatePickerDialog"/, // replaceWith: '"ReactAndroidSpinnerDatePickerDialog"', // }, // ], // }, // }, }, // NOTE(brentvatne): masked-view has been renamed to // @react-native-masked-view/masked-view but we should synchronize moving // over to the new package name along with React Navigation '@react-native-masked-view/masked-view': { source: 'https://github.com/react-native-masked-view/masked-view', }, 'react-native-pager-view': { source: 'https://github.com/callstack/react-native-viewpager', ios: {}, android: { excludeFiles: ['android/gradle{/**,**}', 'android/settings.gradle'], }, }, '@react-native-segmented-control/segmented-control': { source: 'https://github.com/react-native-segmented-control/segmented-control', ios: {}, }, '@react-native-picker/picker': { source: 'https://github.com/react-native-picker/picker', }, '@react-native-community/slider': { source: 'https://github.com/callstack/react-native-slider', rootDir: 'package', ios: {}, android: { includeFiles: 'android/**', excludeFiles: ['android/gradle{/**,**}'], }, }, '@shopify/react-native-skia': { source: '@shopify/react-native-skia', sourceType: 'npm', ios: { async mutatePodspec(podspec: Podspec, sourceDirectory: string, targetDirectory: string) { const vendoredRootDir = path.dirname(path.dirname(path.dirname(targetDirectory))); assert(path.basename(vendoredRootDir) === 'vendored'); const vendoredCommonDir = path.join(vendoredRootDir, 'common'); const vendoredFrameworks = podspec.ios?.vendored_frameworks ?? []; for (const framework of vendoredFrameworks) { // create symlink from node_modules/@shopify/react-native-skia to common lib dir const sourceFrameworkPath = path.join( EXPO_DIR, 'node_modules/@shopify/react-native-skia', framework ); const sharedFrameworkPath = path.join(vendoredCommonDir, path.basename(framework)); await fs.unlink(sharedFrameworkPath); await fs.symlink( path.relative(path.dirname(sharedFrameworkPath), sourceFrameworkPath), sharedFrameworkPath ); // create symlink from common lib dir to module dir, because podspec cannot specify files out of its dir. const symlinkFrameworkPath = path.join(targetDirectory, framework); await fs.ensureDir(path.dirname(symlinkFrameworkPath)); await fs.symlink( path.relative(path.dirname(symlinkFrameworkPath), sharedFrameworkPath), symlinkFrameworkPath ); } // Workaround React-bridging header search path for react-native 0.69 with `generate_multiple_pod_projects=true` if (!podspec.pod_target_xcconfig) { podspec.pod_target_xcconfig = {}; } podspec.pod_target_xcconfig['HEADER_SEARCH_PATHS'] = '"$(PODS_TARGET_SRCROOT)/cpp/"/** "$(PODS_ROOT)/Headers/Private/React-bridging/react/bridging" "$(PODS_CONFIGURATION_BUILD_DIR)/React-bridging/react_bridging.framework/Headers"'; }, }, android: { includeFiles: ['android/**', 'cpp/**'], async postCopyFilesHookAsync(sourceDirectory, targetDirectory) { // create symlink from node_modules/@shopify/react-native-skia to common lib dir const libs = ['libskia.a', 'libskshaper.a', 'libsvg.a']; const archs = ['armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64']; for (const lib of libs) { for (const arch of archs) { const sourceLibPath = path.join( EXPO_DIR, 'node_modules/@shopify/react-native-skia/libs/android', arch, lib ); const commonLibPath = path.join(targetDirectory, '../../../common/libs', arch, lib); await fs.ensureDir(path.dirname(commonLibPath)); await fs.unlink(commonLibPath); await fs.symlink( path.relative(path.dirname(commonLibPath), sourceLibPath), commonLibPath ); } } // patch gradle and cmake files const patchFile = path.join( EXPOTOOLS_DIR, 'src/vendoring/config/react-native-skia.patch' ); const patchContent = await fs.readFile(patchFile, 'utf8'); try { await applyPatchAsync({ patchContent, cwd: targetDirectory, stripPrefixNum: 0, }); } catch (e) { logger.error( `Failed to apply patch: \`patch -p0 -d '${targetDirectory}' < ${patchFile}\`` ); throw e; } }, }, }, '@shopify/flash-list': { source: 'https://github.com/Shopify/flash-list', ios: {}, android: { excludeFiles: ['**/src/test/**'], }, }, '@react-native-async-storage/async-storage': { source: 'https://github.com/react-native-async-storage/async-storage.git', ios: { excludeFiles: 'example/**/*', async mutatePodspec(podspec: Podspec, sourceDirectory: string, targetDirectory: string) { // patch for scoped async storage const patchFile = path.join( EXPOTOOLS_DIR, 'src/vendoring/config/react-native-async-storage-scoped-storage-ios.patch' ); const patchContent = await fs.readFile(patchFile, 'utf8'); try { await applyPatchAsync({ patchContent, cwd: targetDirectory, stripPrefixNum: 0, }); } catch (e) { logger.error( `Failed to apply patch: \`patch -p0 -d '${targetDirectory}' < ${patchFile}\`` ); throw e; } }, }, android: { excludeFiles: 'example/**/*', async postCopyFilesHookAsync(sourceDirectory, targetDirectory) { // patch for scoped async storage const patchFile = path.join( EXPOTOOLS_DIR, 'src/vendoring/config/react-native-async-storage-scoped-storage-android.patch' ); const patchContent = await fs.readFile(patchFile, 'utf8'); try { await applyPatchAsync({ patchContent, cwd: targetDirectory, stripPrefixNum: 0, }); } catch (e) { logger.error( `Failed to apply patch: \`patch -p0 -d '${targetDirectory}' < ${patchFile}\`` ); throw e; } }, }, }, }, }; export default config;