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