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