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