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