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