1import spawnAsync from '@expo/spawn-async'; 2import chalk from 'chalk'; 3import { PromisyClass, TaskQueue } from 'cwait'; 4import fs from 'fs-extra'; 5import glob from 'glob-promise'; 6import inquirer from 'inquirer'; 7import path from 'path'; 8import semver from 'semver'; 9 10import { runReactNativeCodegenAsync } from '../../Codegen'; 11import { EXPO_DIR, IOS_DIR, VERSIONED_RN_IOS_DIR } from '../../Constants'; 12import logger from '../../Logger'; 13import { getListOfPackagesAsync, Package } from '../../Packages'; 14import { copyFileWithTransformsAsync } from '../../Transforms'; 15import type { FileTransforms, StringTransform } from '../../Transforms.types'; 16import { renderExpoKitPodspecAsync } from '../../dynamic-macros/IosMacrosGenerator'; 17import { runTransformPipelineAsync } from './transforms'; 18import { injectMacros } from './transforms/injectMacros'; 19import { kernelFilesTransforms } from './transforms/kernelFilesTransforms'; 20import { podspecTransforms } from './transforms/podspecTransforms'; 21import { postTransforms } from './transforms/postTransforms'; 22import { getVersionedDirectory, getVersionedExpoKitPath } from './utils'; 23import { versionExpoModulesAsync } from './versionExpoModules'; 24import { 25 MODULES_PROVIDER_POD_NAME, 26 versionExpoModulesProviderAsync, 27} from './versionExpoModulesProvider'; 28import { 29 versionVendoredModulesAsync, 30 removeVersionedVendoredModulesAsync, 31} from './versionVendoredModules'; 32 33export { versionVendoredModulesAsync, versionExpoModulesAsync }; 34 35const UNVERSIONED_PLACEHOLDER = '__UNVERSIONED__'; 36const RELATIVE_RN_PATH = './react-native-lab/react-native'; 37 38const EXTERNAL_REACT_ABI_DEPENDENCIES = [ 39 'Amplitude', 40 'Analytics', 41 'AppAuth', 42 'FBAudienceNetwork', 43 'FBSDKCoreKit', 44 'GoogleSignIn', 45 'GoogleMaps', 46 'Google-Maps-iOS-Utils', 47 'lottie-ios', 48 'JKBigInteger', 49 'Branch', 50 'Google-Mobile-Ads-SDK', 51 'RCT-Folly', 52]; 53 54const EXCLUDED_POD_DEPENDENCIES = ['ExpoModulesTestCore']; 55 56/** 57 * Transform and rename the given react native source code files. 58 * @param filenames list of files to transform 59 * @param versionPrefix A version-specific prefix to apply to all symbols in the code, e.g. 60 * RCTSomeClass becomes {versionPrefix}RCTSomeClass 61 * @param versionedPodNames mapping from unversioned cocoapods names to versioned cocoapods names, 62 * e.g. React -> ReactABI99_0_0 63 */ 64async function namespaceReactNativeFilesAsync(filenames, versionPrefix, versionedPodNames) { 65 const reactPodName = versionedPodNames.React; 66 const transformRules = _getReactNativeTransformRules(versionPrefix, reactPodName); 67 const taskQueue = new TaskQueue(Promise as PromisyClass, 4); // Transform up to 4 files simultaneously. 68 const transformRulesCache = {}; 69 70 const transformSingleFile = taskQueue.wrap(async (filename) => { 71 if (_isDirectory(filename)) { 72 return; 73 } 74 // protect contents of EX_UNVERSIONED macro 75 const unversionedCaptures: string[] = []; 76 await _transformFileContentsAsync(filename, (fileString) => { 77 const pattern = /EX_UNVERSIONED\((.*?)\)/g; 78 let match = pattern.exec(fileString); 79 while (match != null) { 80 unversionedCaptures.push(match[1]); 81 match = pattern.exec(fileString); 82 } 83 if (unversionedCaptures.length) { 84 return fileString.replace(pattern, UNVERSIONED_PLACEHOLDER); 85 } 86 return null; 87 }); 88 89 // rename file 90 const dirname = path.dirname(filename); 91 const basename = path.basename(filename); 92 const targetPath = path.join(dirname, `${versionPrefix}${basename}`); 93 94 // filter transformRules to patterns which apply to this dirname 95 const filteredTransformRules = 96 transformRulesCache[dirname] || _getTransformRulesForDirname(transformRules, dirname); 97 transformRulesCache[dirname] = filteredTransformRules; 98 99 // Perform sed find & replace. 100 for (const rule of filteredTransformRules) { 101 await spawnAsync('sed', [rule.flags || '-i', '--', rule.pattern, filename]); 102 } 103 104 // Rename file to be prefixed. 105 await fs.move(filename, targetPath); 106 107 // perform transforms that sed can't express 108 await _transformFileContentsAsync(targetPath, async (fileString) => { 109 // rename misc imports, e.g. Layout.h 110 fileString = fileString.replace( 111 /#(include|import)\s+"((?:[^"\/]+\/)?)([^"]+\.h)"/g, 112 (match, p1, p2, p3) => { 113 return p3.startsWith(versionPrefix) ? match : `#${p1} "${p2}${versionPrefix}${p3}"`; 114 } 115 ); 116 117 // restore EX_UNVERSIONED contents 118 if (unversionedCaptures) { 119 let index = 0; 120 do { 121 fileString = fileString.replace(UNVERSIONED_PLACEHOLDER, unversionedCaptures[index]); 122 index++; 123 } while (fileString.indexOf(UNVERSIONED_PLACEHOLDER) !== -1); 124 } 125 126 const injectedMacrosOutput = await runTransformPipelineAsync({ 127 pipeline: injectMacros(versionPrefix), 128 input: fileString, 129 targetPath, 130 }); 131 132 return await runTransformPipelineAsync({ 133 pipeline: postTransforms(versionPrefix), 134 input: injectedMacrosOutput, 135 targetPath, 136 }); 137 }); 138 // process `filename` 139 }); 140 141 await Promise.all(filenames.map(transformSingleFile)); 142} 143 144/** 145 * Transform and rename all code files we care about under `rnPath` 146 */ 147async function transformReactNativeAsync(rnPath, versionName, versionedPodNames) { 148 const filenameQueries = [`${rnPath}/**/*.[hmSc]`, `${rnPath}/**/*.mm`, `${rnPath}/**/*.cpp`]; 149 let filenames: string[] = []; 150 await Promise.all( 151 filenameQueries.map(async (query) => { 152 const queryFilenames = (await glob(query)) as string[]; 153 if (queryFilenames) { 154 filenames = filenames.concat(queryFilenames); 155 } 156 }) 157 ); 158 159 return namespaceReactNativeFilesAsync(filenames, versionName, versionedPodNames); 160} 161 162/** 163 * For all files matching the given glob query, namespace and rename them 164 * with the given version number. This utility is mainly useful for backporting 165 * small changes into an existing SDK. To create a new SDK version, use `addVersionAsync` 166 * instead. 167 * @param globQuery a string to pass to glob which matches some file paths 168 * @param versionNumber Exponent SDK version, e.g. 42.0.0 169 */ 170export async function versionReactNativeIOSFilesAsync(globQuery, versionNumber) { 171 const filenames = await glob(globQuery); 172 if (!filenames || !filenames.length) { 173 throw new Error(`No files matched the given pattern: ${globQuery}`); 174 } 175 const { versionName, versionedPodNames } = await getConfigsFromArguments(versionNumber); 176 console.log(`Versioning ${filenames.length} files with SDK version ${versionNumber}...`); 177 return namespaceReactNativeFilesAsync(filenames, versionName, versionedPodNames); 178} 179 180async function generateVersionedReactNativeAsync(versionName: string): Promise<void> { 181 const versionedReactNativePath = getVersionedReactNativePath(versionName); 182 183 await fs.mkdirs(versionedReactNativePath); 184 185 // Clone react native latest version 186 console.log(`Copying files from ${chalk.magenta(RELATIVE_RN_PATH)} ...`); 187 188 const filesToCopy = [ 189 'React', 190 'Libraries', 191 'React.podspec', 192 'React-Core.podspec', 193 'ReactCommon/ReactCommon.podspec', 194 'ReactCommon/React-Fabric.podspec', 195 'ReactCommon/React-bridging.podspec', 196 'package.json', 197 ]; 198 199 for (const fileToCopy of filesToCopy) { 200 await fs.copy( 201 path.join(EXPO_DIR, RELATIVE_RN_PATH, fileToCopy), 202 path.join(versionedReactNativePath, fileToCopy) 203 ); 204 } 205 206 console.log(`Removing unnecessary ${chalk.magenta('*.js')} files ...`); 207 208 const jsFiles = (await glob(path.join(versionedReactNativePath, '**', '*.js'))) as string[]; 209 210 for (const jsFile of jsFiles) { 211 await fs.remove(jsFile); 212 } 213 await Promise.all(jsFiles.map((jsFile) => fs.remove(jsFile))); 214 215 console.log('Running react-native-codegen'); 216 await runReactNativeCodegenAsync({ 217 reactNativeRoot: path.join(EXPO_DIR, RELATIVE_RN_PATH), 218 codegenPkgRoot: path.join(EXPO_DIR, RELATIVE_RN_PATH, 'packages', 'react-native-codegen'), 219 outputDir: path.join(versionedReactNativePath, 'codegen', 'ios'), 220 outputDirBaseName: `${versionName}FBReactNativeSpec`, 221 name: 'FBReactNativeSpec', 222 type: 'modules', 223 platform: 'ios', 224 jsSrcsDir: path.join(EXPO_DIR, RELATIVE_RN_PATH, 'Libraries'), 225 keepIntermediateSchema: true, 226 }); 227 228 console.log( 229 `Copying cpp libraries from ${chalk.magenta(path.join(RELATIVE_RN_PATH, 'ReactCommon'))} ...` 230 ); 231 const cppLibraries = getCppLibrariesToVersion(); 232 233 await fs.mkdirs(path.join(versionedReactNativePath, 'ReactCommon')); 234 235 for (const library of cppLibraries) { 236 await fs.copy( 237 path.join(EXPO_DIR, RELATIVE_RN_PATH, 'ReactCommon', library.libName), 238 path.join(versionedReactNativePath, 'ReactCommon', library.libName) 239 ); 240 } 241 242 await generateReactNativePodScriptAsync(versionedReactNativePath, versionName); 243 await generateReactNativePodspecsAsync(versionedReactNativePath, versionName); 244} 245 246/** 247 * There are some kernel files that unfortunately have to call versioned code directly. 248 * This function applies the specified changes in the kernel codebase. 249 * The nature of kernel modifications is that they are temporary and at one point these have to be rollbacked. 250 * @param versionName SDK version, e.g. 21.0.0, 37.0.0, etc. 251 * @param rollback flag indicating whether to invoke rollbacking modification. 252 */ 253async function modifyKernelFilesAsync( 254 versionName: string, 255 rollback: boolean = false 256): Promise<void> { 257 const kernelFilesPath = path.join(IOS_DIR, 'Exponent/kernel'); 258 const filenameQueries = [`${kernelFilesPath}/**/EXAppViewController.m`]; 259 let filenames: string[] = []; 260 await Promise.all( 261 filenameQueries.map(async (query) => { 262 const queryFilenames = (await glob(query)) as string[]; 263 if (queryFilenames) { 264 filenames = filenames.concat(queryFilenames); 265 } 266 }) 267 ); 268 await Promise.all( 269 filenames.map(async (filename) => { 270 console.log(`Modifying ${chalk.magenta(path.relative(EXPO_DIR, filename))}:`); 271 await _transformFileContentsAsync(filename, (fileContents) => 272 runTransformPipelineAsync({ 273 pipeline: kernelFilesTransforms(versionName, rollback), 274 targetPath: filename, 275 input: fileContents, 276 }) 277 ); 278 }) 279 ); 280} 281/** 282 * - Copies `scripts/react_native_pods.rb` script into versioned ReactNative directory. 283 * - Removes pods installed from third-party-podspecs (we don't version them). 284 * - Versions `use_react_native` method and all pods it declares. 285 */ 286async function generateReactNativePodScriptAsync( 287 versionedReactNativePath: string, 288 versionName: string 289): Promise<void> { 290 const reactCodegenDependencies = [ 291 'FBReactNativeSpec', 292 'React-jsiexecutor', 293 'RCTRequired', 294 'RCTTypeSafety', 295 'React-Core', 296 'React-jsi', 297 'ReactCommon/turbomodule/core', 298 'React-graphics', 299 'React-rncore', 300 ]; 301 302 const reactNativePodScriptTransforms: StringTransform[] = [ 303 { 304 find: /\b(def (use_react_native|use_react_native_codegen))!/g, 305 replaceWith: `$1_${versionName}!`, 306 }, 307 { 308 find: /(\bpod\s+([^\n]+)\/third-party-podspecs\/([^\n]+))/g, 309 replaceWith: '# $1', 310 }, 311 { 312 find: /\bpod\s+'([^\']+)'/g, 313 replaceWith: `pod '${versionName}$1'`, 314 }, 315 { 316 find: /(:path => "[^"]+")/g, 317 replaceWith: `$1, :project_name => '${versionName}'`, 318 }, 319 320 // Removes duplicated constants 321 { 322 find: "DEFAULT_OTHER_CPLUSPLUSFLAGS = '$(inherited)'", 323 replaceWith: '', 324 }, 325 { 326 find: "NEW_ARCH_OTHER_CPLUSPLUSFLAGS = '$(inherited) -DRCT_NEW_ARCH_ENABLED=1 -DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1'", 327 replaceWith: '', 328 }, 329 330 // Since `React-Codegen.podspec` is generated during `pod install`, versioning should be done in the pod script. 331 { 332 find: "$CODEGEN_OUTPUT_DIR = 'build/generated/ios'", 333 replaceWith: `$CODEGEN_OUTPUT_DIR = '${path.relative( 334 IOS_DIR, 335 versionedReactNativePath 336 )}/codegen/ios'`, 337 }, 338 { 339 find: /\$(CODEGEN_OUTPUT_DIR)\b/g, 340 replaceWith: `$${versionName}$1`, 341 }, 342 { find: /\b(React-Codegen)\b/g, replaceWith: `${versionName}$1` }, 343 { find: /(\$\(PODS_ROOT\)\/Headers\/Private\/)React-/g, replaceWith: `$1${versionName}React-` }, 344 { 345 find: new RegExp( 346 `["'](${reactCodegenDependencies.join('|')})["']:(\\s*\\[version\\],?)`, 347 'g' 348 ), 349 replaceWith: `"${versionName}$1":$2`, 350 }, 351 ]; 352 353 const transforms: FileTransforms = { 354 content: [ 355 ...reactNativePodScriptTransforms.map((stringTransform) => ({ 356 path: 'react_native_pods.rb', 357 ...stringTransform, 358 })), 359 { 360 paths: ['react_native_pods.rb', 'script_phases.rb'], 361 find: /\b(get_script_phases_with_codegen_discovery|get_script_phases_no_codegen_discovery|get_script_template)\b/g, 362 replaceWith: `$1_${versionName}`, 363 }, 364 ], 365 }; 366 367 const reactNativeScriptsDir = path.join(EXPO_DIR, RELATIVE_RN_PATH, 'scripts'); 368 const scriptFiles = await glob('**/*', { cwd: reactNativeScriptsDir, nodir: true, dot: true }); 369 await Promise.all( 370 scriptFiles.map(async (file) => { 371 await copyFileWithTransformsAsync({ 372 sourceFile: file, 373 sourceDirectory: reactNativeScriptsDir, 374 targetDirectory: path.join(versionedReactNativePath, 'scripts'), 375 transforms, 376 keepFileMode: true, 377 }); 378 }) 379 ); 380} 381 382async function generateReactNativePodspecsAsync( 383 versionedReactNativePath: string, 384 versionName: string 385): Promise<void> { 386 const podspecFiles = await glob(path.join(versionedReactNativePath, '**', '*.podspec')); 387 388 for (const podspecFile of podspecFiles) { 389 const basename = path.basename(podspecFile, '.podspec'); 390 391 if (/^react$/i.test(basename)) { 392 continue; 393 } 394 395 console.log( 396 `Generating podspec for ${chalk.green(basename)} at ${chalk.magenta( 397 path.relative(versionedReactNativePath, podspecFile) 398 )} ...` 399 ); 400 401 const podspecSource = await fs.readFile(podspecFile, 'utf8'); 402 403 const podspecOutput = await runTransformPipelineAsync({ 404 pipeline: podspecTransforms(versionName), 405 input: podspecSource, 406 targetPath: podspecFile, 407 }); 408 409 // Write transformed podspec output to the prefixed file. 410 await fs.writeFile( 411 path.join(path.dirname(podspecFile), `${versionName}${basename}.podspec`), 412 podspecOutput 413 ); 414 415 // Remove original and unprefixed podspec. 416 await fs.remove(podspecFile); 417 } 418 419 await generateReactPodspecAsync(versionedReactNativePath, versionName); 420} 421 422/** 423 * @param versionName Version prefix (e.g. `ABI43_0_0`) 424 * @param sdkNumber Major version of the SDK 425 */ 426async function generateVersionedExpoAsync(versionName: string, sdkNumber: number): Promise<void> { 427 const versionedExpoKitPath = getVersionedExpoKitPath(versionName); 428 const versionedUnimodulePods = await getVersionedUnimodulePodsAsync(versionName); 429 430 await fs.mkdirs(versionedExpoKitPath); 431 432 // Copy versioned exponent modules into the clone 433 console.log(`Copying versioned native modules into the new Pod...`); 434 435 await fs.copy(path.join(IOS_DIR, 'Exponent', 'Versioned'), versionedExpoKitPath); 436 437 await fs.copy( 438 path.join(EXPO_DIR, 'ios', 'ExpoKit.podspec'), 439 path.join(versionedExpoKitPath, 'ExpoKit.podspec') 440 ); 441 442 console.log(`Generating podspec for ${chalk.green('ExpoKit')} ...`); 443 444 await generateExpoKitPodspecAsync( 445 versionedExpoKitPath, 446 versionedUnimodulePods, 447 versionName, 448 `${sdkNumber}.0.0` 449 ); 450 451 logger.info(' Generating Swift modules provider'); 452 453 await versionExpoModulesProviderAsync(sdkNumber); 454} 455 456/** 457 * Transforms ExpoKit.podspec, versioning Expo namespace, React pod name, replacing original ExpoKit podspecs 458 * with Expo and ExpoOptional. 459 * @param specfilePath location of ExpoKit.podspec to modify, e.g. /versioned-react-native/someversion/ 460 * @param versionedReactPodName name of the new pod (and podfile) 461 * @param universalModulesPodNames versioned names of universal modules 462 * @param versionNumber "XX.X.X" 463 */ 464async function generateExpoKitPodspecAsync( 465 specfilePath: string, 466 universalModulesPodNames: { [key: string]: string }, 467 versionName: string, 468 versionNumber: string 469): Promise<void> { 470 const versionedReactPodName = getVersionedReactPodName(versionName); 471 const versionedExpoKitPodName = getVersionedExpoKitPodName(versionName); 472 const specFilename = path.join(specfilePath, 'ExpoKit.podspec'); 473 474 // rename spec to newPodName 475 const sedPattern = `s/\\(s\\.name[[:space:]]*=[[:space:]]\\)"ExpoKit"/\\1"${versionedExpoKitPodName}"/g`; 476 477 await spawnAsync('sed', ['-i', '--', sedPattern, specFilename]); 478 479 // further processing that sed can't do very well 480 await _transformFileContentsAsync(specFilename, async (fileString) => { 481 // `universalModulesPodNames` contains only versioned unimodules, 482 // so we fall back to the original name if the module is not there 483 const universalModulesDependencies = (await getListOfPackagesAsync()) 484 .filter( 485 (pkg) => 486 pkg.isIncludedInExpoClientOnPlatform('ios') && 487 pkg.podspecName && 488 !EXCLUDED_POD_DEPENDENCIES.includes(pkg.podspecName) 489 ) 490 .map( 491 ({ podspecName }) => 492 `ss.dependency "${universalModulesPodNames[podspecName!] || podspecName}"` 493 ).join(` 494 `); 495 const externalDependencies = EXTERNAL_REACT_ABI_DEPENDENCIES.map( 496 (podName) => `ss.dependency "${podName}"` 497 ).join(` 498 `); 499 const subspec = `s.subspec "Expo" do |ss| 500 ss.source_files = "Core/**/*.{h,m,mm,cpp}" 501 502 ss.dependency "${versionedReactPodName}-Core" 503 ss.dependency "${versionedReactPodName}-Core/DevSupport" 504 ss.dependency "${versionedReactPodName}Common" 505 ss.dependency "${versionName}RCTRequired" 506 ss.dependency "${versionName}RCTTypeSafety" 507 ${universalModulesDependencies} 508 ${externalDependencies} 509 ss.dependency "${versionName}${MODULES_PROVIDER_POD_NAME}" 510 end 511 512 s.subspec "ExpoOptional" do |ss| 513 ss.dependency "${versionedExpoKitPodName}/Expo" 514 ss.source_files = "Optional/**/*.{h,m,mm}" 515 end`; 516 fileString = fileString.replace( 517 /(s\.subspec ".+?"[\S\s]+?(?=end\b)end\b[\s]+)+/g, 518 `${subspec}\n` 519 ); 520 521 // correct version number 522 fileString = fileString.replace(/(?<=s.version = ").*?(?=")/g, versionNumber); 523 524 // add Reanimated V2 RCT-Folly dependency 525 fileString = fileString 526 .replace( 527 /(?=Pod::Spec.new do \|s\|)/, 528 ` 529folly_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1' 530folly_compiler_flags = folly_flags + ' ' + '-Wno-comma -Wno-shorten-64-to-32' 531boost_compiler_flags = '-Wno-documentation'\n\n` 532 ) 533 .replace( 534 /(?= s.subspec "Expo" do \|ss\|)/g, 535 ` 536 s.pod_target_xcconfig = { 537 "USE_HEADERMAP" => "YES", 538 "HEADER_SEARCH_PATHS" => "\\"$(PODS_TARGET_SRCROOT)/ReactCommon\\" \\"$(PODS_TARGET_SRCROOT)\\" \\"$(PODS_ROOT)/RCT-Folly\\" \\"$(PODS_ROOT)/boost\\" \\"$(PODS_ROOT)/DoubleConversion\\" \\"$(PODS_ROOT)/Headers/Private/React-Core\\" " 539 } 540 s.compiler_flags = folly_compiler_flags + ' ' + boost_compiler_flags 541 s.xcconfig = { 542 "HEADER_SEARCH_PATHS" => "\\"$(PODS_ROOT)/boost\\" \\"$(PODS_ROOT)/glog\\" \\"$(PODS_ROOT)/RCT-Folly\\" \\"$(PODS_ROOT)/Headers/Private/${versionName}React-Core\\"", 543 "OTHER_CFLAGS" => "$(inherited)" + " " + folly_flags 544 }\n\n` 545 ); 546 547 return fileString; 548 }); 549 550 // move podspec to ${versionedExpoKitPodName}.podspec 551 await fs.move(specFilename, path.join(specfilePath, `${versionedExpoKitPodName}.podspec`)); 552} 553 554/** 555 * @param specfilePath location of React.podspec to modify, e.g. /versioned-react-native/someversion/ 556 * @param versionedReactPodName name of the new pod (and podfile) 557 */ 558async function generateReactPodspecAsync(versionedReactNativePath, versionName) { 559 const versionedReactPodName = getVersionedReactPodName(versionName); 560 const versionedYogaPodName = getVersionedYogaPodName(versionName); 561 const versionedJSIPodName = getVersionedJSIPodName(versionName); 562 const specFilename = path.join(versionedReactNativePath, 'React.podspec'); 563 564 // rename spec to newPodName 565 const sedPattern = `s/\\(s\\.name[[:space:]]*=[[:space:]]\\)"React"/\\1"${versionedReactPodName}"/g`; 566 await spawnAsync('sed', ['-i', '--', sedPattern, specFilename]); 567 568 // rename header_dir 569 await spawnAsync('sed', [ 570 '-i', 571 '--', 572 `s/^\\(.*header_dir.*\\)React\\(.*\\)$/\\1${versionedReactPodName}\\2/`, 573 specFilename, 574 ]); 575 await spawnAsync('sed', [ 576 '-i', 577 '--', 578 `s/^\\(.*header_dir.*\\)jsireact\\(.*\\)$/\\1${versionedJSIPodName}\\2/`, 579 specFilename, 580 ]); 581 582 // point source at . 583 const newPodSource = `{ :path => "." }`; 584 await spawnAsync('sed', [ 585 '-i', 586 '--', 587 `s/\\(s\\.source[[:space:]]*=[[:space:]]\\).*/\\1${newPodSource}/g`, 588 specFilename, 589 ]); 590 591 // further processing that sed can't do very well 592 await _transformFileContentsAsync(specFilename, (fileString) => { 593 // replace React/* dependency with ${versionedReactPodName}/* 594 fileString = fileString.replace( 595 /(\.dependency\s+)"React([^"]+)"/g, 596 `$1"${versionedReactPodName}$2"` 597 ); 598 599 fileString = fileString.replace('/RCTTV', `/${versionName}RCTTV`); 600 601 // namespace cpp libraries 602 const cppLibraries = getCppLibrariesToVersion(); 603 cppLibraries.forEach(({ libName }) => { 604 fileString = fileString.replace( 605 new RegExp(`([^A-Za-z0-9_])${libName}([^A-Za-z0-9_])`, 'g'), 606 `$1${getVersionedLibraryName(libName, versionName)}$2` 607 ); 608 }); 609 610 // fix wrong Yoga pod name 611 fileString = fileString.replace( 612 /^(.*dependency.*["']).*yoga.*?(["'].*)$/m, 613 `$1${versionedYogaPodName}$2` 614 ); 615 616 return fileString; 617 }); 618 619 // move podspec to ${versionedReactPodName}.podspec 620 await fs.move( 621 specFilename, 622 path.join(versionedReactNativePath, `${versionedReactPodName}.podspec`) 623 ); 624} 625 626function getCFlagsToPrefixGlobals(prefix, globals) { 627 return globals.map((val) => `-D${val}=${prefix}${val}`); 628} 629 630/** 631 * Generates `dependencies.rb` and `postinstalls.rb` files for versioned code. 632 * @param versionNumber Semver-compliant version of the SDK/ABI 633 * @param versionName Version prefix used for versioned files, e.g. ABI99_0_0 634 * @param versionedPodNames mapping from pod names to versioned pod names, e.g. React -> ReactABI99_0_0 635 * @param versionedReactPodPath path of the new react pod 636 */ 637async function generatePodfileSubscriptsAsync( 638 versionNumber: string, 639 versionName: string, 640 versionedPodNames: Record<string, string>, 641 versionedReactPodPath: string 642) { 643 if (!versionedPodNames.React) { 644 throw new Error( 645 'Tried to add generate pod dependencies, but missing a name for the versioned library.' 646 ); 647 } 648 649 const relativeReactNativePath = path.relative(IOS_DIR, getVersionedReactNativePath(versionName)); 650 const relativeExpoKitPath = path.relative(IOS_DIR, getVersionedExpoKitPath(versionName)); 651 652 // Add a dependency on newPodName 653 const dependenciesContent = `# @generated by expotools 654 655require './${relativeReactNativePath}/scripts/react_native_pods.rb' 656 657use_react_native_${versionName}! path: './${relativeReactNativePath}' 658 659pod '${getVersionedExpoKitPodName(versionName)}', 660 :path => './${relativeExpoKitPath}', 661 :project_name => '${versionName}', 662 :subspecs => ['Expo', 'ExpoOptional'] 663 664use_pods! '{versioned,vendored}/sdk${semver.major( 665 versionNumber 666 )}/**/*.podspec.json', '${versionName}' 667`; 668 669 await fs.writeFile(path.join(versionedReactPodPath, 'dependencies.rb'), dependenciesContent); 670 671 // Add postinstall. 672 // In particular, resolve conflicting globals from React by redefining them. 673 const globals = { 674 React: [ 675 // RCTNavigator 676 'kNeverRequested', 677 'kNeverProgressed', 678 // react-native-maps 679 'kSMCalloutViewRepositionDelayForUIScrollView', 680 'regionAsJSON', 681 'unionRect', 682 // jschelpers 683 'JSNoBytecodeFileFormatVersion', 684 'JSSamplingProfilerEnabled', 685 // RCTInspectorPackagerConnection 686 'RECONNECT_DELAY_MS', 687 // RCTSpringAnimation 688 'MAX_DELTA_TIME', 689 ], 690 yoga: [ 691 'gCurrentGenerationCount', 692 'gPrintSkips', 693 'gPrintChanges', 694 'layoutNodeInternal', 695 'gDepth', 696 'gPrintTree', 697 'isUndefined', 698 'gNodeInstanceCount', 699 ], 700 }; 701 const configValues = getCFlagsToPrefixGlobals( 702 versionedPodNames.React, 703 globals.React.concat(globals.yoga) 704 ); 705 const indent = ' '.repeat(3); 706 const config = `# @generated by expotools 707 708if pod_name.start_with?('${versionedPodNames.React}') || pod_name == '${versionedPodNames.ExpoKit}' 709 target_installation_result.native_target.build_configurations.each do |config| 710 config.build_settings['OTHER_CFLAGS'] = %w[ 711 ${configValues.join(`\n${indent}`)} 712 ] 713 config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ['$(inherited)'] 714 config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << '${versionName}RCT_DEV=1' 715 config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << '${versionName}RCT_ENABLE_INSPECTOR=0' 716 config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << '${versionName}RCT_DEV_SETTINGS_ENABLE_PACKAGER_CONNECTION=0' 717 # Enable Google Maps support 718 config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << '${versionName}HAVE_GOOGLE_MAPS=1' 719 config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << '${versionName}HAVE_GOOGLE_MAPS_UTILS=1' 720 end 721end 722`; 723 await fs.writeFile(path.join(versionedReactPodPath, 'postinstalls.rb'), config); 724} 725 726/** 727 * @param transformConfig function that takes a config dict and returns a new config dict. 728 */ 729async function modifyVersionConfigAsync(configPath, transformConfig) { 730 const jsConfigFilename = `${configPath}/sdkVersions.json`; 731 await _transformFileContentsAsync(jsConfigFilename, (jsConfigContents) => { 732 let jsConfig; 733 734 // read the existing json config and add the new version to the sdkVersions array 735 try { 736 jsConfig = JSON.parse(jsConfigContents); 737 } catch (e) { 738 console.log('Error parsing existing sdkVersions.json file, writing a new one...', e); 739 console.log('The erroneous file contents was:', jsConfigContents); 740 jsConfig = { 741 sdkVersions: [], 742 }; 743 } 744 // apply changes 745 jsConfig = transformConfig(jsConfig); 746 return JSON.stringify(jsConfig); 747 }); 748 749 // convert json config to plist for iOS 750 await spawnAsync('plutil', [ 751 '-convert', 752 'xml1', 753 jsConfigFilename, 754 '-o', 755 path.join(configPath, 'EXSDKVersions.plist'), 756 ]); 757} 758 759function validateAddVersionDirectories(rootPath, newVersionPath) { 760 // Make sure the paths we want to read are available 761 const relativePathsToCheck = [ 762 RELATIVE_RN_PATH, 763 'ios/versioned-react-native', 764 'ios/Exponent', 765 'ios/Exponent/Versioned', 766 ]; 767 let isValid = true; 768 relativePathsToCheck.forEach((path) => { 769 try { 770 fs.accessSync(`${rootPath}/${path}`, fs.constants.F_OK); 771 } catch { 772 console.log(`${rootPath}/${path} does not exist or is otherwise inaccessible`); 773 isValid = false; 774 } 775 }); 776 // Also, make sure the version we're about to write doesn't already exist 777 try { 778 // we want this to fail 779 fs.accessSync(newVersionPath, fs.constants.F_OK); 780 console.log(`${newVersionPath} already exists, will not overwrite`); 781 isValid = false; 782 } catch {} 783 784 return isValid; 785} 786 787function validateRemoveVersionDirectories(rootPath, newVersionPath) { 788 const pathsToCheck = [ 789 `${rootPath}/ios/versioned-react-native`, 790 `${rootPath}/ios/Exponent`, 791 newVersionPath, 792 ]; 793 let isValid = true; 794 pathsToCheck.forEach((path) => { 795 try { 796 fs.accessSync(path, fs.constants.F_OK); 797 } catch { 798 console.log(`${path} does not exist or is otherwise inaccessible`); 799 isValid = false; 800 } 801 }); 802 return isValid; 803} 804 805async function getConfigsFromArguments(versionNumber) { 806 let versionComponents = versionNumber.split('.'); 807 versionComponents = versionComponents.map((number) => parseInt(number, 10)); 808 const versionName = 'ABI' + versionNumber.replace(/\./g, '_'); 809 const rootPathComponents = EXPO_DIR.split('/'); 810 const versionPathComponents = path.join('ios', 'versioned-react-native', versionName).split('/'); 811 const newVersionPath = rootPathComponents.concat(versionPathComponents).join('/'); 812 813 const versionedPodNames = { 814 React: getVersionedReactPodName(versionName), 815 yoga: getVersionedYogaPodName(versionName), 816 ExpoKit: getVersionedExpoKitPodName(versionName), 817 jsireact: getVersionedJSIPodName(versionName), 818 }; 819 820 return { 821 sdkNumber: semver.major(versionNumber), 822 versionName, 823 newVersionPath, 824 versionedPodNames, 825 versionComponents, 826 }; 827} 828 829async function getVersionedUnimodulePodsAsync( 830 versionName: string 831): Promise<{ [key: string]: string }> { 832 const versionedUnimodulePods = {}; 833 const packages = await getListOfPackagesAsync(); 834 835 packages.forEach((pkg) => { 836 const podName = pkg.podspecName; 837 if (podName && pkg.isVersionableOnPlatform('ios')) { 838 versionedUnimodulePods[podName] = `${versionName}${podName}`; 839 } 840 }); 841 842 return versionedUnimodulePods; 843} 844 845function getVersionedReactPodName(versionName: string): string { 846 return getVersionedLibraryName('React', versionName); 847} 848 849function getVersionedYogaPodName(versionName: string): string { 850 return getVersionedLibraryName('Yoga', versionName); 851} 852 853function getVersionedJSIPodName(versionName: string): string { 854 return getVersionedLibraryName('jsiReact', versionName); 855} 856 857function getVersionedExpoKitPodName(versionName: string): string { 858 return getVersionedLibraryName('ExpoKit', versionName); 859} 860 861function getVersionedLibraryName(libraryName: string, versionName: string): string { 862 return `${versionName}${libraryName}`; 863} 864 865function getVersionedReactNativePath(versionName: string): string { 866 return path.join(VERSIONED_RN_IOS_DIR, versionName, 'ReactNative'); 867} 868 869function getVersionedExpoPath(versionName: string): string { 870 return path.join(VERSIONED_RN_IOS_DIR, versionName, 'Expo'); 871} 872 873function getCppLibrariesToVersion() { 874 return [ 875 { 876 libName: 'cxxreact', 877 }, 878 { 879 libName: 'jsi', 880 }, 881 { 882 libName: 'jsiexecutor', 883 customHeaderDir: 'jsireact', 884 }, 885 { 886 libName: 'jsinspector', 887 }, 888 { 889 libName: 'yoga', 890 }, 891 { 892 libName: 'react', 893 }, 894 { 895 libName: 'callinvoker', 896 customHeaderDir: 'ReactCommon', 897 }, 898 { 899 libName: 'reactperflogger', 900 }, 901 { 902 libName: 'runtimeexecutor', 903 }, 904 { 905 libName: 'logger', 906 }, 907 ]; 908} 909 910export async function addVersionAsync(versionNumber: string, packages: Package[]) { 911 const { sdkNumber, versionName, newVersionPath, versionedPodNames } = 912 await getConfigsFromArguments(versionNumber); 913 914 // Validate the directories we need before doing anything 915 console.log(`Validating root directory ${chalk.magenta(EXPO_DIR)} ...`); 916 const isFilesystemReady = validateAddVersionDirectories(EXPO_DIR, newVersionPath); 917 if (!isFilesystemReady) { 918 throw new Error('Aborting: At least one directory we need is not available'); 919 } 920 921 if (!versionedPodNames.React) { 922 throw new Error('Missing name for versioned pod dependency.'); 923 } 924 925 // Create ABIXX_0_0 directory. 926 console.log( 927 `Creating new ABI version ${chalk.cyan(versionNumber)} at ${chalk.magenta( 928 path.relative(EXPO_DIR, newVersionPath) 929 )}` 930 ); 931 await fs.mkdirs(newVersionPath); 932 933 // Generate new Podspec from the existing React.podspec 934 console.log('Generating versioned ReactNative directory...'); 935 await generateVersionedReactNativeAsync(versionName); 936 937 console.log( 938 `Generating ${chalk.magenta( 939 path.relative(EXPO_DIR, getVersionedExpoPath(versionName)) 940 )} directory...` 941 ); 942 await generateVersionedExpoAsync(versionName, sdkNumber); 943 944 await versionExpoModulesAsync(sdkNumber, packages); 945 946 // Generate versioned Swift modules provider 947 await versionExpoModulesProviderAsync(sdkNumber); 948 949 // Namespace the new React clone 950 console.log('Namespacing/transforming files...'); 951 await transformReactNativeAsync(newVersionPath, versionName, versionedPodNames); 952 953 // Generate Ruby scripts with versioned dependencies and postinstall actions that will be evaluated in the Expo client's Podfile. 954 console.log('Adding dependency to root Podfile...'); 955 await generatePodfileSubscriptsAsync( 956 versionNumber, 957 versionName, 958 versionedPodNames, 959 newVersionPath 960 ); 961 962 // Add the new version to the iOS config list of available versions 963 console.log('Registering new version under sdkVersions config...'); 964 const addVersionToConfig = (config, versionNumber) => { 965 config.sdkVersions.push(versionNumber); 966 return config; 967 }; 968 await modifyVersionConfigAsync(path.join(IOS_DIR, 'Exponent', 'Supporting'), (config) => 969 addVersionToConfig(config, versionNumber) 970 ); 971 await modifyVersionConfigAsync( 972 path.join(EXPO_DIR, 'exponent-view-template', 'ios', 'exponent-view-template', 'Supporting'), 973 (config) => addVersionToConfig(config, versionNumber) 974 ); 975 976 // Modifying kernel files 977 console.log(`Modifying ${chalk.bold('kernel files')} to incorporate new SDK version...`); 978 await modifyKernelFilesAsync(versionName); 979 980 console.log('Removing any `filename--` files from the new pod ...'); 981 982 try { 983 const minusMinusFiles = [ 984 ...(await glob(path.join(newVersionPath, '**', '*--'))), 985 ...(await glob(path.join(IOS_DIR, 'build', versionName, 'generated', 'ios', '**', '*--'))), 986 ]; 987 for (const minusMinusFile of minusMinusFiles) { 988 await fs.remove(minusMinusFile); 989 } 990 } catch { 991 console.warn( 992 "The script wasn't able to remove any possible `filename--` files created by sed. Please ensure there are no such files manually." 993 ); 994 } 995 996 console.log('Finished creating new version.'); 997} 998 999async function askToReinstallPodsAsync(): Promise<boolean> { 1000 if (process.env.CI) { 1001 // If we're on the CI, let's regenerate Pods by default. 1002 return true; 1003 } 1004 const { result } = await inquirer.prompt<{ result: boolean }>([ 1005 { 1006 type: 'confirm', 1007 name: 'result', 1008 message: 'Do you want to reinstall pods?', 1009 default: true, 1010 }, 1011 ]); 1012 return result; 1013} 1014 1015export async function reinstallPodsAsync(force?: boolean, preventReinstall?: boolean) { 1016 if ( 1017 preventReinstall !== true && 1018 (force || (force !== false && (await askToReinstallPodsAsync()))) 1019 ) { 1020 await spawnAsync('pod', ['install'], { stdio: 'inherit', cwd: IOS_DIR }); 1021 console.log( 1022 'Regenerated Podfile and installed new pods. You can now try to build the project in Xcode.' 1023 ); 1024 } else { 1025 console.log( 1026 'Skipped pods regeneration. You might want to run `et ios-generate-dynamic-macros`, then `pod install` in `ios` to configure Xcode project.' 1027 ); 1028 } 1029} 1030 1031export async function removeVersionAsync(versionNumber: string) { 1032 const { sdkNumber, newVersionPath, versionedPodNames, versionName } = 1033 await getConfigsFromArguments(versionNumber); 1034 1035 console.log( 1036 `Removing SDK version ${chalk.cyan(versionNumber)} from ${chalk.magenta( 1037 path.relative(EXPO_DIR, newVersionPath) 1038 )} with Pod name ${chalk.green(versionedPodNames.React)}` 1039 ); 1040 1041 // Validate the directories we need before doing anything 1042 console.log(`Validating root directory ${chalk.magenta(EXPO_DIR)} ...`); 1043 const isFilesystemReady = validateRemoveVersionDirectories(EXPO_DIR, newVersionPath); 1044 if (!isFilesystemReady) { 1045 console.log('Aborting: At least one directory we expect is not available'); 1046 return; 1047 } 1048 1049 // remove directory 1050 console.log( 1051 `Removing versioned files under ${chalk.magenta(path.relative(EXPO_DIR, newVersionPath))}...` 1052 ); 1053 await fs.remove(newVersionPath); 1054 await fs.remove(getVersionedDirectory(sdkNumber)); 1055 1056 console.log('Removing vendored libraries...'); 1057 await removeVersionedVendoredModulesAsync(semver.major(versionNumber)); 1058 1059 // remove dep from main podfile 1060 console.log(`Removing ${chalk.green(versionedPodNames.React)} dependency from root Podfile...`); 1061 1062 // remove from sdkVersions.json 1063 console.log('Unregistering version from sdkVersions config...'); 1064 const removeVersionFromConfig = (config, versionNumber) => { 1065 const index = config.sdkVersions.indexOf(versionNumber); 1066 if (index > -1) { 1067 // modify in place 1068 config.sdkVersions.splice(index, 1); 1069 } 1070 return config; 1071 }; 1072 await modifyVersionConfigAsync(path.join(IOS_DIR, 'Exponent', 'Supporting'), (config) => 1073 removeVersionFromConfig(config, versionNumber) 1074 ); 1075 await modifyVersionConfigAsync( 1076 path.join(EXPO_DIR, 'exponent-view-template', 'ios', 'exponent-view-template', 'Supporting'), 1077 (config) => removeVersionFromConfig(config, versionNumber) 1078 ); 1079 1080 // modify kernel files 1081 console.log('Rollbacking SDK modifications from kernel files...'); 1082 await modifyKernelFilesAsync(versionName, true); 1083 1084 // Update `ios/ExpoKit.podspec` with the newest SDK version 1085 logger.info(' Updating ExpoKit podspec'); 1086 await renderExpoKitPodspecAsync(EXPO_DIR, path.join(EXPO_DIR, 'template-files')); 1087 1088 await reinstallPodsAsync(); 1089} 1090 1091/** 1092 * @return an array of objects representing react native transform rules. 1093 * objects must contain 'pattern' and may optionally contain 'paths' to limit 1094 * the transform to certain file paths. 1095 * 1096 * the rules are applied in order! 1097 */ 1098function _getReactNativeTransformRules(versionPrefix, reactPodName) { 1099 const cppLibraries = getCppLibrariesToVersion().map((lib) => lib.customHeaderDir || lib.libName); 1100 const versionedLibs = [...cppLibraries, 'React', 'FBLazyVector', 'FBReactNativeSpec']; 1101 1102 return [ 1103 { 1104 // Change Obj-C symbols prefix 1105 pattern: `s/RCT/${versionPrefix}RCT/g`, 1106 }, 1107 { 1108 pattern: `s/^EX/${versionPrefix}EX/g`, 1109 // paths: 'EX', 1110 }, 1111 { 1112 pattern: `s/^UM/${versionPrefix}UM/g`, 1113 // paths: 'EX', 1114 }, 1115 { 1116 pattern: `s/\\([^\\<\\/"]\\)YG/\\1${versionPrefix}YG/g`, 1117 }, 1118 { 1119 pattern: `s/\\([\\<,]\\)YG/\\1${versionPrefix}YG/g`, 1120 }, 1121 { 1122 pattern: `s/^YG/${versionPrefix}YG/g`, 1123 }, 1124 { 1125 paths: 'Components', 1126 pattern: `s/\\([^+]\\)AIR/\\1${versionPrefix}AIR/g`, 1127 }, 1128 { 1129 flags: '-Ei', 1130 pattern: `s/(^|[^A-Za-z0-9_+])(RN|REA|EX|UM|ART|SM)/\\1${versionPrefix}\\2/g`, 1131 }, 1132 { 1133 paths: 'Core/Api', 1134 pattern: `s/^RN/${versionPrefix}RN/g`, 1135 }, 1136 { 1137 paths: 'Core/Api', 1138 pattern: `s/HAVE_GOOGLE_MAPS/${versionPrefix}HAVE_GOOGLE_MAPS/g`, 1139 }, 1140 { 1141 paths: 'Core/Api', 1142 pattern: `s/#import "Branch/#import "${versionPrefix}Branch/g`, 1143 }, 1144 { 1145 paths: 'Core/Api', 1146 pattern: `s/#import "NSObject+RNBranch/#import "${versionPrefix}NSObject+RNBranch/g`, 1147 }, 1148 { 1149 // React will be prefixed in a moment 1150 pattern: `s/#import <${versionPrefix}RCTAnimation/#import <React/g`, 1151 }, 1152 { 1153 pattern: `s/^REA/${versionPrefix}REA/g`, 1154 paths: 'Core/Api/Reanimated', 1155 }, 1156 { 1157 // Prefixes all direct references to objects under `reanimated` namespace. 1158 // It must be applied before versioning `namespace reanimated` so 1159 // `using namespace reanimated::` don't get versioned twice. 1160 pattern: `s/reanimated::/${versionPrefix}reanimated::/g`, 1161 }, 1162 { 1163 // Prefixes reanimated namespace. 1164 pattern: `s/namespace reanimated/namespace ${versionPrefix}reanimated/g`, 1165 }, 1166 { 1167 // Fix imports in C++ libs in ReactCommon. 1168 // Extended syntax (-E) is required to use (a|b). 1169 flags: '-Ei', 1170 pattern: `s/([<"])(${versionedLibs.join( 1171 '|' 1172 )})\\//\\1${versionPrefix}\\2\\/${versionPrefix}/g`, 1173 }, 1174 { 1175 // Change React -> new pod name 1176 // e.g. threads and queues namespaced to com.facebook.react, 1177 // file paths beginning with the lib name, 1178 // the cpp facebook::react namespace, 1179 // iOS categories ending in +React 1180 flags: '-Ei', 1181 pattern: `s/[Rr]eact/${reactPodName}/g`, 1182 }, 1183 { 1184 // Imports from cxxreact and jsireact got prefixed twice. 1185 flags: '-Ei', 1186 pattern: `s/([<"])(${versionPrefix})(cxx|jsi)${versionPrefix}React/\\1\\2\\3react/g`, 1187 }, 1188 { 1189 // Fix imports from files like `UIView+React.*`. 1190 flags: '-Ei', 1191 pattern: `s/\\+${versionPrefix}React/\\+React/g`, 1192 }, 1193 { 1194 // Prefixes all direct references to objects under `facebook` and `JS` namespaces. 1195 // It must be applied before versioning `namespace facebook` so 1196 // `using namespace facebook::` don't get versioned twice. 1197 flags: '-Ei', 1198 pattern: `s/(facebook|JS)::/${versionPrefix}\\1::/g`, 1199 }, 1200 { 1201 // Prefixes facebook namespace. 1202 flags: '-Ei', 1203 pattern: `s/namespace (facebook|JS)/namespace ${versionPrefix}\\1/g`, 1204 }, 1205 { 1206 // For UMReactNativeAdapter 1207 // Fix names with 'React' substring occurring twice - only first one should be prefixed 1208 flags: '-Ei', 1209 pattern: `s/${versionPrefix}UM([[:alpha:]]*)${reactPodName}/${versionPrefix}UM\\1React/g`, 1210 }, 1211 { 1212 // For EXReactNativeAdapter 1213 pattern: `s/${versionPrefix}EX${reactPodName}/${versionPrefix}EXReact/g`, 1214 }, 1215 { 1216 // For EXConstants and EXNotifications so that when their migrators 1217 // try to access legacy storage for UUID migration, they access the proper value. 1218 pattern: `s/${versionPrefix}EXDeviceInstallUUIDKey/EXDeviceInstallUUIDKey/g`, 1219 paths: 'Expo', 1220 }, 1221 { 1222 // For EXConstants and EXNotifications so that the installation ID 1223 // stays the same between different SDK versions. (https://github.com/expo/expo/issues/11008#issuecomment-726370187) 1224 pattern: `s/${versionPrefix}EXDeviceInstallationUUIDKey/EXDeviceInstallationUUIDKey/g`, 1225 paths: 'Expo', 1226 }, 1227 { 1228 // RCTPlatform exports version of React Native 1229 pattern: `s/${reactPodName}NativeVersion/reactNativeVersion/g`, 1230 }, 1231 { 1232 pattern: `s/@"${versionPrefix}RCT"/@"RCT"/g`, 1233 }, 1234 { 1235 // Unprefix everything that got prefixed twice or more times. 1236 flags: '-Ei', 1237 pattern: `s/(${versionPrefix}){2,}/\\1/g`, 1238 }, 1239 { 1240 flags: '-Ei', 1241 pattern: `s/(#import |__has_include\\()<(Expo|RNReanimated)/\\1<${versionPrefix}\\2/g`, 1242 }, 1243 ]; 1244} 1245 1246function _getTransformRulesForDirname(transformRules, dirname) { 1247 return transformRules.filter((rule) => { 1248 return ( 1249 // no paths specified, so apply rule to everything 1250 !rule.paths || 1251 // otherwise, limit this rule to paths specified 1252 dirname.indexOf(rule.paths) !== -1 1253 ); 1254 }); 1255} 1256 1257// TODO: use the one in XDL 1258function _isDirectory(dir) { 1259 try { 1260 if (fs.statSync(dir).isDirectory()) { 1261 return true; 1262 } 1263 1264 return false; 1265 } catch { 1266 return false; 1267 } 1268} 1269 1270// TODO: use the one in XDL 1271async function _transformFileContentsAsync( 1272 filename: string, 1273 transform: (fileString: string) => Promise<string> | string | null 1274) { 1275 const fileString = await fs.readFile(filename, 'utf8'); 1276 const newFileString = await transform(fileString); 1277 if (newFileString !== null) { 1278 await fs.writeFile(filename, newFileString); 1279 } 1280} 1281