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