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