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