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 { arrayize, 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// The pattern that matches the file that need to be renamed in `*.podspec.json`.
22const PODSPEC_FILES_TO_RENAME_PATTERN = /^(Expo|EX|UM|EAS|React|RCT|Yoga|hermes-engine)(?!-Folly)/;
23
24/**
25 * Function that versions expo modules.
26 */
27export async function versionExpoModulesAsync(
28  sdkNumber: number,
29  packages: Package[]
30): Promise<void> {
31  const prefix = getVersionPrefix(sdkNumber);
32  const transforms = expoModulesTransforms(prefix);
33  const versionedDirectory = getVersionedDirectory(sdkNumber);
34  const taskQueue = new TaskQueue(Promise as PromisyClass, os.cpus().length);
35
36  // Prepare versioning task (for single package).
37  const versionPackageTask = taskQueue.wrap(async (pkg: Package) => {
38    logger.log(`- ${chalk.green(pkg.podspecName!)}`);
39
40    if (!pkg.podspecPath || !pkg.podspecName) {
41      throw new Error(`Podspec for package ${pkg.packageName} not found`);
42    }
43
44    const sourceDirectory = path.join(pkg.path, path.dirname(pkg.podspecPath));
45    const targetDirectory = path.join(versionedDirectory, pkg.podspecName);
46
47    // Ensure the target directory is empty
48    if (await fs.pathExists(targetDirectory)) {
49      await fs.remove(targetDirectory);
50    }
51
52    // Create a podspec in JSON format so we don't have to keep `package.json`s
53    const podspec = await generateVersionedPodspecAsync(pkg, prefix, targetDirectory);
54
55    // Find files within the package based on source_files in the podspec, except the podspec itself.
56    // Podspecs depend on the corresponding `package.json`,
57    // that we don't want to copy (no need to version JS files, workspace project names duplication).
58    // Instead, we generate the static podspec in JSON format (see `generateVersionedPodspecAsync`).
59    // Be aware that it doesn't include source files for subspecs!
60    const files = await searchFilesAsync(sourceDirectory, podspec.source_files, {
61      ignore: [`${pkg.podspecName}.podspec`],
62    });
63
64    // Copy files to the new directory with applied transforms
65    for (const sourceFile of files) {
66      await copyFileWithTransformsAsync({
67        sourceFile,
68        targetDirectory,
69        sourceDirectory,
70        transforms,
71      });
72    }
73  });
74
75  logger.info('�� Versioning expo modules');
76  console.time(TIMER_LABEL);
77
78  // Enqueue packages to version.
79  await Promise.all(packages.map(versionPackageTask));
80
81  console.timeEnd(TIMER_LABEL);
82}
83
84/**
85 * Generates versioned static podspec in JSON format.
86 */
87async function generateVersionedPodspecAsync(
88  pkg: Package,
89  prefix: string,
90  targetDirectory: string
91): Promise<Podspec> {
92  const podspec = await pkg.getPodspecAsync();
93
94  if (!podspec) {
95    throw new Error(`Podspec for package ${pkg.packageName} not found`);
96  }
97  if (podspec.name) {
98    podspec.name = `${prefix}${podspec.name}`;
99  }
100  if (podspec.header_dir) {
101    podspec.header_dir = `${prefix}${podspec.header_dir}`;
102  }
103  if (podspec.dependencies) {
104    Object.keys(podspec.dependencies)
105      .filter((key) => PODSPEC_DEPS_TO_RENAME_PATTERN.test(key))
106      .forEach((key) => {
107        const newKey = `${prefix}${key}`;
108        podspec.dependencies[newKey] = podspec.dependencies[key];
109        delete podspec.dependencies[key];
110      });
111  }
112  if (podspec.public_header_files) {
113    podspec.public_header_files = transformVersionedFiles(podspec.public_header_files, prefix);
114  }
115  if (podspec.pod_target_xcconfig?.HEADER_SEARCH_PATHS) {
116    // using ' ' to split HEADER_SEARCH_PATHS is not 100% correct but good enough for expo-modules' podspec
117    const headerSearchPaths = transformVersionedFiles(
118      podspec.pod_target_xcconfig.HEADER_SEARCH_PATHS.split(' '),
119      prefix
120    );
121    podspec.pod_target_xcconfig.HEADER_SEARCH_PATHS = headerSearchPaths.join(' ');
122  }
123
124  if (['expo-updates', 'expo-constants'].includes(pkg.packageName)) {
125    // For expo-updates and expo-constants in Expo Go, we don't need app.config and app.manifest in versioned code.
126    delete podspec['script_phases'];
127    delete podspec['resource_bundles'];
128  }
129
130  const targetPath = path.join(targetDirectory, `${prefix}${pkg.podspecName}.podspec.json`);
131
132  // Write a new one
133  await fs.outputJson(targetPath, podspec, {
134    spaces: 2,
135  });
136
137  return podspec;
138}
139
140/**
141 * Transform files into versioned file names.
142 * For versioning `source_files` or `HEADER_SEARCH_PATHS` in podspec
143 */
144function transformVersionedFiles(files: string | string[], prefix: string): string[] {
145  const result = arrayize(files).map((item) => {
146    const dirname = path.dirname(item);
147    const basename = path.basename(item);
148    const versionedBasename = PODSPEC_FILES_TO_RENAME_PATTERN.test(basename)
149      ? `${prefix}${basename}`
150      : basename;
151    return path.join(dirname, versionedBasename);
152  });
153  return result;
154}
155