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