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