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