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