xref: /expo/tools/src/versioning/ios/index.ts (revision cedf99eb)
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