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