1import chalk from 'chalk';
2import { PromisyClass, TaskQueue } from 'cwait';
3import fs from 'fs-extra';
4import os from 'os';
5import path from 'path';
6
7import logger from '../../Logger';
8import { Package } from '../../Packages';
9import { copyFileWithTransformsAsync } from '../../Transforms';
10import { searchFilesAsync } from '../../Utils';
11import { expoModulesTransforms } from './transforms/expoModulesTransforms';
12import { getVersionPrefix, getVersionedDirectory } from './utils';
13
14// Label of the console's timer used during versioning
15const TIMER_LABEL = 'Versioning expo modules finished in';
16
17// List of pod names to exclude from versioning
18const EXCLUDED_POD_NAMES = ['EXPaymentsStripe'];
19
20// The pattern that matches the dependency pods that need to be renamed in `*.podspec.json`.
21const PODSPEC_DEPS_TO_RENAME_PATTERN = /^(Expo|EX(?!GL_CPP)|UM|React|RCT|Yoga)/;
22
23/**
24 * Function that versions expo modules.
25 */
26export async function versionExpoModulesAsync(
27  sdkNumber: number,
28  packages: Package[]
29): Promise<void> {
30  const prefix = getVersionPrefix(sdkNumber);
31  const transforms = expoModulesTransforms(prefix);
32  const versionedDirectory = getVersionedDirectory(sdkNumber);
33  const taskQueue = new TaskQueue(Promise as PromisyClass, os.cpus().length);
34
35  // Prepare versioning task (for single package).
36  const versionPackageTask = taskQueue.wrap(async (pkg) => {
37    if (EXCLUDED_POD_NAMES.includes(pkg.podspecName)) {
38      return;
39    }
40
41    logger.log(`- ${chalk.green(pkg.podspecName!)}`);
42    const sourceDirectory = path.join(pkg.path, pkg.iosSubdirectory);
43    const targetDirectory = path.join(versionedDirectory, pkg.podspecName!);
44
45    // Find all iOS files within the package, except the podspec.
46    // Podspecs depend on the corresponding `package.json`,
47    // that we don't want to copy (no need to version JS files, workspace project names duplication).
48    // Instead, we generate the static podspec in JSON format (see `generateVersionedPodspecAsync`).
49    const files = await searchFilesAsync(sourceDirectory, '**', {
50      ignore: [`${pkg.podspecName}.podspec`],
51    });
52
53    // Ensure the target directory is empty
54    if (await fs.pathExists(targetDirectory)) {
55      await fs.remove(targetDirectory);
56    }
57
58    // Copy files to the new directory with applied transforms
59    for (const sourceFile of files) {
60      await copyFileWithTransformsAsync({
61        sourceFile,
62        targetDirectory,
63        sourceDirectory,
64        transforms,
65      });
66    }
67
68    // Create a podspec in JSON format so we don't have to keep `package.json`s
69    await generateVersionedPodspecAsync(pkg, prefix, targetDirectory);
70  });
71
72  logger.info('�� Versioning expo modules');
73  console.time(TIMER_LABEL);
74
75  // Enqueue packages to version.
76  await Promise.all(packages.map(versionPackageTask));
77
78  console.timeEnd(TIMER_LABEL);
79}
80
81/**
82 * Generates versioned static podspec in JSON format.
83 */
84async function generateVersionedPodspecAsync(
85  pkg: Package,
86  prefix: string,
87  targetDirectory: string
88) {
89  const podspec = await pkg.getPodspecAsync();
90
91  if (!podspec) {
92    throw new Error(`Podspec for package ${pkg.packageName} not found`);
93  }
94  if (podspec.name) {
95    podspec.name = `${prefix}${podspec.name}`;
96  }
97  if (podspec.header_dir) {
98    podspec.header_dir = `${prefix}${podspec.header_dir}`;
99  }
100  if (podspec.dependencies) {
101    Object.keys(podspec.dependencies)
102      .filter(
103        (key) => !EXCLUDED_POD_NAMES.includes(key) && PODSPEC_DEPS_TO_RENAME_PATTERN.test(key)
104      )
105      .forEach((key) => {
106        const newKey = `${prefix}${key}`;
107        podspec.dependencies[newKey] = podspec.dependencies[key];
108        delete podspec.dependencies[key];
109      });
110  }
111
112  // Remove script phases.
113  // expo-constants has one, but the `scripts` folder isn't being copied and there is no need to have it in versioned code.
114  delete podspec['script_phases'];
115
116  const targetPath = path.join(targetDirectory, `${prefix}${pkg.podspecName}.podspec.json`);
117
118  // Write a new one
119  await fs.outputJson(targetPath, podspec, {
120    spaces: 2,
121  });
122}
123