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