xref: /expo/tools/src/commands/Vendor.ts (revision 75e080d0)
1import { Command } from '@expo/commander';
2import chalk from 'chalk';
3import inquirer from 'inquirer';
4import os from 'os';
5import path from 'path';
6
7import {
8  Clone,
9  CopyFiles,
10  Pipe,
11  TransformFilesContent,
12  TransformFilesName,
13  RemoveDirectory,
14  prefixPackage,
15  renameIOSSymbols,
16  renameIOSFiles,
17  Platform,
18  renameClass,
19  Append,
20} from '../vendoring/devmenu';
21
22const CONFIGURATIONS = {
23  '[dev-menu] reanimated': getReanimatedPipe(),
24  '[dev-menu] gesture-handler': getGestureHandlerPipe(),
25  '[dev-menu] safe-area-context': getSafeAreaPipe(),
26};
27
28function getReanimatedPipe() {
29  const destination = 'packages/expo-dev-menu/vendored/react-native-reanimated';
30
31  // prettier-ignore
32  return new Pipe().addSteps(
33    'all',
34      new Clone({
35        url: '[email protected]:software-mansion/react-native-reanimated.git',
36        tag: '1.13.0',
37      }),
38      new RemoveDirectory({
39        name: 'clean vendored folder',
40        target: destination,
41      }),
42      new CopyFiles({
43        filePattern: ['src/**/*.*', '*.d.ts'],
44        to: destination,
45      }),
46
47    'android',
48      prefixPackage({
49        packageName: 'com.swmansion.reanimated',
50        prefix: 'devmenu',
51      }),
52      renameClass({
53        filePattern: 'android/**/*.@(java|kt)',
54        className: 'UIManagerReanimatedHelper',
55        newClassName: 'DevMenuUIManagerReanimatedHelper'
56      }),
57      new CopyFiles({
58        subDirectory: 'android/src/main/java/com/swmansion',
59        filePattern: '**/*.@(java|kt|xml)',
60        to: path.join(destination, 'android/devmenu/com/swmansion'),
61      }),
62      new CopyFiles({
63        subDirectory: 'android/src/main/java/com/facebook',
64        filePattern: '**/*.@(java|kt|xml)',
65        to: path.join(destination, 'android/com/facebook'),
66      }),
67
68    'ios',
69      new TransformFilesName({
70        filePattern: 'ios/**/*REA*.@(m|h)',
71        find: 'REA',
72        replace: 'DevMenuREA',
73      }),
74      renameIOSSymbols({
75        find: 'REA',
76        replace: 'DevMenuREA',
77      }),
78      new TransformFilesContent({
79        filePattern: 'ios/**/*.@(m|h)',
80        find: 'SimAnimationDragCoefficient',
81        replace: 'DevMenuSimAnimationDragCoefficient',
82      }),
83      new TransformFilesContent({
84        filePattern: 'ios/**/*.@(m|h)',
85        find: '^RCT_EXPORT_MODULE\\((.*)\\)',
86        replace: '+ (NSString *)moduleName { return @"$1"; }',
87      }),
88      new CopyFiles({
89        filePattern: 'ios/**/*.@(m|h)',
90        to: destination,
91      })
92  );
93}
94
95function getGestureHandlerPipe() {
96  const destination = 'packages/expo-dev-menu/vendored/react-native-gesture-handler';
97
98  // prettier-ignore
99  return new Pipe().addSteps(
100    'all',
101      new Clone({
102        url: '[email protected]:software-mansion/react-native-gesture-handler.git',
103        tag: '1.7.0',
104      }),
105      new RemoveDirectory({
106        name: 'clean vendored folder',
107        target: destination,
108      }),
109      new CopyFiles({
110        filePattern: ['*.js', 'touchables/*.js', '*.d.ts'],
111        to: path.join(destination, 'src'),
112      }),
113
114    'android',
115      prefixPackage({
116        packageName: 'com.swmansion.gesturehandler',
117        prefix: 'devmenu',
118      }),
119      renameClass({
120        filePattern: 'android/**/*.@(java|kt)',
121        className: 'RNGHModalUtils',
122        newClassName: 'DevMenuRNGHModalUtils'
123      }),
124      new CopyFiles({
125        subDirectory: 'android/src/main/java/com/swmansion',
126        filePattern: '**/*.@(java|kt|xml)',
127        to: path.join(destination, 'android/devmenu/com/swmansion'),
128      }),
129      new CopyFiles({
130        subDirectory: 'android/lib/src/main/java',
131        filePattern: '**/*.@(java|kt|xml)',
132        to: path.join(destination, 'android/devmenu'),
133      }),
134      new CopyFiles({
135        subDirectory: 'android/src/main/java/com/facebook',
136        filePattern: '**/*.@(java|kt|xml)',
137        to: path.join(destination, 'android/com/facebook'),
138      }),
139
140    'ios',
141      renameIOSFiles({
142        find: 'RN',
143        replace: 'DevMenuRN',
144      }),
145      renameIOSSymbols({
146        find: 'RN',
147        replace: 'DevMenuRN',
148      }),
149      new TransformFilesContent({
150        filePattern: path.join('ios', '**', '*.@(m|h)'),
151        find: '^RCT_EXPORT_MODULE\\(DevMenu(.*)\\)',
152        replace: '+ (NSString *)moduleName { return @"$1"; }',
153      }),
154      new TransformFilesContent({
155        filePattern: 'ios/**/*.@(m|h)',
156        find: '^RCT_EXPORT_MODULE\\(\\)',
157        replace: '+ (NSString *)moduleName { return @"RNGestureHandlerModule"; }',
158      }),
159      new TransformFilesContent({
160        filePattern: 'ios/**/DevMenuRNGestureHandlerModule.m',
161        find: '@interface DevMenuRNGestureHandlerButtonManager([\\s\\S]*?)@end',
162        replace: ''
163      }),
164      new Append({
165        filePattern: 'ios/**/DevMenuRNGestureHandlerModule.h',
166        append: `@interface DevMenuRNGestureHandlerButtonManager : RCTViewManager
167@end
168`
169      }),
170      new CopyFiles({
171        filePattern: 'ios/**/*.@(m|h)',
172        to: destination,
173      })
174  );
175}
176
177function getSafeAreaPipe() {
178  const destination = 'packages/expo-dev-menu/vendored/react-native-safe-area-context';
179
180  // prettier-ignore
181  return new Pipe().addSteps(
182    'all',
183      new Clone({
184        url: '[email protected]:th3rdwave/react-native-safe-area-context.git',
185        tag: 'v3.3.2',
186      }),
187      new RemoveDirectory({
188        name: 'clean vendored folder',
189        target: destination,
190      }),
191      new CopyFiles({
192        filePattern: ['src/**/*.*', '*.d.ts'],
193        to: destination,
194      }),
195    'android',
196      prefixPackage({
197        packageName: 'com.th3rdwave.safeareacontext',
198        prefix: 'devmenu',
199      }),
200      new CopyFiles({
201        subDirectory: 'android/src/main/java/com/th3rdwave',
202        filePattern: '**/*.@(java|kt|xml)',
203        to: path.join(destination, 'android/devmenu/com/th3rdwave'),
204      }),
205      new CopyFiles({
206        subDirectory: 'android/src/main/java/com/facebook',
207        filePattern: '**/*.@(java|kt|xml)',
208        to: path.join(destination, 'android/com/facebook'),
209      }),
210
211    'ios',
212      new TransformFilesName({
213        filePattern: 'ios/**/*RNC*.@(m|h)',
214        find: 'RNC',
215        replace: 'DevMenuRNC',
216      }),
217      new TransformFilesName({
218        filePattern: 'ios/**/*SafeAreaCompat.@(m|h)',
219        find: 'SafeAreaCompat',
220        replace: 'DevMenuSafeAreaCompat',
221      }),
222      renameIOSSymbols({
223        find: 'RNC',
224        replace: 'DevMenuRNC',
225      }),
226      renameIOSSymbols({
227        find: 'SafeAreaCompat',
228        replace: 'DevMenuSafeAreaCompat',
229      }),
230      new TransformFilesContent({
231        filePattern: 'ios/**/*.@(m|h)',
232        find: 'UIEdgeInsetsEqualToEdgeInsetsWithThreshold',
233        replace: 'DevMenuUIEdgeInsetsEqualToEdgeInsetsWithThreshold',
234      }),
235      new TransformFilesContent({
236        filePattern: 'ios/**/DevMenuRNCSafeAreaProviderManager.@(m|h)',
237        find: '^RCT_EXPORT_MODULE\\((.*)\\)',
238        replace: '+ (NSString *)moduleName { return @"RNCSafeAreaProvider"; }',
239      }),
240      new TransformFilesContent({
241        filePattern: 'ios/**/DevMenuRNCSafeAreaViewManager.@(m|h)',
242        find: '^RCT_EXPORT_MODULE\\((.*)\\)',
243        replace: '+ (NSString *)moduleName { return @"RNCSafeAreaView"; }',
244      }),
245
246      new TransformFilesContent({
247        filePattern: 'ios/**/DevMenuRNCSafeAreaProviderManager.@(m|h)',
248        find: 'constantsToExport',
249        replace: 'constantsToExportAsync',
250      }),
251      new TransformFilesContent({
252        filePattern: 'ios/**/DevMenuRNCSafeAreaProviderManager.m',
253        find: '@end',
254        replace: '',
255      }),
256
257      new Append({
258        filePattern: 'ios/**/DevMenuRNCSafeAreaProviderManager.m',
259        append: `// this method cannot be called from background thread - enforcing dispatch_sync()
260        - (NSDictionary *)constantsToExport
261 {
262   __block NSDictionary *constants;
263
264   dispatch_sync(dispatch_get_main_queue(), ^{
265     UIWindow* window = [[UIApplication sharedApplication] keyWindow];
266     if (@available(iOS 11.0, *)) {
267       UIEdgeInsets safeAreaInsets = window.safeAreaInsets;
268       constants = @{
269         @"initialWindowMetrics": @{
270           @"insets": @{
271             @"top": @(safeAreaInsets.top),
272             @"right": @(safeAreaInsets.right),
273             @"bottom": @(safeAreaInsets.bottom),
274             @"left": @(safeAreaInsets.left),
275           },
276           @"frame": @{
277             @"x": @(window.frame.origin.x),
278             @"y": @(window.frame.origin.y),
279             @"width": @(window.frame.size.width),
280             @"height": @(window.frame.size.height),
281           },
282         }
283       };
284     } else {
285       constants = @{ @"initialWindowMetrics": @{
286           @"insets": @{
287             @"top": @(20),
288             @"right": @(0),
289             @"bottom": @(0),
290             @"left": @(0),
291           },
292           @"frame": @{
293             @"x": @(window.frame.origin.x),
294             @"y": @(window.frame.origin.y),
295             @"width": @(window.frame.size.width),
296             @"height": @(window.frame.size.height),
297           },
298         }
299       } ;
300     }
301   });
302
303  return constants;
304}
305
306@end
307`
308      }),
309
310      new CopyFiles({
311        filePattern: 'ios/**/*.@(m|h)',
312        to: destination,
313      }),
314  );
315}
316
317async function askForConfigurations(): Promise<string[]> {
318  const { configurationNames } = await inquirer.prompt<{ configurationNames: string[] }>([
319    {
320      type: 'checkbox',
321      name: 'configurationNames',
322      message: 'Which configuration would you like to run?\n  ● selected ○ unselected\n',
323      choices: Object.keys(CONFIGURATIONS),
324      default: Object.keys(CONFIGURATIONS),
325    },
326  ]);
327  return configurationNames;
328}
329
330type ActionOptions = {
331  platform: Platform;
332  configuration: string[];
333};
334
335async function action({ configuration, platform }: ActionOptions) {
336  if (!configuration.length) {
337    configuration = await askForConfigurations();
338  }
339
340  const pipes = configuration.map((name) => ({ name, pipe: CONFIGURATIONS[name] as Pipe }));
341  const tmpdir = os.tmpdir();
342  for (const { name, pipe } of pipes) {
343    console.log(`Run configuration: ${chalk.green(name)}`);
344    pipe.setWorkingDirectory(path.join(tmpdir, name));
345    await pipe.start(platform);
346    console.log();
347  }
348}
349
350export default (program: Command) => {
351  program
352    .command('vendor')
353    .alias('v')
354    .description('Vendors 3rd party modules.')
355    .option(
356      '-p, --platform <string>',
357      "A platform on which the vendored module will be updated. Valid options: 'all' | 'ios' | 'android'.",
358      'all'
359    )
360    .option(
361      '-c, --configuration [string]',
362      'Vendor configuration which should be run. Can be passed multiple times.',
363      (value, previous) => previous.concat(value),
364      []
365    )
366
367    .asyncAction(action);
368};
369