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