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