1import JsonFile from '@expo/json-file'; 2import chalk from 'chalk'; 3import fs from 'fs-extra'; 4import glob from 'glob-promise'; 5import path from 'path'; 6 7import { IOS_DIR } from '../../Constants'; 8import logger from '../../Logger'; 9import { copyFileWithTransformsAsync } from '../../Transforms'; 10import { FileTransforms } from '../../Transforms.types'; 11import { searchFilesAsync } from '../../Utils'; 12import vendoredModulesTransforms from './transforms/vendoredModulesTransforms'; 13 14const IOS_VENDORED_DIR = path.join(IOS_DIR, 'vendored'); 15 16/** 17 * Versions iOS vendored modules. 18 */ 19export async function versionVendoredModulesAsync( 20 sdkNumber: number, 21 filterModules: string[] | null 22): Promise<void> { 23 const prefix = `ABI${sdkNumber}_0_0`; 24 const config = vendoredModulesTransforms(prefix); 25 const baseTransforms = baseTransformsFactory(prefix); 26 const unversionedDir = path.join(IOS_VENDORED_DIR, 'unversioned'); 27 const versionedDir = vendoredDirectoryForSDK(sdkNumber); 28 let vendoredModuleNames = await getVendoredModuleNamesAsync(unversionedDir); 29 if (filterModules) { 30 vendoredModuleNames = vendoredModuleNames.filter((name) => filterModules.includes(name)); 31 } 32 33 for (const name of vendoredModuleNames) { 34 logger.info(' Versioning vendored module %s', chalk.green(name)); 35 36 const moduleConfig = config[name]; 37 const sourceDirectory = path.join(unversionedDir, name); 38 const targetDirectory = path.join(versionedDir, name); 39 const files = await searchFilesAsync(sourceDirectory, '**'); 40 41 await fs.remove(targetDirectory); 42 43 for (const sourceFile of files) { 44 await copyFileWithTransformsAsync({ 45 sourceFile, 46 sourceDirectory, 47 targetDirectory, 48 transforms: { 49 path: [...baseTransforms.path, ...(moduleConfig?.path ?? [])], 50 content: [...baseTransforms.content, ...(moduleConfig?.content ?? [])], 51 }, 52 }); 53 } 54 await postTransformHooks[name]?.(sourceDirectory, targetDirectory); 55 } 56} 57 58/** 59 * Gets the library name of each vendored module in a specific directory. 60 */ 61async function getVendoredModuleNamesAsync(directory: string): Promise<string[]> { 62 const vendoredPodspecPaths = await glob(`**/*.podspec.json`, { 63 cwd: directory, 64 nodir: true, 65 realpath: true, 66 }); 67 68 const podspecPattern = new RegExp(`${directory}/(.*)/.*podspec.json`, 'i'); 69 70 return vendoredPodspecPaths.reduce((result, podspecPath) => { 71 const moduleName = podspecPath.match(podspecPattern); 72 if (moduleName) { 73 result.push(moduleName[1]); 74 } 75 return result; 76 }, [] as string[]); 77} 78 79/** 80 * Removes the directory with vendored modules for given SDK number. 81 */ 82export async function removeVersionedVendoredModulesAsync(sdkNumber: number): Promise<void> { 83 const versionedDir = vendoredDirectoryForSDK(sdkNumber); 84 await fs.remove(versionedDir); 85} 86 87/** 88 * Generates base transforms to apply for all vendored modules. 89 */ 90function baseTransformsFactory(prefix: string): Required<FileTransforms> { 91 return { 92 path: [ 93 { 94 find: /([^/]+\.podspec\.json)$\b/, 95 replaceWith: `${prefix}$1`, 96 }, 97 { 98 find: /\b(RCT|RNC|RNG|RNR|REA|RNS)([^/]*\.)(h|m|mm)/, 99 replaceWith: `${prefix}$1$2$3`, 100 }, 101 ], 102 content: [ 103 { 104 paths: '*.podspec.json', 105 find: /"name": "([\w-]+)"/, 106 replaceWith: `"name": "${prefix}$1"`, 107 }, 108 { 109 // Prefixes `React` with word-boundary and also in `insertReactSubview`, `removeReactSubview`. 110 find: /(\b|insert|remove)(React)/g, 111 replaceWith: `$1${prefix}$2`, 112 }, 113 { 114 find: /\b(RCT|RNC|RNG|RNR|REA|RNS|YG)(\w+)\b/g, 115 replaceWith: `${prefix}$1$2`, 116 }, 117 { 118 find: /facebook::/g, 119 replaceWith: `${prefix}facebook::`, 120 }, 121 { 122 find: /react::/g, 123 replaceWith: `${prefix}React::`, 124 }, 125 { 126 find: /namespace (facebook|react)/g, 127 replaceWith: (_, p1) => { 128 return `namespace ${prefix}${p1 === 'react' ? 'React' : p1}`; 129 }, 130 }, 131 { 132 // Objective-C only, see the comment in the rule below. 133 paths: '*.{h,m,mm}', 134 find: /r(eactTag|eactSubviews|eactSuperview|eactViewController|eactSetFrame|eactAddControllerToClosestParent|eactZIndex|eactLayoutDirection)/gi, 135 replaceWith: `${prefix}R$1`, 136 }, 137 { 138 // Swift translates uppercased letters at the beginning of the method name to lowercased letters, we must comply with it. 139 paths: '*.swift', 140 find: /r(eactTag|eactSubviews|eactSuperview|eactViewController|eactSetFrame|eactAddControllerToClosestParent|eactZIndex)/gi, 141 replaceWith: (_, p1) => `${prefix.toLowerCase()}R${p1}`, 142 }, 143 { 144 // Modules written in Swift are registered using `RCT_EXTERN_MODULE` macro in Objective-C. 145 // These modules are usually unprefixed at this point as they don't include any common prefixes (e.g. RCT, RNC). 146 // We have to remap them to prefixed names. It's necessary for at least Stripe, Lottie and FlashList. 147 paths: '*.m', 148 find: new RegExp(`RCT_EXTERN_MODULE\\((?!${prefix})(\\w+)`, 'g'), 149 replaceWith: `RCT_EXTERN_REMAP_MODULE($1, ${prefix}$1`, 150 }, 151 { 152 find: /<jsi\/(.*)\.h>/, 153 replaceWith: `<${prefix}jsi/${prefix}$1.h>`, 154 }, 155 { 156 find: /(JSCExecutorFactory|HermesExecutorFactory)\.h/g, 157 replaceWith: `${prefix}$1.h`, 158 }, 159 { 160 find: /viewForReactTag/g, 161 replaceWith: `viewFor${prefix}ReactTag`, 162 }, 163 { 164 find: /isReactRootView/g, 165 replaceWith: `is${prefix}ReactRootView`, 166 }, 167 { 168 find: /IsReactRootView/g, 169 replaceWith: `Is${prefix}ReactRootView`, 170 }, 171 { 172 // Prefix only unindented `@objc` (notice `^` and `m` flag in the pattern). Method names shouldn't get prefixed. 173 paths: '*.swift', 174 find: /^@objc\(([^)]+)\)/gm, 175 replaceWith: `@objc(${prefix}$1)`, 176 }, 177 { 178 paths: '*.podspec.json', 179 find: new RegExp(`${prefix}React-${prefix}RCT`, 'g'), 180 replaceWith: `${prefix}React-RCT`, 181 }, 182 { 183 paths: '*.podspec.json', 184 find: new RegExp(`${prefix}React-Core\\/${prefix}RCT`, 'g'), 185 replaceWith: `${prefix}React-Core/RCT`, 186 }, 187 { 188 find: `${prefix}${prefix}`, 189 replaceWith: `${prefix}`, 190 }, 191 ], 192 }; 193} 194 195/** 196 * Provides a hook for vendored module to do some custom tasks after transforming files 197 */ 198type PostTramsformHook = (sourceDirectory: string, targetDirectory: string) => Promise<void>; 199const postTransformHooks: Record<string, PostTramsformHook> = { 200 '@shopify/react-native-skia': async (sourceDirectory: string, targetDirectory: string) => { 201 const podspecPath = (await glob('*.podspec.json', { cwd: targetDirectory, absolute: true }))[0]; 202 const podspec = await JsonFile.readAsync(podspecPath); 203 // remove the shared vendored_frameworks 204 delete podspec?.['ios']?.['vendored_frameworks']; 205 await JsonFile.writeAsync(podspecPath, podspec); 206 }, 207}; 208 209/** 210 * Returns the vendored directory for given SDK number. 211 */ 212function vendoredDirectoryForSDK(sdkNumber: number): string { 213 return path.join(IOS_VENDORED_DIR, `sdk${sdkNumber}`); 214} 215