1import chalk from 'chalk';
2import { PromisyClass, TaskQueue } from 'cwait';
3import fs from 'fs-extra';
4import os from 'os';
5import path from 'path';
6
7import { Podspec } from '../../CocoaPods';
8import logger from '../../Logger';
9import { Package } from '../../Packages';
10import { copyFileWithTransformsAsync } from '../../Transforms';
11import { searchFilesAsync } from '../../Utils';
12import { expoModulesTransforms } from './transforms/expoModulesTransforms';
13import { getVersionPrefix, getVersionedDirectory } from './utils';
14
15// Label of the console's timer used during versioning
16const TIMER_LABEL = 'Versioning expo modules finished in';
17
18// The pattern that matches the dependency pods that need to be renamed in `*.podspec.json`.
19const PODSPEC_DEPS_TO_RENAME_PATTERN = /^(Expo|EX|UM|EAS|React|RCT|Yoga)/;
20
21/**
22 * Function that versions expo modules.
23 */
24export async function versionExpoModulesAsync(
25  sdkNumber: number,
26  packages: Package[]
27): Promise<void> {
28  const prefix = getVersionPrefix(sdkNumber);
29  const transforms = expoModulesTransforms(prefix);
30  const versionedDirectory = getVersionedDirectory(sdkNumber);
31  const taskQueue = new TaskQueue(Promise as PromisyClass, os.cpus().length);
32
33  // Prepare versioning task (for single package).
34  const versionPackageTask = taskQueue.wrap(async (pkg: Package) => {
35    logger.log(`- ${chalk.green(pkg.podspecName!)}`);
36
37    if (!pkg.podspecPath || !pkg.podspecName) {
38      throw new Error(`Podspec for package ${pkg.packageName} not found`);
39    }
40
41    const sourceDirectory = path.join(pkg.path, path.dirname(pkg.podspecPath));
42    const targetDirectory = path.join(versionedDirectory, pkg.podspecName);
43
44    // Ensure the target directory is empty
45    if (await fs.pathExists(targetDirectory)) {
46      await fs.remove(targetDirectory);
47    }
48
49    // Create a podspec in JSON format so we don't have to keep `package.json`s
50    const podspec = await generateVersionedPodspecAsync(pkg, prefix, targetDirectory);
51
52    // Find files within the package based on source_files in the podspec, except the podspec itself.
53    // Podspecs depend on the corresponding `package.json`,
54    // that we don't want to copy (no need to version JS files, workspace project names duplication).
55    // Instead, we generate the static podspec in JSON format (see `generateVersionedPodspecAsync`).
56    // Be aware that it doesn't include source files for subspecs!
57    const files = await searchFilesAsync(sourceDirectory, podspec.source_files, {
58      ignore: [`${pkg.podspecName}.podspec`],
59    });
60
61    // Copy files to the new directory with applied transforms
62    for (const sourceFile of files) {
63      await copyFileWithTransformsAsync({
64        sourceFile,
65        targetDirectory,
66        sourceDirectory,
67        transforms,
68      });
69    }
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): Promise<Podspec> {
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((key) => PODSPEC_DEPS_TO_RENAME_PATTERN.test(key))
103      .forEach((key) => {
104        const newKey = `${prefix}${key}`;
105        podspec.dependencies[newKey] = podspec.dependencies[key];
106        delete podspec.dependencies[key];
107      });
108  }
109
110  if (['expo-updates', 'expo-constants'].includes(pkg.packageName)) {
111    // For expo-updates and expo-constants in Expo Go, we don't need app.config and app.manifest in versioned code.
112    delete podspec['script_phases'];
113    delete podspec['resource_bundles'];
114  }
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  return podspec;
124}
125