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