xref: /expo/tools/src/commands/Vendor.ts (revision 95b09b82)
1import { Command } from '@expo/commander';
2import chalk from 'chalk';
3import fs from 'fs-extra';
4import inquirer from 'inquirer';
5import os from 'os';
6import path from 'path';
7
8import { Podspec, readPodspecAsync } from '../CocoaPods';
9import {
10  buildFrameworksForProjectAsync,
11  cleanTemporaryFilesAsync,
12  generateXcodeProjectSpecFromPodspecAsync,
13} from '../prebuilds/Prebuilder';
14import {
15  Append,
16  Clone,
17  CopyFiles,
18  Pipe,
19  Platform,
20  PrefixHeaders,
21  prefixPackage,
22  RemoveDirectory,
23  renameClass,
24  renameIOSFiles,
25  renameIOSSymbols,
26  TransformFilesContent,
27  TransformFilesName,
28} from '../vendoring/devmenu';
29import { GenerateJsonFromPodspec } from '../vendoring/devmenu/steps/GenerateJsonFromPodspec';
30import { MessageType, Print } from '../vendoring/devmenu/steps/Print';
31import { RemoveFiles } from '../vendoring/devmenu/steps/RemoveFiles';
32import { toRepoPath } from '../vendoring/devmenu/utils';
33
34async function getRequierdIosVersion(): Promise<string> {
35  const devMenuPodspec = await readPodspecAsync(
36    toRepoPath('packages/expo-dev-menu/expo-dev-menu.podspec')
37  );
38
39  return devMenuPodspec['platforms']['ios'] as string;
40}
41
42type Config = {
43  transformations: Pipe;
44  prebuild?: PrebuildConfig;
45};
46
47type PrebuildConfig = {
48  podspecPath: string;
49  output: string;
50};
51
52const CONFIGURATIONS: { [name: string]: Config } = {
53  '[dev-menu] reanimated': getReanimatedPipe(),
54  '[dev-menu] gesture-handler': getGestureHandlerPipe(),
55  '[dev-menu] safe-area-context': getSafeAreaConfig(),
56};
57
58function getReanimatedPipe() {
59  const destination = 'packages/expo-dev-menu/vendored/react-native-reanimated';
60
61  // prettier-ignore
62  const transformations = new Pipe().addSteps(
63    'all',
64      new Print(MessageType.WARNING, 'You have to adjust the installation steps of the react-native-reanimated to work well with the react-native-gesture-handler. For more information go to the https://github.com/expo/expo/pull/17878 and https://github.com/expo/expo/pull/18562' ),
65      new Clone({
66        url: '[email protected]:software-mansion/react-native-reanimated.git',
67        tag: '2.14.4',
68      }),
69      new RemoveDirectory({
70        name: 'clean vendored folder',
71        target: destination,
72      }),
73      new TransformFilesContent({
74        filePattern: '**/*.@(h|cpp)',
75        find: 'namespace reanimated',
76        replace: 'namespace devmenureanimated',
77      }),
78      new TransformFilesContent({
79         filePattern: '**/*.@(h|cpp)',
80         find: 'reanimated::',
81         replace: 'devmenureanimated::',
82       }),
83      new TransformFilesContent({
84        filePattern: 'Common/**/ReanimatedHiddenHeaders.h',
85        find: 'Common/cpp',
86        replace: 'vendored/react-native-reanimated/Common/cpp',
87      }),
88      new PrefixHeaders({
89        prefix: "DevMenu",
90        subPath: 'Common',
91        filePattern: "**/*.@(h|cpp|m|mm)",
92        debug: true
93      }),
94      new CopyFiles({
95        filePattern: ['src/**/*.*', '*.d.ts', 'plugin.js', 'Common/**/*.@(h|cpp)'],
96        to: destination,
97      }),
98
99    'android',
100      prefixPackage({
101        packageName: 'com.swmansion.reanimated',
102        prefix: 'devmenu',
103      }),
104      prefixPackage({
105        packageName: 'com.swmansion.common',
106        prefix: 'devmenu',
107      }),
108      renameClass({
109        filePattern: 'android/**/*.@(java|kt)',
110        className: 'UIManagerReanimatedHelper',
111        newClassName: 'DevMenuUIManagerReanimatedHelper'
112      }),
113       new TransformFilesContent({
114        filePattern: 'android/src/main/cpp/**/*.@(h|cpp)',
115        find: 'Lcom/swmansion/reanimated',
116        replace: 'Ldevmenu/com/swmansion/reanimated',
117      }),
118      new TransformFilesContent({
119        filePattern: 'android/**/*.@(java|kt)',
120        find: 'System\\.loadLibrary\\("reanimated"\\)',
121        replace: 'System.loadLibrary("devmenureanimated")',
122      }),
123      new TransformFilesContent({
124        filePattern: 'android/CMakeLists.txt',
125        find: 'set \\(PACKAGE_NAME "reanimated"\\)',
126        replace: 'set (PACKAGE_NAME "devmenureanimated")',
127      }),
128      new TransformFilesName({
129        filePattern: 'android/**/ReanimatedUIManager.java',
130        find: 'ReanimatedUIManager',
131        replace: 'DevMenuReanimatedUIManager',
132      }),
133      new TransformFilesContent({
134        filePattern: 'android/**/*.@(java|kt)',
135        find: 'ReaUiImplementationProvider',
136        replace: 'DevMenuReaUiImplementationProvider',
137      }),
138      new TransformFilesContent({
139        filePattern: 'android/**/*.@(java|kt)',
140        find: 'ReanimatedUIManager',
141        replace: 'DevMenuReanimatedUIManager',
142      }),
143      new TransformFilesName({
144        filePattern: 'android/**/ReanimatedUIImplementation.java',
145        find: 'ReanimatedUIImplementation',
146        replace: 'DevMenuReanimatedUIImplementation',
147      }),
148      new TransformFilesContent({
149        filePattern: 'android/**/*.@(java|kt)',
150        find: 'ReanimatedUIImplementation',
151        replace: 'DevMenuReanimatedUIImplementation',
152      }),
153      new TransformFilesContent({
154        filePattern: 'android/**/ReanimatedPackage.java',
155        find: 'public class ReanimatedPackage extends TurboReactPackage implements ReactPackage {',
156        replace: 'public class ReanimatedPackage extends TurboReactPackage implements ReactPackage {\n  public ReactInstanceManager instanceManager;\n',
157      }),
158      new TransformFilesContent({
159        filePattern: 'android/**/ReanimatedPackage.java',
160        find: 'public ReactInstanceManager getReactInstanceManager(ReactApplicationContext reactContext) {',
161        replace: 'public ReactInstanceManager getReactInstanceManager(ReactApplicationContext reactContext) {\nreturn instanceManager;\n',
162      }),
163    'ios',
164      new RemoveFiles({
165        filePattern: 'ios/native/UIResponder+*'
166      }),
167      new TransformFilesContent({
168        filePattern: 'ios/**/*.@(h|mm)',
169        find: 'namespace reanimated',
170        replace: 'namespace devmenureanimated',
171      }),
172      new TransformFilesContent({
173         filePattern: 'ios/**/*.@(h|mm)',
174         find: 'reanimated::',
175         replace: 'devmenureanimated::',
176       }),
177      new TransformFilesContent({
178        filePattern: 'ios/**/*.@(h|m|mm)',
179        find: '#import <RNReanimated\\/(.*)>',
180        replace: '#import "$1"',
181      }),
182      new TransformFilesName({
183        filePattern: 'ios/**/*REA*.@(h|m|mm)',
184        find: 'REA',
185        replace: 'DevMenuREA',
186      }),
187      renameIOSSymbols({
188        find: 'REA',
189        replace: 'DevMenuREA',
190      }),
191      new TransformFilesName({
192        filePattern: 'ios/**/*Reanimated*.@(h|m|mm)',
193        find: 'Reanimated',
194        replace: 'DevMenuReanimated',
195      }),
196      renameIOSSymbols({
197        find: 'Reanimated',
198        replace: 'DevMenuReanimated',
199      }),
200      new TransformFilesContent({
201        filePattern: 'ios/**/*.@(h|m|mm)',
202        find: 'SimAnimationDragCoefficient',
203        replace: 'DevMenuSimAnimationDragCoefficient',
204      }),
205      new TransformFilesContent({
206        filePattern: 'ios/**/*.@(h|m|mm)',
207        find: '^RCT_EXPORT_MODULE\\((.*)\\)',
208        replace: '+ (NSString *)moduleName { return @"$1"; }',
209      }),
210      new TransformFilesName({
211        filePattern: 'ios/RNGestureHandlerStateManager.h',
212        find: 'RNGestureHandlerStateManager',
213        replace: 'DevMenuRNGestureHandlerStateManager',
214      }),
215      new TransformFilesContent({
216        filePattern: 'ios/**/*.@(h|m|mm)',
217        find: 'RNGestureHandlerStateManager',
218        replace: 'DevMenuRNGestureHandlerStateManager',
219      }),
220      new TransformFilesContent({
221        filePattern: 'ios/**/RNGestureHandler.m',
222        find: 'UIGestureRecognizer (GestureHandler)',
223        replace: 'UIGestureRecognizer (DevMenuGestureHandler)'
224      }),
225      new TransformFilesContent({
226        filePattern: 'ios/**/RNGestureHandler.m',
227        find: 'gestureHandler',
228        replace: 'devmenugestureHandler'
229      }),
230      new CopyFiles({
231        filePattern: 'ios/**/*.@(m|h|mm)',
232        to: destination,
233      }),
234  );
235
236  return { transformations };
237}
238
239function getGestureHandlerPipe() {
240  const destination = 'packages/expo-dev-menu/vendored/react-native-gesture-handler';
241
242  // prettier-ignore
243  const transformations = new Pipe().addSteps(
244    'all',
245      new Clone({
246        url: '[email protected]:software-mansion/react-native-gesture-handler.git',
247        tag: '2.1.2',
248      }),
249      new RemoveDirectory({
250        name: 'clean vendored folder',
251        target: destination,
252      }),
253      new CopyFiles({
254        subDirectory: 'src',
255        filePattern: ['**/*.ts', '**/*.tsx'],
256        to: path.join(destination, 'src'),
257      }),
258      new CopyFiles({
259        filePattern: 'jestSetup.js',
260        to: destination,
261      }),
262
263    'android',
264      prefixPackage({
265        packageName: 'com.swmansion.gesturehandler',
266        prefix: 'devmenu',
267      }),
268      renameClass({
269        filePattern: 'android/**/*.@(java|kt)',
270        className: 'RNGHModalUtils',
271        newClassName: 'DevMenuRNGHModalUtils'
272      }),
273      new CopyFiles({
274        subDirectory: 'android/src/main/java/com/swmansion',
275        filePattern: '**/*.@(java|kt|xml)',
276        to: path.join(destination, 'android/devmenu/com/swmansion'),
277      }),
278      new CopyFiles({
279        subDirectory: 'android/lib/src/main/java',
280        filePattern: '**/*.@(java|kt|xml)',
281        to: path.join(destination, 'android/devmenu'),
282      }),
283      new CopyFiles({
284        subDirectory: 'android/common/src/main/java',
285        filePattern: '**/*.@(java|kt|xml)',
286        to: path.join(destination, 'android/devmenu'),
287      }),
288      new CopyFiles({
289        subDirectory: 'android/src/main/java/com/facebook',
290        filePattern: '**/*.@(java|kt|xml)',
291        to: path.join(destination, 'android/com/facebook'),
292      }),
293
294    'ios',
295      renameIOSFiles({
296        find: 'RN',
297        replace: 'DevMenuRN',
298      }),
299      renameIOSSymbols({
300        find: 'RN',
301        replace: 'DevMenuRN',
302      }),
303      new TransformFilesContent({
304        filePattern: path.join('ios', '**', '*.@(m|h)'),
305        find: '^RCT_EXPORT_MODULE\\(DevMenu(.*)\\)',
306        replace: '+ (NSString *)moduleName { return @"$1"; }',
307      }),
308      new TransformFilesContent({
309        filePattern: 'ios/**/*.@(m|h)',
310        find: '^RCT_EXPORT_MODULE\\(\\)',
311        replace: '+ (NSString *)moduleName { return @"RNGestureHandlerModule"; }',
312      }),
313      new TransformFilesContent({
314        filePattern: 'ios/**/DevMenuRNGestureHandlerModule.m',
315        find: '@interface DevMenuRNGestureHandlerButtonManager([\\s\\S]*?)@end',
316        replace: ''
317      }),
318      new TransformFilesContent({
319        filePattern: 'ios/**/DevMenuRNGestureHandler',
320        find: 'UIGestureRecognizer (GestureHandler)',
321        replace: 'UIGestureRecognizer \(DevMenuGestureHandler\)'
322      }),
323      new TransformFilesContent({
324        filePattern: 'ios/**/DevMenuRNGestureHandler',
325        find: 'gestureHandler',
326        replace: 'devMenuGestureHandler'
327      }),
328      new Append({
329        filePattern: 'ios/**/DevMenuRNGestureHandlerModule.h',
330        append: `@interface DevMenuRNGestureHandlerButtonManager : RCTViewManager
331@end
332`
333      }),
334      new CopyFiles({
335        filePattern: 'ios/**/*.@(h|m)',
336        to: destination,
337      }),
338      new GenerateJsonFromPodspec({
339        from: 'RNGestureHandler.podspec',
340        saveTo: `${destination}/RNGestureHandler.podspec.json`,
341        transform: async (podspec) => ({...podspec, name: 'DevMenuRNGestureHandler', platforms: {'ios': await getRequierdIosVersion()}})
342      })
343  );
344
345  return {
346    transformations,
347    prebuild: {
348      podspecPath: `${destination}/RNGestureHandler.podspec.json`,
349      output: destination,
350    },
351  };
352}
353
354function getSafeAreaConfig() {
355  const destination = 'packages/expo-dev-menu/vendored/react-native-safe-area-context';
356  const version = '3.3.2';
357
358  // prettier-ignore
359  const transformations = new Pipe().addSteps(
360    'all',
361      new Clone({
362        url: '[email protected]:th3rdwave/react-native-safe-area-context.git',
363        tag: `v${version}`,
364      }),
365      new RemoveDirectory({
366        name: 'clean vendored folder',
367        target: destination,
368      }),
369      new CopyFiles({
370        filePattern: ['src/**/*.*', '*.d.ts'],
371        to: destination,
372      }),
373    'android',
374      prefixPackage({
375        packageName: 'com.th3rdwave.safeareacontext',
376        prefix: 'devmenu',
377      }),
378      new CopyFiles({
379        subDirectory: 'android/src/main/java/com/th3rdwave',
380        filePattern: '**/*.@(java|kt|xml)',
381        to: path.join(destination, 'android/devmenu/com/th3rdwave'),
382      }),
383      new CopyFiles({
384        subDirectory: 'android/src/main/java/com/facebook',
385        filePattern: '**/*.@(java|kt|xml)',
386        to: path.join(destination, 'android/com/facebook'),
387      }),
388
389    'ios',
390      new TransformFilesName({
391        filePattern: 'ios/**/*RNC*.@(m|h)',
392        find: 'RNC',
393        replace: 'DevMenuRNC',
394      }),
395      new TransformFilesName({
396        filePattern: 'ios/**/*SafeAreaCompat.@(m|h)',
397        find: 'SafeAreaCompat',
398        replace: 'DevMenuSafeAreaCompat',
399      }),
400      renameIOSSymbols({
401        find: 'RNC',
402        replace: 'DevMenuRNC',
403      }),
404      renameIOSSymbols({
405        find: 'SafeAreaCompat',
406        replace: 'DevMenuSafeAreaCompat',
407      }),
408      new TransformFilesContent({
409        filePattern: 'ios/**/*.@(m|h)',
410        find: 'UIEdgeInsetsEqualToEdgeInsetsWithThreshold',
411        replace: 'DevMenuUIEdgeInsetsEqualToEdgeInsetsWithThreshold',
412      }),
413      new TransformFilesContent({
414        filePattern: 'ios/**/DevMenuRNCSafeAreaProviderManager.@(m|h)',
415        find: '^RCT_EXPORT_MODULE\\((.*)\\)',
416        replace: '+ (NSString *)moduleName { return @"RNCSafeAreaProvider"; }',
417      }),
418      new TransformFilesContent({
419        filePattern: 'ios/**/DevMenuRNCSafeAreaViewManager.@(m|h)',
420        find: '^RCT_EXPORT_MODULE\\((.*)\\)',
421        replace: '+ (NSString *)moduleName { return @"RNCSafeAreaView"; }',
422      }),
423      new TransformFilesContent({
424        filePattern: 'ios/**/DevMenuRNCSafeAreaProviderManager.@(m|h)',
425        find: 'constantsToExport',
426        replace: 'constantsToExportAsync',
427      }),
428      new TransformFilesContent({
429        filePattern: 'ios/**/DevMenuRNCSafeAreaProviderManager.m',
430        find: '@end',
431        replace: '',
432      }),
433
434      new Append({
435        filePattern: 'ios/**/DevMenuRNCSafeAreaProviderManager.m',
436        append: `// this method cannot be called from background thread - enforcing dispatch_sync()
437        - (NSDictionary *)constantsToExport
438 {
439   __block NSDictionary *constants;
440
441   dispatch_sync(dispatch_get_main_queue(), ^{
442     UIWindow* window = [[UIApplication sharedApplication] keyWindow];
443     if (@available(iOS 11.0, *)) {
444       UIEdgeInsets safeAreaInsets = window.safeAreaInsets;
445       constants = @{
446         @"initialWindowMetrics": @{
447           @"insets": @{
448             @"top": @(safeAreaInsets.top),
449             @"right": @(safeAreaInsets.right),
450             @"bottom": @(safeAreaInsets.bottom),
451             @"left": @(safeAreaInsets.left),
452           },
453           @"frame": @{
454             @"x": @(window.frame.origin.x),
455             @"y": @(window.frame.origin.y),
456             @"width": @(window.frame.size.width),
457             @"height": @(window.frame.size.height),
458           },
459         }
460       };
461     } else {
462       constants = @{ @"initialWindowMetrics": @{
463           @"insets": @{
464             @"top": @(20),
465             @"right": @(0),
466             @"bottom": @(0),
467             @"left": @(0),
468           },
469           @"frame": @{
470             @"x": @(window.frame.origin.x),
471             @"y": @(window.frame.origin.y),
472             @"width": @(window.frame.size.width),
473             @"height": @(window.frame.size.height),
474           },
475         }
476       } ;
477     }
478   });
479
480  return constants;
481}
482
483@end
484`
485      }),
486      new CopyFiles({
487        filePattern: 'ios/**/*.@(m|h)',
488        to: destination,
489      }),
490      new GenerateJsonFromPodspec({
491        from: 'react-native-safe-area-context.podspec',
492        saveTo: `${destination}/react-native-safe-area-context.podspec.json`,
493        transform: async (podspec) => ({...podspec, name: 'dev-menu-react-native-safe-area-context', platforms: {'ios': await getRequierdIosVersion()}})
494      })
495  );
496
497  return {
498    transformations,
499    prebuild: {
500      podspecPath: `${destination}/react-native-safe-area-context.podspec.json`,
501      output: destination,
502    },
503  };
504}
505
506async function askForConfigurations(): Promise<string[]> {
507  const { configurationNames } = await inquirer.prompt<{ configurationNames: string[] }>([
508    {
509      type: 'checkbox',
510      name: 'configurationNames',
511      message: 'Which configuration would you like to run?\n  ● selected ○ unselected\n',
512      choices: Object.keys(CONFIGURATIONS),
513      default: Object.keys(CONFIGURATIONS),
514    },
515  ]);
516  return configurationNames;
517}
518
519type ActionOptions = {
520  platform: Platform;
521  configuration: string[];
522  onlyPrebuild: boolean;
523};
524
525async function action({ configuration, platform, onlyPrebuild }: ActionOptions) {
526  if (!configuration.length) {
527    configuration = await askForConfigurations();
528  }
529
530  const configurations = configuration.map((name) => ({ name, config: CONFIGURATIONS[name] }));
531  const tmpdir = os.tmpdir();
532  for (const { name, config } of configurations) {
533    console.log(`Run configuration: ${chalk.green(name)}`);
534    const { transformations, prebuild } = config;
535    if (!onlyPrebuild) {
536      transformations.setWorkingDirectory(path.join(tmpdir, name));
537      await transformations.start(platform);
538      console.log();
539    }
540
541    if (prebuild) {
542      const { podspecPath, output } = prebuild;
543      console.log('�� Prebuilding ...');
544
545      const podspec = JSON.parse(await fs.readFile(toRepoPath(podspecPath), 'utf8')) as Podspec;
546      const xcodeProject = await generateXcodeProjectSpecFromPodspecAsync(
547        podspec,
548        toRepoPath(output)
549      );
550      await buildFrameworksForProjectAsync(xcodeProject);
551      await cleanTemporaryFilesAsync(xcodeProject);
552      console.log();
553    }
554  }
555}
556
557export default (program: Command) => {
558  program
559    .command('vendor')
560    .alias('v')
561    .description('Vendors 3rd party modules.')
562    .option(
563      '-p, --platform <string>',
564      "A platform on which the vendored module will be updated. Valid options: 'all' | 'ios' | 'android'.",
565      'all'
566    )
567    .option('--only-prebuild', 'Run only prebuild script.')
568    .option(
569      '-c, --configuration [string]',
570      'Vendor configuration which should be run. Can be passed multiple times.',
571      (value, previous) => previous.concat(value),
572      []
573    )
574
575    .asyncAction(action);
576};
577