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