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