xref: /expo/tools/src/vendoring/legacy.ts (revision 5b564ca4)
1import chalk from 'chalk';
2import fs from 'fs-extra';
3import glob from 'glob-promise';
4import once from 'lodash/once';
5import ncp from 'ncp';
6import path from 'path';
7import xcode from 'xcode';
8
9import { EXPO_DIR } from '../Constants';
10import * as Directories from '../Directories';
11
12interface VendoredModuleUpdateStep {
13  iosPrefix?: string;
14  sourceIosPath?: string;
15  targetIosPath?: string;
16  sourceAndroidPath?: string;
17  targetAndroidPath?: string;
18  sourceAndroidPackage?: string;
19  targetAndroidPackage?: string;
20  recursive?: boolean;
21  updatePbxproj?: boolean;
22
23  // should cleanup target path before vendoring
24  cleanupTargetPath?: boolean;
25
26  /**
27   * Hook that is fired by the end of vendoring an Android file.
28   * You should use it to perform some extra operations that are not covered by the main flow.
29   * @deprecated Use {@link VendoredModuleConfig.moduleModifier} instead.
30   */
31  onDidVendorAndroidFile?: (file: string) => Promise<void>;
32}
33
34type ModuleModifier = (
35  moduleConfig: VendoredModuleConfig,
36  clonedProjectPath: string
37) => Promise<void>;
38
39interface VendoredModuleConfig {
40  repoUrl: string;
41  packageName?: string;
42  packageJsonPath?: string;
43  installableInManagedApps?: boolean;
44  semverPrefix?: '~' | '^';
45  skipCleanup?: boolean;
46  steps: VendoredModuleUpdateStep[];
47  /**
48   * These modifiers are run before files are copied to the target directory.
49   */
50  moduleModifier?: ModuleModifier;
51  warnings?: string[];
52}
53
54const IOS_DIR = Directories.getIosDir();
55const ANDROID_DIR = Directories.getAndroidDir();
56
57const SvgModifier: ModuleModifier = async function (
58  moduleConfig: VendoredModuleConfig,
59  clonedProjectPath: string
60): Promise<void> {
61  const removeMacFiles = async () => {
62    const macPattern = path.join(clonedProjectPath, 'apple', '**', '*.macos.@(h|m)');
63    const macFiles = await glob(macPattern);
64    for (const file of macFiles) {
65      await fs.remove(file);
66    }
67  };
68
69  const addHeaderImport = async () => {
70    const targetPath = path.join(clonedProjectPath, 'apple', 'Text', 'RNSVGTopAlignedLabel.h');
71    const content = await fs.readFile(targetPath, 'utf8');
72    const transformedContent = `#import "RNSVGUIKit.h"\n${content}`;
73    await fs.writeFile(targetPath, transformedContent, 'utf8');
74  };
75
76  await removeMacFiles();
77  await addHeaderImport();
78};
79
80const MapsModifier: ModuleModifier = async function (
81  moduleConfig: VendoredModuleConfig,
82  clonedProjectPath: string
83): Promise<void> {
84  const fixGoogleMapsImports = async () => {
85    const targetPath = path.join(clonedProjectPath, 'ios', 'AirGoogleMaps', 'AIRGoogleMap.m');
86    let content = await fs.readFile(targetPath, 'utf8');
87    content = content.replace(/^#import "(GMU.+?\.h)"$/gm, '#import <Google-Maps-iOS-Utils/$1>');
88    await fs.writeFile(targetPath, content, 'utf8');
89  };
90
91  await fixGoogleMapsImports();
92};
93
94const ReanimatedModifier: ModuleModifier = async function (
95  moduleConfig: VendoredModuleConfig,
96  clonedProjectPath: string
97): Promise<void> {
98  const firstStep = moduleConfig.steps[0];
99  const androidMainPathReanimated = path.join(clonedProjectPath, 'android', 'src', 'main');
100  const androidMainPathExpoview = path.join(ANDROID_DIR, 'expoview', 'src', 'main');
101  const JNIOldPackagePrefix = firstStep.sourceAndroidPackage!.split('.').join('/');
102  const JNINewPackagePrefix = firstStep.targetAndroidPackage!.split('.').join('/');
103
104  const replaceJNIPackages = async () => {
105    const cppPattern = path.join(androidMainPathReanimated, 'cpp', '**', '*.@(h|cpp)');
106    const androidCpp = await glob(cppPattern);
107    for (const file of androidCpp) {
108      const content = await fs.readFile(file, 'utf8');
109      const transformedContent = content.split(JNIOldPackagePrefix).join(JNINewPackagePrefix);
110      await fs.writeFile(file, transformedContent, 'utf8');
111    }
112  };
113
114  const copyCPP = async () => {
115    const dirs = ['Common', 'cpp'];
116    for (const dir of dirs) {
117      await fs.remove(path.join(androidMainPathExpoview, dir)); // clean
118      // copy
119      await new Promise<void>((res, rej) => {
120        ncp(
121          path.join(androidMainPathReanimated, dir),
122          path.join(androidMainPathExpoview, dir),
123          { dereference: true },
124          () => {
125            res();
126          }
127        );
128      });
129    }
130  };
131
132  const prepareIOSNativeFiles = async () => {
133    const patternCommon = path.join(clonedProjectPath, 'Common', '**', '*.@(h|mm|cpp)');
134    const patternNative = path.join(clonedProjectPath, 'ios', 'native', '**', '*.@(h|mm|cpp)');
135    const commonFiles = await glob(patternCommon);
136    const iosOnlyFiles = await glob(patternNative);
137    const files = [...commonFiles, ...iosOnlyFiles];
138    for (const file of files) {
139      console.log(file);
140      const fileName = file.split(path.sep).slice(-1)[0];
141      await fs.copy(file, path.join(clonedProjectPath, 'ios', fileName));
142    }
143
144    await fs.remove(path.join(clonedProjectPath, 'ios', 'native'));
145  };
146
147  const transformGestureHandlerImports = async () => {
148    const javaFiles = await glob(path.join(clonedProjectPath, 'android', '**', '*.java'));
149    await Promise.all(
150      javaFiles.map(async (file) => {
151        let content = await fs.readFile(file, 'utf8');
152        content = content.replace(
153          /^import com\.swmansion\.common\./gm,
154          'import versioned.host.exp.exponent.modules.api.components.gesturehandler.'
155        );
156        await fs.writeFile(file, content);
157      })
158    );
159  };
160
161  const applyRNVersionPatches = async () => {
162    const rnVersion = '0.67.2';
163    const patchVersion = rnVersion.split('.')[1];
164    const patchSourceDir = path.join(clonedProjectPath, 'android', 'rnVersionPatch', patchVersion);
165    const javaFiles = await glob('**/*.java', {
166      cwd: patchSourceDir,
167    });
168    await Promise.all(
169      javaFiles.map(async (file) => {
170        const srcPath = path.join(patchSourceDir, file);
171        const dstPath = path.join(
172          clonedProjectPath,
173          'android',
174          'src',
175          'main',
176          'java',
177          'com',
178          'swmansion',
179          'reanimated',
180          file
181        );
182        await fs.copy(srcPath, dstPath);
183      })
184    );
185  };
186
187  await applyRNVersionPatches();
188  await replaceJNIPackages();
189  await copyCPP();
190  await prepareIOSNativeFiles();
191  await transformGestureHandlerImports();
192};
193
194const PickerModifier: ModuleModifier = once(async function (moduleConfig, clonedProjectPath) {
195  const addResourceImportAsync = async () => {
196    const files = [
197      `${clonedProjectPath}/android/src/main/java/com/reactnativecommunity/picker/ReactPicker.java`,
198      `${clonedProjectPath}/android/src/main/java/com/reactnativecommunity/picker/ReactPickerManager.java`,
199    ];
200    await Promise.all(
201      files
202        .map((file) => path.resolve(file))
203        .map(async (file) => {
204          let content = await fs.readFile(file, 'utf8');
205          content = content.replace(/^(package .+)$/gm, '$1\n\nimport host.exp.expoview.R;');
206          await fs.writeFile(file, content, 'utf8');
207        })
208    );
209  };
210
211  await addResourceImportAsync();
212});
213
214const GestureHandlerModifier: ModuleModifier = async function (
215  moduleConfig: VendoredModuleConfig,
216  clonedProjectPath: string
217): Promise<void> {
218  const addResourceImportAsync = async () => {
219    const files = [
220      `${clonedProjectPath}/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt`,
221    ];
222    await Promise.all(
223      files
224        .map((file) => path.resolve(file))
225        .map(async (file) => {
226          let content = await fs.readFile(file, 'utf8');
227          content = content.replace(/^(package .+)$/gm, '$1\nimport host.exp.expoview.R');
228          await fs.writeFile(file, content, 'utf8');
229        })
230    );
231  };
232
233  const replaceOrAddBuildConfigImportAsync = async () => {
234    const files = [
235      `${clonedProjectPath}/android/lib/src/main/java/com/swmansion/gesturehandler/GestureHandler.kt`,
236      `${clonedProjectPath}/android/src/main/java/com/swmansion/gesturehandler/RNGestureHandlerPackage.kt`,
237      `${clonedProjectPath}/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerModule.kt`,
238    ];
239    await Promise.all(
240      files
241        .map((file) => path.resolve(file))
242        .map(async (file) => {
243          let content = await fs.readFile(file, 'utf8');
244          content = content
245            .replace(/^.*\.BuildConfig$/gm, '')
246            .replace(/^(package .+)$/gm, '$1\nimport host.exp.expoview.BuildConfig');
247          await fs.writeFile(file, content, 'utf8');
248        })
249    );
250  };
251
252  const transformImportsAsync = async () => {
253    const files = [
254      `${clonedProjectPath}/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerModule.kt`,
255    ];
256    await Promise.all(
257      files
258        .map((file) => path.resolve(file))
259        .map(async (file) => {
260          let content = await fs.readFile(file, 'utf8');
261          content = content.replace(
262            /^import com\.swmansion\.common\./gm,
263            'import versioned.host.exp.exponent.modules.api.components.gesturehandler.'
264          );
265          await fs.writeFile(file, content, 'utf8');
266        })
267    );
268  };
269
270  const commentOurReanimatedCode = async () => {
271    const files = [
272      `${clonedProjectPath}/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerModule.kt`,
273    ];
274    await Promise.all(
275      files
276        .map((file) => path.resolve(file))
277        .map(async (file) => {
278          let content = await fs.readFile(file, 'utf8');
279          content = content.replace(
280            'ReanimatedEventDispatcher.sendEvent(event, reactApplicationContext)',
281            '// $& // COMMENTED OUT BY VENDORING SCRIPT'
282          );
283          await fs.writeFile(file, content, 'utf8');
284        })
285    );
286  };
287
288  await addResourceImportAsync();
289  await replaceOrAddBuildConfigImportAsync();
290  await transformImportsAsync();
291  await commentOurReanimatedCode();
292};
293
294const ScreensModifier: ModuleModifier = async function (
295  moduleConfig: VendoredModuleConfig,
296  clonedProjectPath: string
297): Promise<void> {
298  const viewmanagersExpoviewDir = path.join(
299    ANDROID_DIR,
300    'expoview',
301    'src',
302    'main',
303    'java',
304    'com',
305    'facebook',
306    'react',
307    'viewmanagers'
308  );
309
310  const copyPaperViewManager = async () => {
311    await fs.remove(viewmanagersExpoviewDir); // clean
312    // copy
313    await new Promise<void>((res, rej) => {
314      ncp(
315        path.join(
316          clonedProjectPath,
317          'android',
318          'src',
319          'paper',
320          'java',
321          'com',
322          'facebook',
323          'react',
324          'viewmanagers'
325        ),
326        viewmanagersExpoviewDir,
327        { dereference: true },
328        () => {
329          res();
330        }
331      );
332    });
333  };
334
335  await copyPaperViewManager();
336};
337
338const vendoredModulesConfig: { [key: string]: VendoredModuleConfig } = {
339  'react-native-gesture-handler': {
340    repoUrl: 'https://github.com/software-mansion/react-native-gesture-handler.git',
341    installableInManagedApps: true,
342    semverPrefix: '~',
343    moduleModifier: GestureHandlerModifier,
344    steps: [
345      {
346        sourceAndroidPath: 'android/src/main/java/com/swmansion/gesturehandler',
347        targetAndroidPath: 'modules/api/components/gesturehandler',
348        sourceAndroidPackage: 'com.swmansion.gesturehandler',
349        targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.components.gesturehandler',
350      },
351      {
352        sourceAndroidPath: 'android/lib/src/main/java/com/swmansion/gesturehandler',
353        targetAndroidPath: 'modules/api/components/gesturehandler',
354        sourceAndroidPackage: 'com.swmansion.gesturehandler',
355        targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.components.gesturehandler',
356        cleanupTargetPath: false, // first step cleans parent directory
357      },
358      {
359        sourceAndroidPath: 'android/common/src/main/java/com/swmansion/common',
360        targetAndroidPath: 'modules/api/components/gesturehandler/common',
361        sourceAndroidPackage: 'com.swmansion.common',
362        targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.components.gesturehandler',
363        cleanupTargetPath: false, // first steps cleans parent directory
364      },
365      {
366        sourceAndroidPath: 'android/src/paper/java/com/facebook/react/viewmanagers',
367        targetAndroidPath: '../../../../com/facebook/react/viewmanagers',
368        sourceAndroidPackage: 'com.facebook.react.viewmanagers',
369        targetAndroidPackage: 'com.facebook.react.viewmanagers',
370        cleanupTargetPath: false,
371      },
372      {
373        sourceAndroidPath: 'android/src/paper/java/com/swmansion/gesturehandler',
374        targetAndroidPath: 'modules/api/components/gesturehandler',
375        sourceAndroidPackage: 'com.swmansion.gesturehandler',
376        targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.components.gesturehandler',
377        cleanupTargetPath: false,
378      },
379    ],
380  },
381  'react-native-reanimated': {
382    repoUrl: 'https://github.com/software-mansion/react-native-reanimated.git',
383    installableInManagedApps: true,
384    semverPrefix: '~',
385    moduleModifier: ReanimatedModifier,
386    steps: [
387      {
388        recursive: true,
389        sourceIosPath: 'ios',
390        targetIosPath: 'Api/Reanimated',
391        sourceAndroidPath: 'android/src/main/java/com/swmansion/reanimated',
392        targetAndroidPath: 'modules/api/reanimated',
393        sourceAndroidPackage: 'com.swmansion.reanimated',
394        targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.reanimated',
395        onDidVendorAndroidFile: async (file: string) => {
396          const fileName = path.basename(file);
397          if (fileName === 'ReanimatedUIManager.java') {
398            // reanimated tries to override react native `UIManager` implementation.
399            // this file is placed inside `com/swmansion/reanimated/layoutReanimation/ReanimatedUIManager.java`
400            // but its package name is `package com.facebook.react.uimanager;`.
401            // we should put this into correct folder structure so that other files can
402            // `import com.facebook.react.uimanager.ReanimatedUIManager`
403            await fs.move(
404              file,
405              path.join(
406                ANDROID_DIR,
407                'expoview',
408                'src',
409                'main',
410                'java',
411                'com',
412                'facebook',
413                'react',
414                'uimanager',
415                fileName
416              ),
417              { overwrite: true }
418            );
419          }
420        },
421      },
422    ],
423    warnings: [
424      `NOTE: Any files in ${chalk.magenta(
425        'com.facebook.react'
426      )} will not be updated -- you'll need to add these to expoview manually!`,
427      `NOTE: Some imports have to be changed from ${chalk.magenta('<>')} form to
428      ${chalk.magenta('""')}`,
429    ],
430  },
431  'react-native-screens': {
432    repoUrl: 'https://github.com/software-mansion/react-native-screens.git',
433    installableInManagedApps: true,
434    semverPrefix: '~',
435    moduleModifier: ScreensModifier,
436    steps: [
437      {
438        sourceIosPath: 'ios',
439        targetIosPath: 'Api/Screens',
440        sourceAndroidPath: 'android/src/main/java/com/swmansion/rnscreens',
441        targetAndroidPath: 'modules/api/screens',
442        sourceAndroidPackage: 'com.swmansion.rnscreens',
443        targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.screens',
444        onDidVendorAndroidFile: async (file: string) => {
445          const filename = path.basename(file);
446          const CHANGES = {
447            'ScreenStack.kt': {
448              find: /(?=^class ScreenStack\()/m,
449              replaceWith: `import host.exp.expoview.R\n\n`,
450            },
451            'ScreenStackHeaderConfig.kt': {
452              find: /(?=^class ScreenStackHeaderConfig\()/m,
453              replaceWith: `import host.exp.expoview.BuildConfig\nimport host.exp.expoview.R\n\n`,
454            },
455            'RNScreensPackage.kt': {
456              find: /(?=^class RNScreensPackage\ :)/m,
457              replaceWith: `import host.exp.expoview.BuildConfig\n\n`,
458            },
459            'Screen.kt': {
460              find: /(?=^@SuppressLint\(\"ViewConstructor\"\)\nclass Screen)/m,
461              replaceWith: `import host.exp.expoview.BuildConfig\n\n`,
462            },
463          };
464
465          const fileConfig = CHANGES[filename];
466          if (!fileConfig) {
467            return;
468          }
469
470          const originalFileContent = await fs.readFile(file, 'utf8');
471          const newFileContent = originalFileContent.replace(
472            fileConfig.find,
473            fileConfig.replaceWith
474          );
475          await fs.writeFile(file, newFileContent, 'utf8');
476        },
477      },
478      {
479        cleanupTargetPath: false,
480        sourceAndroidPath: 'android/src/paper/java/com/swmansion/rnscreens',
481        targetAndroidPath: 'modules/api/screens',
482        sourceAndroidPackage: 'com.swmansion.rnscreens',
483        targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.screens',
484      },
485    ],
486  },
487  'amazon-cognito-identity-js': {
488    repoUrl: 'https://github.com/aws-amplify/amplify-js.git',
489    installableInManagedApps: false,
490    steps: [
491      {
492        sourceIosPath: 'packages/amazon-cognito-identity-js/ios',
493        targetIosPath: 'Api/Cognito',
494        sourceAndroidPath:
495          'packages/amazon-cognito-identity-js/android/src/main/java/com/amazonaws',
496        targetAndroidPath: 'modules/api/cognito',
497        sourceAndroidPackage: 'com.amazonaws',
498        targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.cognito',
499      },
500    ],
501  },
502  'react-native-view-shot': {
503    repoUrl: 'https://github.com/gre/react-native-view-shot.git',
504    steps: [
505      {
506        sourceIosPath: 'ios',
507        targetIosPath: 'Api/ViewShot',
508        sourceAndroidPath: 'android/src/main/java/fr/greweb/reactnativeviewshot',
509        targetAndroidPath: 'modules/api/viewshot',
510        sourceAndroidPackage: 'fr.greweb.reactnativeviewshot',
511        targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.viewshot',
512      },
513    ],
514  },
515  'react-native-branch': {
516    repoUrl: 'https://github.com/BranchMetrics/react-native-branch-deep-linking.git',
517    steps: [
518      {
519        sourceIosPath: 'ios',
520        targetIosPath: '../../../../packages/expo-branch/ios/EXBranch/RNBranch',
521        sourceAndroidPath: 'android/src/main/java/io/branch/rnbranch',
522        targetAndroidPath:
523          '../../../../../../../../../packages/expo-branch/android/src/main/java/io/branch/rnbranch',
524        sourceAndroidPackage: 'io.branch.rnbranch',
525        targetAndroidPackage: 'io.branch.rnbranch',
526        recursive: false,
527        updatePbxproj: false,
528      },
529    ],
530  },
531  'lottie-react-native': {
532    repoUrl: 'https://github.com/react-native-community/lottie-react-native.git',
533    installableInManagedApps: true,
534    steps: [
535      {
536        iosPrefix: 'LRN',
537        sourceIosPath: 'src/ios/LottieReactNative',
538        targetIosPath: 'Api/Components/Lottie',
539        sourceAndroidPath: 'src/android/src/main/java/com/airbnb/android/react/lottie',
540        targetAndroidPath: 'modules/api/components/lottie',
541        sourceAndroidPackage: 'com.airbnb.android.react.lottie',
542        targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.components.lottie',
543      },
544    ],
545  },
546  'react-native-svg': {
547    repoUrl: 'https://github.com/react-native-community/react-native-svg.git',
548    installableInManagedApps: true,
549    moduleModifier: SvgModifier,
550    steps: [
551      {
552        recursive: true,
553        sourceIosPath: 'apple',
554        targetIosPath: 'Api/Components/Svg',
555        sourceAndroidPath: 'android/src/main/java/com/horcrux/svg',
556        targetAndroidPath: 'modules/api/components/svg',
557        sourceAndroidPackage: 'com.horcrux.svg',
558        targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.components.svg',
559      },
560    ],
561  },
562  'react-native-maps': {
563    repoUrl: 'https://github.com/react-native-community/react-native-maps.git',
564    installableInManagedApps: true,
565    moduleModifier: MapsModifier,
566    steps: [
567      {
568        sourceIosPath: 'ios/AirGoogleMaps',
569        targetIosPath: 'Api/Components/GoogleMaps',
570      },
571      {
572        recursive: true,
573        sourceIosPath: 'ios/AirMaps',
574        targetIosPath: 'Api/Components/Maps',
575        sourceAndroidPath: 'android/src/main/java/com/rnmaps/maps',
576        targetAndroidPath: 'modules/api/components/maps',
577        sourceAndroidPackage: 'com.rnmaps.maps',
578        targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.components.maps',
579      },
580    ],
581  },
582  '@react-native-community/netinfo': {
583    repoUrl: 'https://github.com/react-native-community/react-native-netinfo.git',
584    installableInManagedApps: true,
585    steps: [
586      {
587        sourceIosPath: 'ios',
588        targetIosPath: 'Api/NetInfo',
589        sourceAndroidPath: 'android/src/main/java/com/reactnativecommunity/netinfo',
590        targetAndroidPath: 'modules/api/netinfo',
591        sourceAndroidPackage: 'com.reactnativecommunity.netinfo',
592        targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.netinfo',
593      },
594    ],
595  },
596  'react-native-webview': {
597    repoUrl: 'https://github.com/react-native-community/react-native-webview.git',
598    installableInManagedApps: true,
599    steps: [
600      {
601        sourceIosPath: 'apple',
602        targetIosPath: 'Api/Components/WebView',
603        sourceAndroidPath: 'android/src/main/java/com/reactnativecommunity/webview',
604        targetAndroidPath: 'modules/api/components/webview',
605        sourceAndroidPackage: 'com.reactnativecommunity.webview',
606        targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.components.webview',
607      },
608      {
609        sourceAndroidPath: 'android/src/oldarch/com/reactnativecommunity/webview',
610        cleanupTargetPath: false,
611        targetAndroidPath: 'modules/api/components/webview',
612        sourceAndroidPackage: 'com.reactnativecommunity.webview',
613        targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.components.webview',
614        onDidVendorAndroidFile: async (file: string) => {
615          const fileName = path.basename(file);
616          if (fileName === 'RNCWebViewPackage.java') {
617            let content = await fs.readFile(file, 'utf8');
618            content = content.replace(
619              /^(package .+)$/gm,
620              '$1\nimport host.exp.expoview.BuildConfig;'
621            );
622            await fs.writeFile(file, content, 'utf8');
623          }
624        },
625      },
626    ],
627  },
628  'react-native-safe-area-context': {
629    repoUrl: 'https://github.com/th3rdwave/react-native-safe-area-context',
630    steps: [
631      {
632        sourceIosPath: 'ios',
633        targetIosPath: 'Api/SafeAreaContext',
634        sourceAndroidPath: 'android/src/main/java/com/th3rdwave/safeareacontext',
635        targetAndroidPath: 'modules/api/safeareacontext',
636        sourceAndroidPackage: 'com.th3rdwave.safeareacontext',
637        targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.safeareacontext',
638        onDidVendorAndroidFile: async (file: string) => {
639          const fileName = path.basename(file);
640          if (fileName === 'SafeAreaContextPackage.kt') {
641            let content = await fs.readFile(file, 'utf8');
642            content = content.replace(
643              /^(package .+)$/gm,
644              '$1\nimport host.exp.expoview.BuildConfig'
645            );
646            await fs.writeFile(file, content, 'utf8');
647          }
648        },
649      },
650      {
651        sourceIosPath: 'ios/SafeAreaContextSpec',
652        targetIosPath: 'Api/SafeAreaContext',
653        cleanupTargetPath: false,
654      },
655      {
656        sourceAndroidPath: 'android/src/paper/java/com/th3rdwave/safeareacontext',
657        targetAndroidPath: 'modules/api/safeareacontext',
658        sourceAndroidPackage: 'com.th3rdwave.safeareacontext',
659        targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.safeareacontext',
660        cleanupTargetPath: false,
661      },
662      {
663        sourceAndroidPath: 'android/src/paper/java/com/facebook/react/viewmanagers',
664        targetAndroidPath: 'modules/api/safeareacontext',
665        sourceAndroidPackage: 'com.facebook.react.viewmanagers',
666        targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.safeareacontext',
667        cleanupTargetPath: false,
668      },
669    ],
670  },
671  '@react-native-community/datetimepicker': {
672    repoUrl: 'https://github.com/react-native-community/react-native-datetimepicker.git',
673    installableInManagedApps: true,
674    steps: [
675      {
676        sourceIosPath: 'ios',
677        targetIosPath: 'Api/Components/DateTimePicker',
678        sourceAndroidPath: 'android/src/main/java/com/reactcommunity/rndatetimepicker',
679        targetAndroidPath: 'modules/api/components/datetimepicker',
680        sourceAndroidPackage: 'com.reactcommunity.rndatetimepicker',
681        targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.components.datetimepicker',
682      },
683      {
684        sourceAndroidPath: 'android/src/paper/java/com/reactcommunity/rndatetimepicker',
685        targetAndroidPath: 'modules/api/components/datetimepicker',
686        sourceAndroidPackage: 'com.reactcommunity.rndatetimepicker',
687        targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.components.datetimepicker',
688        cleanupTargetPath: false,
689      },
690    ],
691    warnings: [
692      `NOTE: In Expo, native Android styles are prefixed with ${chalk.magenta(
693        'ReactAndroid'
694      )}. Please ensure that ${chalk.magenta(
695        'resourceName'
696      )}s used for grabbing style of dialogs are being resolved properly.`,
697    ],
698  },
699  '@react-native-masked-view/masked-view': {
700    repoUrl: 'https://github.com/react-native-masked-view/masked-view',
701    installableInManagedApps: true,
702    steps: [
703      {
704        sourceIosPath: 'ios',
705        targetIosPath: 'Api/Components/MaskedView',
706        sourceAndroidPath: 'android/src/main/java/org/reactnative/maskedview',
707        targetAndroidPath: 'modules/api/components/maskedview',
708        sourceAndroidPackage: 'org.reactnative.maskedview',
709        targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.components.maskedview',
710      },
711    ],
712  },
713  '@react-native-segmented-control/segmented-control': {
714    repoUrl: 'https://github.com/react-native-segmented-control/segmented-control',
715    installableInManagedApps: true,
716    steps: [
717      {
718        sourceIosPath: 'ios',
719        targetIosPath: 'Api/Components/SegmentedControl',
720      },
721    ],
722  },
723  '@react-native-picker/picker': {
724    repoUrl: 'https://github.com/react-native-picker/picker',
725    installableInManagedApps: true,
726    moduleModifier: PickerModifier,
727    steps: [
728      {
729        sourceIosPath: 'ios',
730        targetIosPath: 'Api/Components/Picker',
731        sourceAndroidPath: 'android/src/main/java/com/reactnativecommunity/picker',
732        targetAndroidPath: 'modules/api/components/picker',
733        sourceAndroidPackage: 'com.reactnativecommunity.picker',
734        targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.components.picker',
735      },
736    ],
737  },
738  '@stripe/stripe-react-native': {
739    repoUrl: 'https://github.com/stripe/stripe-react-native',
740    installableInManagedApps: true,
741    steps: [
742      {
743        sourceAndroidPath: 'android/src/main/java/com/reactnativestripesdk',
744        targetAndroidPath: 'modules/api/components/reactnativestripesdk',
745        sourceAndroidPackage: 'com.reactnativestripesdk',
746        targetAndroidPackage:
747          'versioned.host.exp.exponent.modules.api.components.reactnativestripesdk',
748      },
749    ],
750  },
751};
752
753async function renameIOSSymbolsAsync(file: string, iosPrefix: string) {
754  const content = await fs.readFile(file, 'utf8');
755
756  // Do something more sophisticated if this causes issues with more complex modules.
757  const transformedContent = content.replace(new RegExp(iosPrefix, 'g'), 'EX');
758  const newFileName = file.replace(iosPrefix, 'EX');
759
760  await fs.writeFile(newFileName, transformedContent, 'utf8');
761  await fs.remove(file);
762}
763
764async function findObjcFilesAsync(dir: string, recursive: boolean): Promise<string[]> {
765  const pattern = path.join(dir, recursive ? '**' : '', '*.@(h|m|c|mm|cpp|swift)');
766  return await glob(pattern);
767}
768
769async function renamePackageAndroidAsync(
770  file: string,
771  sourceAndroidPackage: string,
772  targetAndroidPackage: string
773) {
774  const content = await fs.readFile(file, 'utf8');
775
776  // Note: this only works for a single package. If react-native-svg separates
777  // its code into multiple packages we will have to do something more
778  // sophisticated here.
779  const transformedContent = content.replace(
780    new RegExp(sourceAndroidPackage, 'g'),
781    targetAndroidPackage
782  );
783
784  await fs.writeFile(file, transformedContent, 'utf8');
785}
786
787async function findAndroidFilesAsync(dir: string): Promise<string[]> {
788  const pattern = path.join(dir, '**', '*.@(java|kt)');
789  return await glob(pattern);
790}
791
792async function loadXcodeprojFileAsync(file: string): Promise<any> {
793  return new Promise((resolve, reject) => {
794    const pbxproj = xcode.project(file);
795    pbxproj.parse((err) => (err ? reject(err) : resolve(pbxproj)));
796  });
797}
798
799function pbxGroupChild(file) {
800  const obj = Object.create(null);
801  obj.value = file.fileRef;
802  obj.comment = file.basename;
803  return obj;
804}
805
806function pbxGroupHasChildWithRef(group: any, ref: string): boolean {
807  return group.children.some((child) => child.value === ref);
808}
809
810async function addFileToPbxprojAsync(
811  filePath: string,
812  targetDir: string,
813  pbxproj: any
814): Promise<void> {
815  const fileName = path.basename(filePath);
816
817  // The parent group of the target directory that should already be created in the project, e.g. `Components` or `Api`.
818  const targetGroup = pbxproj.pbxGroupByName(path.basename(path.dirname(targetDir)));
819
820  if (!pbxproj.hasFile(fileName)) {
821    console.log(`Adding ${chalk.magenta(fileName)} to pbxproj configuration ...`);
822
823    const fileOptions = {
824      // Mute warnings from 3rd party modules.
825      compilerFlags: '-w',
826    };
827
828    // The group name is mostly just a basename of the path.
829    const groupName = path.basename(path.dirname(filePath));
830
831    // Add a file to pbxproj tree.
832    const file =
833      path.extname(fileName) === '.h'
834        ? pbxproj.addHeaderFile(fileName, fileOptions, groupName)
835        : pbxproj.addSourceFile(fileName, fileOptions, groupName);
836
837    // Search for the group where the file should be placed.
838    const group = pbxproj.pbxGroupByName(groupName);
839
840    // Our files has `includeInIndex` set to 1, so let's continue doing that.
841    file.includeInIndex = 1;
842
843    if (group) {
844      // Add a file if it is not there already.
845      if (!pbxGroupHasChildWithRef(group, file.fileRef)) {
846        group.children.push(pbxGroupChild(file));
847      }
848    } else {
849      // Create a pbx group with this file.
850      const { uuid } = pbxproj.addPbxGroup([file.path], groupName, groupName);
851
852      // Add newly created group to the parent group.
853      if (!pbxGroupHasChildWithRef(targetGroup, uuid)) {
854        targetGroup.children.push(pbxGroupChild({ fileRef: uuid, basename: groupName }));
855      }
856    }
857  }
858}
859
860async function copyFilesAsync(
861  files: string[],
862  sourceDir: string,
863  targetDir: string
864): Promise<void> {
865  for (const file of files) {
866    const fileRelativePath = path.relative(sourceDir, file);
867    const fileTargetPath = path.join(targetDir, fileRelativePath);
868
869    await fs.mkdirs(path.dirname(fileTargetPath));
870    await fs.copy(file, fileTargetPath);
871
872    console.log(chalk.yellow('>'), chalk.magenta(path.relative(EXPO_DIR, fileTargetPath)));
873  }
874}
875
876export async function legacyVendorModuleAsync(
877  moduleName: string,
878  platform: string,
879  tmpDir: string
880) {
881  const moduleConfig = vendoredModulesConfig[moduleName];
882
883  if (!moduleConfig) {
884    throw new Error(
885      `Module \`${chalk.green(
886        moduleName
887      )}\` doesn't match any of currently supported 3rd party modules. Run with \`--list\` to show a list of modules.`
888    );
889  }
890
891  moduleConfig.installableInManagedApps =
892    moduleConfig.installableInManagedApps == null ? true : moduleConfig.installableInManagedApps;
893
894  if (moduleConfig.warnings) {
895    moduleConfig.warnings.forEach((warning) => console.warn(warning));
896  }
897
898  if (moduleConfig.moduleModifier) {
899    await moduleConfig.moduleModifier(moduleConfig, tmpDir);
900  }
901
902  for (const step of moduleConfig.steps) {
903    const executeAndroid = ['all', 'android'].includes(platform);
904    const executeIOS = ['all', 'ios'].includes(platform);
905
906    step.recursive = step.recursive === true;
907    step.updatePbxproj = !(step.updatePbxproj === false);
908    const cleanupTargetPath = step.cleanupTargetPath ?? true;
909
910    // iOS
911    if (executeIOS && step.sourceIosPath && step.targetIosPath) {
912      const sourceDir = path.join(tmpDir, step.sourceIosPath);
913      const targetDir = path.join(IOS_DIR, 'Exponent', 'Versioned', 'Core', step.targetIosPath);
914
915      if (cleanupTargetPath) {
916        console.log(
917          `\nCleaning up iOS files at ${chalk.magenta(path.relative(IOS_DIR, targetDir))} ...`
918        );
919
920        await fs.remove(targetDir);
921      }
922      await fs.mkdirs(targetDir);
923
924      console.log('\nCopying iOS files ...');
925
926      const objcFiles = await findObjcFilesAsync(sourceDir, step.recursive);
927      const pbxprojPath = path.join(IOS_DIR, 'Exponent.xcodeproj', 'project.pbxproj');
928      const pbxproj = await loadXcodeprojFileAsync(pbxprojPath);
929
930      await copyFilesAsync(objcFiles, sourceDir, targetDir);
931
932      if (step.updatePbxproj) {
933        console.log(`\nUpdating pbxproj configuration ...`);
934
935        for (const file of objcFiles) {
936          const fileRelativePath = path.relative(sourceDir, file);
937          const fileTargetPath = path.join(targetDir, fileRelativePath);
938
939          await addFileToPbxprojAsync(fileTargetPath, targetDir, pbxproj);
940        }
941
942        console.log(
943          `Saving updated pbxproj structure to the file ${chalk.magenta(
944            path.relative(IOS_DIR, pbxprojPath)
945          )} ...`
946        );
947        await fs.writeFile(pbxprojPath, pbxproj.writeSync());
948      }
949
950      if (step.iosPrefix) {
951        console.log(`\nUpdating classes prefix to ${chalk.yellow(step.iosPrefix)} ...`);
952
953        const files = await findObjcFilesAsync(targetDir, step.recursive);
954
955        for (const file of files) {
956          await renameIOSSymbolsAsync(file, step.iosPrefix);
957        }
958      }
959
960      console.log(
961        chalk.yellow(
962          `\nSuccessfully updated iOS files, but please make sure Xcode project files are setup correctly in ${chalk.magenta(
963            `Exponent/Versioned/Core/${step.targetIosPath}`
964          )}`
965        )
966      );
967    }
968
969    // Android
970    if (
971      executeAndroid &&
972      step.sourceAndroidPath &&
973      step.targetAndroidPath &&
974      step.sourceAndroidPackage &&
975      step.targetAndroidPackage
976    ) {
977      const sourceDir = path.join(tmpDir, step.sourceAndroidPath);
978      const targetDir = path.join(
979        ANDROID_DIR,
980        'expoview',
981        'src',
982        'main',
983        'java',
984        'versioned',
985        'host',
986        'exp',
987        'exponent',
988        step.targetAndroidPath
989      );
990
991      if (cleanupTargetPath) {
992        console.log(
993          `\nCleaning up Android files at ${chalk.magenta(
994            path.relative(ANDROID_DIR, targetDir)
995          )} ...`
996        );
997
998        await fs.remove(targetDir);
999      }
1000      await fs.mkdirs(targetDir);
1001
1002      console.log('\nCopying Android files ...');
1003
1004      const javaFiles = await findAndroidFilesAsync(sourceDir);
1005
1006      await copyFilesAsync(javaFiles, sourceDir, targetDir);
1007
1008      const files = await findAndroidFilesAsync(targetDir);
1009
1010      for (const file of files) {
1011        await renamePackageAndroidAsync(file, step.sourceAndroidPackage, step.targetAndroidPackage);
1012        await step.onDidVendorAndroidFile?.(file);
1013      }
1014    }
1015  }
1016}
1017