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