xref: /expo/tools/src/versioning/ios/index.ts (revision 9fbde599)
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();
460
461  for (const pkg of packages) {
462    const modulePath = path.join(EXPO_DIR, RELATIVE_UNIVERSAL_MODULES_PATH, pkg.packageName);
463    const podName = pkg.podspecName;
464
465    if (podName && pkg.isVersionableOnPlatform('ios') && !excludedPodNames.includes(podName)) {
466      await fs.copy(path.join(modulePath, 'ios'), path.join(versionedExpoPath, podName));
467      await fs.move(
468        path.join(versionedExpoPath, podName, podName),
469        path.join(versionedExpoPath, podName, versionedUnimodulePods[podName])
470      );
471      await fs.copy(
472        path.join(modulePath, 'package.json'),
473        path.join(versionedExpoPath, podName, 'package.json')
474      );
475    }
476  }
477
478  for (const originalUnimodulePodName of originalUnimodulePodNames) {
479    const versionedUnimodulePodName = versionedUnimodulePods[originalUnimodulePodName];
480
481    const originalPodSpecPath = path.join(
482      versionedExpoPath,
483      originalUnimodulePodName,
484      `${originalUnimodulePodName}.podspec`
485    );
486    const prefixedPodSpecPath = path.join(
487      versionedExpoPath,
488      originalUnimodulePodName,
489      `${versionedUnimodulePodName}.podspec`
490    );
491
492    if (!(await fs.pathExists(originalPodSpecPath))) {
493      continue;
494    }
495
496    console.log(`Generating podspec for ${chalk.green(originalUnimodulePodName)} ...`);
497
498    await fs.move(originalPodSpecPath, prefixedPodSpecPath);
499
500    // Replaces versioned modules in the podspec eg. 'EXCore' => 'ABI28_0_0EXCore'
501    // `E` flag is required for extended syntax which allows to use `(a|b)`
502    await spawnAsync('sed', [
503      '-Ei',
504      '--',
505      `s/'(${depsToReplace})('|\\/)/'${versionName}\\1\\2/g`,
506      prefixedPodSpecPath,
507    ]);
508    await spawnAsync('sed', [
509      '-i',
510      '--',
511      `s/React/${versionedReactPodName}/g`,
512      prefixedPodSpecPath,
513    ]);
514    await spawnAsync('sed', [
515      '-i',
516      '--',
517      `s/${versionName}UM${versionedReactPodName}/${versionName}UMReact/g`,
518      prefixedPodSpecPath,
519    ]);
520    await spawnAsync('sed', [
521      '-i',
522      '--',
523      "s/'..', 'package.json'/'package.json'/g",
524      prefixedPodSpecPath,
525    ]);
526  }
527
528  console.log(`Generating podspec for ${chalk.green('ExpoKit')} ...`);
529
530  await generateExpoKitPodspecAsync(
531    versionedExpoKitPath,
532    versionedUnimodulePods,
533    versionName,
534    versionNumber
535  );
536}
537
538/**
539 * Transforms ExpoKit.podspec, versioning Expo namespace, React pod name, replacing original ExpoKit podspecs
540 * with Expo and ExpoOptional.
541 * @param specfilePath location of ExpoKit.podspec to modify, e.g. /versioned-react-native/someversion/
542 * @param versionedReactPodName name of the new pod (and podfile)
543 * @param universalModulesPodNames versioned names of universal modules
544 * @param versionNumber "XX.X.X"
545 */
546async function generateExpoKitPodspecAsync(
547  specfilePath: string,
548  universalModulesPodNames: { [key: string]: string },
549  versionName: string,
550  versionNumber: string
551): Promise<void> {
552  const versionedReactPodName = getVersionedReactPodName(versionName);
553  const versionedExpoKitPodName = getVersionedExpoKitPodName(versionName);
554  const specFilename = path.join(specfilePath, 'ExpoKit.podspec');
555  const excludedPodNames = getExcludedPodNames();
556
557  // rename spec to newPodName
558  const sedPattern = `s/\\(s\\.name[[:space:]]*=[[:space:]]\\)"ExpoKit"/\\1"${versionedExpoKitPodName}"/g`;
559
560  await spawnAsync('sed', ['-i', '--', sedPattern, specFilename]);
561
562  // further processing that sed can't do very well
563  await _transformFileContentsAsync(specFilename, async (fileString) => {
564    // `universalModulesPodNames` contains only versioned unimodules,
565    // so we fall back to the original name if the module is not there
566    const universalModulesDependencies = (await getListOfPackagesAsync())
567      .filter(
568        (pkg) =>
569          pkg.isIncludedInExpoClientOnPlatform('ios') &&
570          pkg.podspecName &&
571          !excludedPodNames.includes(pkg.podspecName)
572      )
573      .map(
574        ({ podspecName }) =>
575          `ss.dependency         "${universalModulesPodNames[podspecName!] || podspecName}"`
576      ).join(`
577    `);
578    const externalDependencies = EXTERNAL_REACT_ABI_DEPENDENCIES.map(
579      (podName) => `ss.dependency         "${podName}"`
580    ).join(`
581    `);
582    let subspec = `s.subspec "Expo" do |ss|
583    ss.source_files     = "Core/**/*.{h,m,mm,cpp}"
584
585    ss.dependency         "${versionedReactPodName}-Core"
586    ss.dependency         "${versionedReactPodName}-Core/DevSupport"
587    ss.dependency         "${versionedReactPodName}Common"
588    ${universalModulesDependencies}
589    ${externalDependencies}
590  end
591
592  s.subspec "ExpoOptional" do |ss|
593    ss.dependency         "${versionedExpoKitPodName}/Expo"
594    ss.source_files     = "Optional/**/*.{h,m,mm}"
595  end`;
596    fileString = fileString.replace(
597      /(s\.subspec ".+?"[\S\s]+?(?=end\b)end\b[\s]+)+/g,
598      `${subspec}\n`
599    );
600
601    // correct version number
602    fileString = fileString.replace(/(?<=s.version = ").*?(?=")/g, versionNumber);
603
604    // add Reanimated V2 Folly dependency
605    fileString = fileString
606      .replace(
607        /(?=Pod::Spec.new do \|s\|)/,
608        `
609folly_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1'
610folly_compiler_flags = folly_flags + ' ' + '-Wno-comma -Wno-shorten-64-to-32'
611folly_version = '2020.01.13.00'
612boost_compiler_flags = '-Wno-documentation'\n\n`
613      )
614      .replace(
615        /(?=  s.subspec "Expo" do \|ss\|)/g,
616        `
617  s.pod_target_xcconfig    = {
618    "USE_HEADERMAP"       => "YES",
619    "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\\" "
620  }
621  s.compiler_flags = folly_compiler_flags + ' ' + boost_compiler_flags
622  s.xcconfig               = {
623    "HEADER_SEARCH_PATHS" => "\\"$(PODS_ROOT)/boost-for-react-native\\" \\"$(PODS_ROOT)/glog\\" \\"$(PODS_ROOT)/Folly\\" \\"$(PODS_ROOT)/Headers/Private/${versionName}React-Core\\"",
624    "OTHER_CFLAGS"        => "$(inherited)" + " " + folly_flags
625  }\n\n`
626      );
627
628    return fileString;
629  });
630
631  // move podspec to ${versionedExpoKitPodName}.podspec
632  await fs.move(specFilename, path.join(specfilePath, `${versionedExpoKitPodName}.podspec`));
633}
634
635/**
636 *  @param specfilePath location of React.podspec to modify, e.g. /versioned-react-native/someversion/
637 *  @param versionedReactPodName name of the new pod (and podfile)
638 */
639async function generateReactPodspecAsync(versionedReactNativePath, versionName) {
640  const versionedReactPodName = getVersionedReactPodName(versionName);
641  const versionedYogaPodName = getVersionedYogaPodName(versionName);
642  const versionedJSIPodName = getVersionedJSIPodName(versionName);
643  const specFilename = path.join(versionedReactNativePath, 'React.podspec');
644
645  // rename spec to newPodName
646  const sedPattern = `s/\\(s\\.name[[:space:]]*=[[:space:]]\\)"React"/\\1"${versionedReactPodName}"/g`;
647  await spawnAsync('sed', ['-i', '--', sedPattern, specFilename]);
648
649  // rename header_dir
650  await spawnAsync('sed', [
651    '-i',
652    '--',
653    `s/^\\(.*header_dir.*\\)React\\(.*\\)$/\\1${versionedReactPodName}\\2/`,
654    specFilename,
655  ]);
656  await spawnAsync('sed', [
657    '-i',
658    '--',
659    `s/^\\(.*header_dir.*\\)jsireact\\(.*\\)$/\\1${versionedJSIPodName}\\2/`,
660    specFilename,
661  ]);
662
663  // point source at .
664  const newPodSource = `{ :path => "." }`;
665  await spawnAsync('sed', [
666    '-i',
667    '--',
668    `s/\\(s\\.source[[:space:]]*=[[:space:]]\\).*/\\1${newPodSource}/g`,
669    specFilename,
670  ]);
671
672  // further processing that sed can't do very well
673  await _transformFileContentsAsync(specFilename, (fileString) => {
674    // replace React/* dependency with ${versionedReactPodName}/*
675    fileString = fileString.replace(
676      /(\.dependency\s+)"React([^"]+)"/g,
677      `$1"${versionedReactPodName}$2"`
678    );
679
680    fileString = fileString.replace('/RCTTV', `/${versionName}RCTTV`);
681
682    // namespace cpp libraries
683    const cppLibraries = getCppLibrariesToVersion();
684    cppLibraries.forEach(({ libName }) => {
685      fileString = fileString.replace(
686        new RegExp(`([^A-Za-z0-9_])${libName}([^A-Za-z0-9_])`, 'g'),
687        `$1${getVersionedLibraryName(libName, versionName)}$2`
688      );
689    });
690
691    // fix wrong Yoga pod name
692    fileString = fileString.replace(
693      /^(.*dependency.*["']).*yoga.*?(["'].*)$/m,
694      `$1${versionedYogaPodName}$2`
695    );
696
697    return fileString;
698  });
699
700  // move podspec to ${versionedReactPodName}.podspec
701  await fs.move(
702    specFilename,
703    path.join(versionedReactNativePath, `${versionedReactPodName}.podspec`)
704  );
705}
706
707function getCFlagsToPrefixGlobals(prefix, globals) {
708  return globals.map((val) => `-D${val}=${prefix}${val}`);
709}
710
711/**
712 * Generates `dependencies.rb` and `postinstalls.rb` files for versioned code.
713 * @param versionNumber Semver-compliant version of the SDK/ABI
714 * @param versionName Version prefix used for versioned files, e.g. ABI99_0_0
715 * @param versionedPodNames mapping from pod names to versioned pod names, e.g. React -> ReactABI99_0_0
716 * @param versionedReactPodPath path of the new react pod
717 */
718async function generatePodfileSubscriptsAsync(
719  versionNumber: string,
720  versionName: string,
721  versionedPodNames: Record<string, string>,
722  versionedReactPodPath: string
723) {
724  if (!versionedPodNames.React) {
725    throw new Error(
726      'Tried to add generate pod dependencies, but missing a name for the versioned library.'
727    );
728  }
729
730  const relativeReactNativePath = path.relative(IOS_DIR, getVersionedReactNativePath(versionName));
731  const relativeExpoKitPath = path.relative(IOS_DIR, getVersionedExpoKitPath(versionName));
732  const relativeExpoPath = path.relative(IOS_DIR, getVersionedExpoPath(versionName));
733
734  const versionableUnimodulesPods = Object.entries(
735    await getVersionedUnimodulePodsAsync(versionName)
736  )
737    .map(([originalUnimodulePodName, versionedUnimodulePodName]) => {
738      return `pod '${versionedUnimodulePodName}',
739  :path => './${relativeExpoPath}/${originalUnimodulePodName}',
740  :project_name => '${versionName}'`;
741    })
742    .join('\n');
743
744  // Add a dependency on newPodName
745  let dep = `# @generated by expotools
746
747require './${relativeReactNativePath}/react_native_pods.rb'
748
749use_react_native_${versionName}! path: './${relativeReactNativePath}'
750
751pod '${getVersionedExpoKitPodName(versionName)}',
752  :path => './${relativeExpoKitPath}',
753  :project_name => '${versionName}',
754  :subspecs => ['Expo', 'ExpoOptional']
755
756use_pods! 'vendored/sdk${semver.major(versionNumber)}/*/*.podspec.json', '${versionName}'
757
758${versionableUnimodulesPods}
759`;
760  await fs.writeFile(path.join(versionedReactPodPath, 'dependencies.rb'), dep);
761
762  // Add postinstall.
763  // In particular, resolve conflicting globals from React by redefining them.
764  let globals = {
765    React: [
766      // RCTNavigator
767      'kNeverRequested',
768      'kNeverProgressed',
769      // react-native-maps
770      'kSMCalloutViewRepositionDelayForUIScrollView',
771      'regionAsJSON',
772      'unionRect',
773      // jschelpers
774      'JSNoBytecodeFileFormatVersion',
775      'JSSamplingProfilerEnabled',
776      // RCTInspectorPackagerConnection
777      'RECONNECT_DELAY_MS',
778      // RCTSpringAnimation
779      'MAX_DELTA_TIME',
780    ],
781    yoga: [
782      'gCurrentGenerationCount',
783      'gPrintSkips',
784      'gPrintChanges',
785      'layoutNodeInternal',
786      'gDepth',
787      'gPrintTree',
788      'isUndefined',
789      'gNodeInstanceCount',
790    ],
791  };
792  let configValues = getCFlagsToPrefixGlobals(
793    versionedPodNames.React,
794    globals.React.concat(globals.yoga)
795  );
796  const indent = '  '.repeat(3);
797  const config = `# @generated by expotools
798
799if pod_name.start_with?('${versionedPodNames.React}') || pod_name == '${versionedPodNames.ExpoKit}'
800  target_installation_result.native_target.build_configurations.each do |config|
801    config.build_settings['OTHER_CFLAGS'] = %w[
802      ${configValues.join(`\n${indent}`)}
803    ]
804    config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ['$(inherited)']
805    config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << '${versionName}RCT_DEV=1'
806    config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << '${versionName}RCT_ENABLE_INSPECTOR=0'
807    config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << '${versionName}ENABLE_PACKAGER_CONNECTION=0'
808    # Enable Google Maps support
809    config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << '${versionName}HAVE_GOOGLE_MAPS=1'
810    config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << '${versionName}HAVE_GOOGLE_MAPS_UTILS=1'
811  end
812end
813`;
814  await fs.writeFile(path.join(versionedReactPodPath, 'postinstalls.rb'), config);
815}
816
817/**
818 * @param transformConfig function that takes a config dict and returns a new config dict.
819 */
820async function modifyVersionConfigAsync(configPath, transformConfig) {
821  let jsConfigFilename = `${configPath}/sdkVersions.json`;
822  await _transformFileContentsAsync(jsConfigFilename, (jsConfigContents) => {
823    let jsConfig;
824
825    // read the existing json config and add the new version to the sdkVersions array
826    try {
827      jsConfig = JSON.parse(jsConfigContents);
828    } catch (e) {
829      console.log('Error parsing existing sdkVersions.json file, writing a new one...', e);
830      console.log('The erroneous file contents was:', jsConfigContents);
831      jsConfig = {
832        sdkVersions: [],
833      };
834    }
835    // apply changes
836    jsConfig = transformConfig(jsConfig);
837    return JSON.stringify(jsConfig);
838  });
839
840  // convert json config to plist for iOS
841  await spawnAsync('plutil', [
842    '-convert',
843    'xml1',
844    jsConfigFilename,
845    '-o',
846    path.join(configPath, 'EXSDKVersions.plist'),
847  ]);
848}
849
850function validateAddVersionDirectories(rootPath, newVersionPath) {
851  // Make sure the paths we want to read are available
852  let relativePathsToCheck = [
853    RELATIVE_RN_PATH,
854    'ios/versioned-react-native',
855    'ios/Exponent',
856    'ios/Exponent/Versioned',
857  ];
858  let isValid = true;
859  relativePathsToCheck.forEach((path) => {
860    try {
861      fs.accessSync(`${rootPath}/${path}`, fs.constants.F_OK);
862    } catch (e) {
863      console.log(`${rootPath}/${path} does not exist or is otherwise inaccessible`);
864      isValid = false;
865    }
866  });
867  // Also, make sure the version we're about to write doesn't already exist
868  try {
869    // we want this to fail
870    fs.accessSync(newVersionPath, fs.constants.F_OK);
871    console.log(`${newVersionPath} already exists, will not overwrite`);
872    isValid = false;
873  } catch (e) {}
874
875  return isValid;
876}
877
878function validateRemoveVersionDirectories(rootPath, newVersionPath) {
879  let pathsToCheck = [
880    `${rootPath}/ios/versioned-react-native`,
881    `${rootPath}/ios/Exponent`,
882    newVersionPath,
883  ];
884  let isValid = true;
885  pathsToCheck.forEach((path) => {
886    try {
887      fs.accessSync(path, fs.constants.F_OK);
888    } catch (e) {
889      console.log(`${path} does not exist or is otherwise inaccessible`);
890      isValid = false;
891    }
892  });
893  return isValid;
894}
895
896async function getConfigsFromArguments(versionNumber) {
897  let versionComponents = versionNumber.split('.');
898  versionComponents = versionComponents.map((number) => parseInt(number, 10));
899  let versionName = 'ABI' + versionNumber.replace(/\./g, '_');
900  let rootPathComponents = EXPO_DIR.split('/');
901  let versionPathComponents = path.join('ios', 'versioned-react-native', versionName).split('/');
902  let newVersionPath = rootPathComponents.concat(versionPathComponents).join('/');
903
904  let versionedPodNames = {
905    React: getVersionedReactPodName(versionName),
906    yoga: getVersionedYogaPodName(versionName),
907    ExpoKit: getVersionedExpoKitPodName(versionName),
908    jsireact: getVersionedJSIPodName(versionName),
909  };
910
911  return {
912    versionName,
913    newVersionPath,
914    versionedPodNames,
915    versionComponents,
916  };
917}
918
919async function getVersionedUnimodulePodsAsync(
920  versionName: string
921): Promise<{ [key: string]: string }> {
922  const versionedUnimodulePods = {};
923  const packages = await getListOfPackagesAsync();
924  const excludedPodNames = getExcludedPodNames();
925
926  packages.forEach((pkg) => {
927    const podName = pkg.podspecName;
928    if (podName && pkg.isVersionableOnPlatform('ios') && !excludedPodNames.includes(podName)) {
929      versionedUnimodulePods[podName] = `${versionName}${podName}`;
930    }
931  });
932
933  return versionedUnimodulePods;
934}
935
936function getVersionedReactPodName(versionName: string): string {
937  return getVersionedLibraryName('React', versionName);
938}
939
940function getVersionedYogaPodName(versionName: string): string {
941  return getVersionedLibraryName('Yoga', versionName);
942}
943
944function getVersionedJSIPodName(versionName: string): string {
945  return getVersionedLibraryName('jsiReact', versionName);
946}
947
948function getVersionedExpoKitPodName(versionName: string): string {
949  return getVersionedLibraryName('ExpoKit', versionName);
950}
951
952function getVersionedLibraryName(libraryName: string, versionName: string): string {
953  return `${versionName}${libraryName}`;
954}
955
956function getVersionedReactNativePath(versionName: string): string {
957  return path.join(VERSIONED_RN_IOS_DIR, versionName, 'ReactNative');
958}
959
960function getVersionedExpoPath(versionName: string): string {
961  return path.join(VERSIONED_RN_IOS_DIR, versionName, 'Expo');
962}
963
964function getVersionedExpoKitPath(versionName: string): string {
965  return path.join(getVersionedExpoPath(versionName), 'ExpoKit');
966}
967
968function getCppLibrariesToVersion() {
969  return [
970    {
971      libName: 'cxxreact',
972    },
973    {
974      libName: 'jsi',
975    },
976    {
977      libName: 'jsiexecutor',
978      customHeaderDir: 'jsireact',
979    },
980    {
981      libName: 'jsinspector',
982    },
983    {
984      libName: 'yoga',
985    },
986    {
987      libName: 'fabric',
988    },
989    {
990      libName: 'turbomodule',
991      customHeaderDir: 'ReactCommon',
992    },
993    {
994      libName: 'callinvoker',
995      customHeaderDir: 'ReactCommon',
996    },
997  ];
998}
999
1000function getExcludedPodNames() {
1001  // we don't want Payments in Expo Client versions for now
1002  return ['EXPaymentsStripe'];
1003}
1004
1005export async function addVersionAsync(versionNumber: string) {
1006  let { versionName, newVersionPath, versionedPodNames } = await getConfigsFromArguments(
1007    versionNumber
1008  );
1009
1010  // Validate the directories we need before doing anything
1011  console.log(`Validating root directory ${chalk.magenta(EXPO_DIR)} ...`);
1012  let isFilesystemReady = validateAddVersionDirectories(EXPO_DIR, newVersionPath);
1013  if (!isFilesystemReady) {
1014    throw new Error('Aborting: At least one directory we need is not available');
1015  }
1016
1017  if (!versionedPodNames.React) {
1018    throw new Error('Missing name for versioned pod dependency.');
1019  }
1020
1021  // Create ABIXX_0_0 directory.
1022  console.log(
1023    `Creating new ABI version ${chalk.cyan(versionNumber)} at ${chalk.magenta(
1024      path.relative(EXPO_DIR, newVersionPath)
1025    )}`
1026  );
1027  await fs.mkdirs(newVersionPath);
1028
1029  // Generate new Podspec from the existing React.podspec
1030  console.log('Generating versioned ReactNative directory...');
1031  await generateVersionedReactNativeAsync(versionName);
1032
1033  console.log(
1034    `Generating ${chalk.magenta(
1035      path.relative(EXPO_DIR, getVersionedExpoPath(versionName))
1036    )} directory...`
1037  );
1038  await generateVersionedExpoAsync(versionName, versionNumber);
1039
1040  // Namespace the new React clone
1041  console.log('Namespacing/transforming files...');
1042  await transformReactNativeAsync(newVersionPath, versionName, versionedPodNames);
1043
1044  // Generate Ruby scripts with versioned dependencies and postinstall actions that will be evaluated in the Expo client's Podfile.
1045  console.log('Adding dependency to root Podfile...');
1046  await generatePodfileSubscriptsAsync(
1047    versionNumber,
1048    versionName,
1049    versionedPodNames,
1050    newVersionPath
1051  );
1052
1053  // Add the new version to the iOS config list of available versions
1054  console.log('Registering new version under sdkVersions config...');
1055  const addVersionToConfig = (config, versionNumber) => {
1056    config.sdkVersions.push(versionNumber);
1057    return config;
1058  };
1059  await modifyVersionConfigAsync(path.join(IOS_DIR, 'Exponent', 'Supporting'), (config) =>
1060    addVersionToConfig(config, versionNumber)
1061  );
1062  await modifyVersionConfigAsync(
1063    path.join(EXPO_DIR, 'exponent-view-template', 'ios', 'exponent-view-template', 'Supporting'),
1064    (config) => addVersionToConfig(config, versionNumber)
1065  );
1066
1067  // Modifying kernel files
1068  console.log(`Modifying ${chalk.bold('kernel files')} to incorporate new SDK version...`);
1069  await modifyKernelFilesAsync(versionName);
1070
1071  console.log('Removing any `filename--` files from the new pod ...');
1072
1073  try {
1074    const minusMinusFiles = await glob(path.join(newVersionPath, '**', '*--'));
1075    for (const minusMinusFile of minusMinusFiles) {
1076      await fs.remove(minusMinusFile);
1077    }
1078  } catch (error) {
1079    console.warn(
1080      "The script wasn't able to remove any possible `filename--` files created by sed. Please ensure there are no such files manually."
1081    );
1082  }
1083
1084  console.log('Finished creating new version.');
1085}
1086
1087async function askToReinstallPodsAsync(): Promise<boolean> {
1088  if (process.env.CI) {
1089    // If we're on the CI, let's regenerate Pods by default.
1090    return true;
1091  }
1092  const { result } = await inquirer.prompt<{ result: boolean }>([
1093    {
1094      type: 'confirm',
1095      name: 'result',
1096      message: 'Do you want to reinstall pods?',
1097      default: true,
1098    },
1099  ]);
1100  return result;
1101}
1102
1103export async function reinstallPodsAsync(force?: boolean, preventReinstall?: boolean) {
1104  if (
1105    preventReinstall !== true &&
1106    (force || (force !== false && (await askToReinstallPodsAsync())))
1107  ) {
1108    await spawnAsync('pod', ['install'], { stdio: 'inherit', cwd: IOS_DIR });
1109    console.log(
1110      'Regenerated Podfile and installed new pods. You can now try to build the project in Xcode.'
1111    );
1112  } else {
1113    console.log(
1114      'Skipped pods regeneration. You might want to run `et ios-generate-dynamic-macros`, then `pod install` in `ios` to configure Xcode project.'
1115    );
1116  }
1117}
1118
1119export async function removeVersionAsync(versionNumber: string) {
1120  let { newVersionPath, versionedPodNames, versionName } = await getConfigsFromArguments(
1121    versionNumber
1122  );
1123  console.log(
1124    `Removing SDK version ${chalk.cyan(versionNumber)} from ${chalk.magenta(
1125      path.relative(EXPO_DIR, newVersionPath)
1126    )} with Pod name ${chalk.green(versionedPodNames.React)}`
1127  );
1128
1129  // Validate the directories we need before doing anything
1130  console.log(`Validating root directory ${chalk.magenta(EXPO_DIR)} ...`);
1131  let isFilesystemReady = validateRemoveVersionDirectories(EXPO_DIR, newVersionPath);
1132  if (!isFilesystemReady) {
1133    console.log('Aborting: At least one directory we expect is not available');
1134    return;
1135  }
1136
1137  // remove directory
1138  console.log(
1139    `Removing versioned files under ${chalk.magenta(path.relative(EXPO_DIR, newVersionPath))}...`
1140  );
1141  await fs.remove(newVersionPath);
1142
1143  // remove dep from main podfile
1144  console.log(`Removing ${chalk.green(versionedPodNames.React)} dependency from root Podfile...`);
1145
1146  // remove from sdkVersions.json
1147  console.log('Unregistering version from sdkVersions config...');
1148  const removeVersionFromConfig = (config, versionNumber) => {
1149    let index = config.sdkVersions.indexOf(versionNumber);
1150    if (index > -1) {
1151      // modify in place
1152      config.sdkVersions.splice(index, 1);
1153    }
1154    return config;
1155  };
1156  await modifyVersionConfigAsync(path.join(IOS_DIR, 'Exponent', 'Supporting'), (config) =>
1157    removeVersionFromConfig(config, versionNumber)
1158  );
1159  await modifyVersionConfigAsync(
1160    path.join(EXPO_DIR, 'exponent-view-template', 'ios', 'exponent-view-template', 'Supporting'),
1161    (config) => removeVersionFromConfig(config, versionNumber)
1162  );
1163
1164  // modify kernel files
1165  console.log('Rollbacking SDK modifications from kernel files...');
1166  await modifyKernelFilesAsync(versionName, true);
1167
1168  await reinstallPodsAsync();
1169}
1170
1171/**
1172 *  @return an array of objects representing react native transform rules.
1173 *    objects must contain 'pattern' and may optionally contain 'paths' to limit
1174 *    the transform to certain file paths.
1175 *
1176 *  the rules are applied in order!
1177 */
1178function _getReactNativeTransformRules(versionPrefix, reactPodName) {
1179  const cppLibraries = getCppLibrariesToVersion().map((lib) => lib.customHeaderDir || lib.libName);
1180  const versionedLibs = [...cppLibraries, 'React', 'FBLazyVector', 'FBReactNativeSpec'];
1181
1182  return [
1183    {
1184      // Change Obj-C symbols prefix
1185      pattern: `s/RCT/${versionPrefix}RCT/g`,
1186    },
1187    {
1188      pattern: `s/^EX/${versionPrefix}EX/g`,
1189      // paths: 'EX',
1190    },
1191    {
1192      pattern: `s/^UM/${versionPrefix}UM/g`,
1193      // paths: 'EX',
1194    },
1195    {
1196      pattern: `s/\\([^\\<\\/"]\\)YG/\\1${versionPrefix}YG/g`,
1197    },
1198    {
1199      pattern: `s/\\([\\<,]\\)YG/\\1${versionPrefix}YG/g`,
1200    },
1201    {
1202      pattern: `s/^YG/${versionPrefix}YG/g`,
1203    },
1204    {
1205      paths: 'Components',
1206      pattern: `s/\\([^+]\\)AIR/\\1${versionPrefix}AIR/g`,
1207    },
1208    {
1209      pattern: `s/\\([^A-Za-z0-9_]\\)EX/\\1${versionPrefix}EX/g`,
1210    },
1211    {
1212      pattern: `s/\\([^A-Za-z0-9_]\\)UM/\\1${versionPrefix}UM/g`,
1213    },
1214    {
1215      pattern: `s/\\([^A-Za-z0-9_+]\\)ART/\\1${versionPrefix}ART/g`,
1216    },
1217    {
1218      pattern: `s/ENABLE_PACKAGER_CONNECTION/${versionPrefix}ENABLE_PACKAGER_CONNECTION/g`,
1219    },
1220    {
1221      paths: 'Components',
1222      pattern: `s/\\([^A-Za-z0-9_+]\\)SM/\\1${versionPrefix}SM/g`,
1223    },
1224    {
1225      paths: 'Core/Api',
1226      pattern: `s/\\([^A-Za-z0-9_+]\\)RN/\\1${versionPrefix}RN/g`,
1227    },
1228    {
1229      paths: 'Core/Api',
1230      pattern: `s/^RN/${versionPrefix}RN/g`,
1231    },
1232    {
1233      paths: 'Core/Api',
1234      pattern: `s/HAVE_GOOGLE_MAPS/${versionPrefix}HAVE_GOOGLE_MAPS/g`,
1235    },
1236    {
1237      paths: 'Core/Api',
1238      pattern: `s/#import "Branch/#import "${versionPrefix}Branch/g`,
1239    },
1240    {
1241      paths: 'Core/Api',
1242      pattern: `s/#import "NSObject+RNBranch/#import "${versionPrefix}NSObject+RNBranch/g`,
1243    },
1244    {
1245      // React will be prefixed in a moment
1246      pattern: `s/#import <${versionPrefix}RCTAnimation/#import <React/g`,
1247    },
1248    {
1249      paths: 'Core/Api/Reanimated',
1250      pattern: `s/\\([^A-Za-z0-9_+]\\)REA/\\1${versionPrefix}REA/g`,
1251    },
1252    {
1253      pattern: `s/^REA/${versionPrefix}REA/g`,
1254      paths: 'Core/Api/Reanimated',
1255    },
1256    {
1257      // Prefixes all direct references to objects under `reanimated` namespace.
1258      // It must be applied before versioning `namespace reanimated` so
1259      // `using namespace reanimated::` don't get versioned twice.
1260      pattern: `s/reanimated::/${versionPrefix}reanimated::/g`,
1261    },
1262    {
1263      // Prefixes reanimated namespace.
1264      pattern: `s/namespace reanimated/namespace ${versionPrefix}reanimated/g`,
1265    },
1266    {
1267      // Fix imports in C++ libs in ReactCommon.
1268      // Extended syntax (-E) is required to use (a|b).
1269      flags: '-Ei',
1270      pattern: `s/([<"])(${versionedLibs.join(
1271        '|'
1272      )})\\//\\1${versionPrefix}\\2\\/${versionPrefix}/g`,
1273    },
1274    {
1275      // Change React -> new pod name
1276      // e.g. threads and queues namespaced to com.facebook.react,
1277      // file paths beginning with the lib name,
1278      // the cpp facebook::react namespace,
1279      // iOS categories ending in +React
1280      flags: '-Ei',
1281      pattern: `s/[Rr]eact/${reactPodName}/g`,
1282    },
1283    {
1284      // Imports from cxxreact and jsireact got prefixed twice.
1285      flags: '-Ei',
1286      pattern: `s/([<"])(${versionPrefix})(cxx|jsi)${versionPrefix}React/\\1\\2\\3react/g`,
1287    },
1288    {
1289      // Fix imports from files like `UIView+React.*`.
1290      flags: '-Ei',
1291      pattern: `s/\\+${versionPrefix}React/\\+React/g`,
1292    },
1293    {
1294      // Prefixes all direct references to objects under `facebook` namespace.
1295      // It must be applied before versioning `namespace facebook` so
1296      // `using namespace facebook::` don't get versioned twice.
1297      pattern: `s/facebook::/${versionPrefix}facebook::/g`,
1298    },
1299    {
1300      // Prefixes facebook namespace.
1301      pattern: `s/namespace facebook/namespace ${versionPrefix}facebook/g`,
1302    },
1303    {
1304      // For UMReactNativeAdapter
1305      // Fix names with 'React' substring occurring twice - only first one should be prefixed
1306      flags: '-Ei',
1307      pattern: `s/${versionPrefix}UM([[:alpha:]]*)${reactPodName}/${versionPrefix}UM\\1React/g`,
1308    },
1309    {
1310      // For EXReactNativeAdapter
1311      pattern: `s/${versionPrefix}EX${reactPodName}/${versionPrefix}EXReact/g`,
1312    },
1313    {
1314      // For EXConstants and EXNotifications so that when their migrators
1315      // try to access legacy storage for UUID migration, they access the proper value.
1316      pattern: `s/${versionPrefix}EXDeviceInstallUUIDKey/EXDeviceInstallUUIDKey/g`,
1317      paths: 'Expo',
1318    },
1319    {
1320      // For EXConstants and EXNotifications so that the installation ID
1321      // stays the same between different SDK versions. (https://github.com/expo/expo/issues/11008#issuecomment-726370187)
1322      pattern: `s/${versionPrefix}EXDeviceInstallationUUIDKey/EXDeviceInstallationUUIDKey/g`,
1323      paths: 'Expo',
1324    },
1325    {
1326      // RCTPlatform exports version of React Native
1327      pattern: `s/${reactPodName}NativeVersion/reactNativeVersion/g`,
1328    },
1329    {
1330      pattern: `s/@"${versionPrefix}RCT"/@"RCT"/g`,
1331    },
1332    {
1333      // Unversion EXGL_CPP imports: `<ABI37_0_0EXGL_CPP/` => `<EXGL_CPP/`
1334      pattern: `s/<${versionPrefix}EXGL_CPP\\//<EXGL_CPP\\//g`,
1335    },
1336    {
1337      // Unprefix everything that got prefixed twice or more times.
1338      flags: '-Ei',
1339      pattern: `s/(${versionPrefix}){2,}/\\1/g`,
1340    },
1341  ];
1342}
1343
1344function _getTransformRulesForDirname(transformRules, dirname) {
1345  return transformRules.filter((rule) => {
1346    return (
1347      // no paths specified, so apply rule to everything
1348      !rule.paths ||
1349      // otherwise, limit this rule to paths specified
1350      dirname.indexOf(rule.paths) !== -1
1351    );
1352  });
1353}
1354
1355// TODO: use the one in XDL
1356function _isDirectory(dir) {
1357  try {
1358    if (fs.statSync(dir).isDirectory()) {
1359      return true;
1360    }
1361
1362    return false;
1363  } catch (e) {
1364    return false;
1365  }
1366}
1367
1368// TODO: use the one in XDL
1369async function _transformFileContentsAsync(
1370  filename: string,
1371  transform: (fileString: string) => Promise<string> | string | null
1372) {
1373  let fileString = await fs.readFile(filename, 'utf8');
1374  let newFileString = await transform(fileString);
1375  if (newFileString !== null) {
1376    await fs.writeFile(filename, newFileString);
1377  }
1378}
1379