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|react|hermes)::/g, 119 replaceWith: (_, p1) => { 120 return `${prefix}${p1 === 'react' ? 'React' : p1}::`; 121 }, 122 }, 123 { 124 find: /namespace (facebook|react|hermes)/g, 125 replaceWith: (_, p1) => { 126 return `namespace ${prefix}${p1 === 'react' ? 'React' : p1}`; 127 }, 128 }, 129 { 130 // namespace ABI48_0_0React = ABI48_0_0facebook::react -> namespace ABI48_0_0React = ABI48_0_0facebook::ABI48_0_0React 131 // using namespace ABI48_0_0facebook::react -> using namespace ABI48_0_0facebook::ABI48_0_0React 132 find: /namespace ([\w\s=]+facebook)::react/g, 133 replaceWith: `namespace $1::${prefix}React`, 134 }, 135 { 136 // Objective-C only, see the comment in the rule below. 137 paths: '*.{h,m,mm}', 138 find: /r(eactTag|eactSubviews|eactSuperview|eactViewController|eactSetFrame|eactAddControllerToClosestParent|eactZIndex|eactLayoutDirection)/gi, 139 replaceWith: `${prefix}R$1`, 140 }, 141 { 142 // Swift translates uppercased letters at the beginning of the method name to lowercased letters, we must comply with it. 143 paths: '*.swift', 144 find: /r(eactTag|eactSubviews|eactSuperview|eactViewController|eactSetFrame|eactAddControllerToClosestParent|eactZIndex)/gi, 145 replaceWith: (_, p1) => `${prefix.toLowerCase()}R${p1}`, 146 }, 147 { 148 // Modules written in Swift are registered using `RCT_EXTERN_MODULE` macro in Objective-C. 149 // These modules are usually unprefixed at this point as they don't include any common prefixes (e.g. RCT, RNC). 150 // We have to remap them to prefixed names. It's necessary for at least Stripe, Lottie and FlashList. 151 paths: '*.m', 152 find: new RegExp(`RCT_EXTERN_MODULE\\((?!${prefix})(\\w+)`, 'g'), 153 replaceWith: `RCT_EXTERN_REMAP_MODULE($1, ${prefix}$1`, 154 }, 155 { 156 find: /<jsi\/(.*)\.h>/, 157 replaceWith: `<${prefix}jsi/${prefix}$1.h>`, 158 }, 159 { 160 find: /(JSCExecutorFactory|HermesExecutorFactory)\.h/g, 161 replaceWith: `${prefix}$1.h`, 162 }, 163 { 164 find: /viewForReactTag/g, 165 replaceWith: `viewFor${prefix}ReactTag`, 166 }, 167 { 168 find: /isReactRootView/g, 169 replaceWith: `is${prefix}ReactRootView`, 170 }, 171 { 172 find: /IsReactRootView/g, 173 replaceWith: `Is${prefix}ReactRootView`, 174 }, 175 { 176 find: `UIView+${prefix}React.h`, 177 replaceWith: `${prefix}UIView+React.h`, 178 }, 179 { 180 // Prefix only unindented `@objc` (notice `^` and `m` flag in the pattern). Method names shouldn't get prefixed. 181 paths: '*.swift', 182 find: /^@objc\(([^)]+)\)/gm, 183 replaceWith: `@objc(${prefix}$1)`, 184 }, 185 { 186 paths: '*.podspec.json', 187 find: new RegExp(`${prefix}React-${prefix}RCT`, 'g'), 188 replaceWith: `${prefix}React-RCT`, 189 }, 190 { 191 paths: '*.podspec.json', 192 find: /\b(hermes-engine)\b/g, 193 replaceWith: `${prefix}$1`, 194 }, 195 { 196 paths: '*.podspec.json', 197 find: new RegExp(`${prefix}React-Core\\/${prefix}RCT`, 'g'), 198 replaceWith: `${prefix}React-Core/RCT`, 199 }, 200 { 201 find: `${prefix}${prefix}`, 202 replaceWith: `${prefix}`, 203 }, 204 ], 205 }; 206} 207 208/** 209 * Provides a hook for vendored module to do some custom tasks after transforming files 210 */ 211type PostTramsformHook = (sourceDirectory: string, targetDirectory: string) => Promise<void>; 212const postTransformHooks: Record<string, PostTramsformHook> = { 213 '@shopify/react-native-skia': async (sourceDirectory: string, targetDirectory: string) => { 214 const podspecPath = (await glob('*.podspec.json', { cwd: targetDirectory, absolute: true }))[0]; 215 const podspec = await JsonFile.readAsync(podspecPath); 216 // remove the shared vendored_frameworks 217 delete podspec?.['ios']?.['vendored_frameworks']; 218 await JsonFile.writeAsync(podspecPath, podspec); 219 }, 220}; 221 222/** 223 * Returns the vendored directory for given SDK number. 224 */ 225function vendoredDirectoryForSDK(sdkNumber: number): string { 226 return path.join(IOS_VENDORED_DIR, `sdk${sdkNumber}`); 227} 228