1import { existsSync, readFileSync } from 'fs'; 2import { sync as globSync } from 'glob'; 3import * as path from 'path'; 4 5import * as Entitlements from './Entitlements'; 6import { UnexpectedError } from '../utils/errors'; 7import { addWarningIOS } from '../utils/warnings'; 8 9const ignoredPaths = ['**/@(Carthage|Pods|vendor|node_modules)/**']; 10 11interface ProjectFile<L extends string = string> { 12 path: string; 13 language: L; 14 contents: string; 15} 16 17type AppleLanguage = 'objc' | 'objcpp' | 'swift'; 18 19export type AppDelegateProjectFile = ProjectFile<AppleLanguage>; 20 21export function getAppDelegateHeaderFilePath(projectRoot: string): string { 22 const [using, ...extra] = globSync('ios/*/AppDelegate.h', { 23 absolute: true, 24 cwd: projectRoot, 25 ignore: ignoredPaths, 26 }); 27 28 if (!using) { 29 throw new UnexpectedError( 30 `Could not locate a valid AppDelegate header at root: "${projectRoot}"` 31 ); 32 } 33 34 if (extra.length) { 35 warnMultipleFiles({ 36 tag: 'app-delegate-header', 37 fileName: 'AppDelegate', 38 projectRoot, 39 using, 40 extra, 41 }); 42 } 43 44 return using; 45} 46 47export function getAppDelegateFilePath(projectRoot: string): string { 48 const [using, ...extra] = globSync('ios/*/AppDelegate.@(m|mm|swift)', { 49 absolute: true, 50 cwd: projectRoot, 51 ignore: ignoredPaths, 52 }); 53 54 if (!using) { 55 throw new UnexpectedError(`Could not locate a valid AppDelegate at root: "${projectRoot}"`); 56 } 57 58 if (extra.length) { 59 warnMultipleFiles({ 60 tag: 'app-delegate', 61 fileName: 'AppDelegate', 62 projectRoot, 63 using, 64 extra, 65 }); 66 } 67 68 return using; 69} 70 71export function getAppDelegateObjcHeaderFilePath(projectRoot: string): string { 72 const [using, ...extra] = globSync('ios/*/AppDelegate.h', { 73 absolute: true, 74 cwd: projectRoot, 75 ignore: ignoredPaths, 76 }); 77 78 if (!using) { 79 throw new UnexpectedError(`Could not locate a valid AppDelegate.h at root: "${projectRoot}"`); 80 } 81 82 if (extra.length) { 83 warnMultipleFiles({ 84 tag: 'app-delegate-objc-header', 85 fileName: 'AppDelegate.h', 86 projectRoot, 87 using, 88 extra, 89 }); 90 } 91 92 return using; 93} 94 95function getLanguage(filePath: string): AppleLanguage { 96 const extension = path.extname(filePath); 97 switch (extension) { 98 case '.mm': 99 return 'objcpp'; 100 case '.m': 101 case '.h': 102 return 'objc'; 103 case '.swift': 104 return 'swift'; 105 default: 106 throw new UnexpectedError(`Unexpected iOS file extension: ${extension}`); 107 } 108} 109 110export function getFileInfo(filePath: string) { 111 return { 112 path: path.normalize(filePath), 113 contents: readFileSync(filePath, 'utf8'), 114 language: getLanguage(filePath), 115 }; 116} 117 118export function getAppDelegate(projectRoot: string): AppDelegateProjectFile { 119 const filePath = getAppDelegateFilePath(projectRoot); 120 return getFileInfo(filePath); 121} 122 123export function getSourceRoot(projectRoot: string): string { 124 const appDelegate = getAppDelegate(projectRoot); 125 return path.dirname(appDelegate.path); 126} 127 128export function findSchemePaths(projectRoot: string): string[] { 129 return globSync('ios/*.xcodeproj/xcshareddata/xcschemes/*.xcscheme', { 130 absolute: true, 131 cwd: projectRoot, 132 ignore: ignoredPaths, 133 }); 134} 135 136export function findSchemeNames(projectRoot: string): string[] { 137 const schemePaths = findSchemePaths(projectRoot); 138 return schemePaths.map((schemePath) => path.parse(schemePath).name); 139} 140 141export function getAllXcodeProjectPaths(projectRoot: string): string[] { 142 const iosFolder = 'ios'; 143 const pbxprojPaths = globSync('ios/**/*.xcodeproj', { cwd: projectRoot, ignore: ignoredPaths }) 144 .filter( 145 (project) => !/test|example|sample/i.test(project) || path.dirname(project) === iosFolder 146 ) 147 // sort alphabetically to ensure this works the same across different devices (Fail in CI (linux) without this) 148 .sort() 149 .sort((a, b) => { 150 const isAInIos = path.dirname(a) === iosFolder; 151 const isBInIos = path.dirname(b) === iosFolder; 152 // preserve previous sort order 153 if ((isAInIos && isBInIos) || (!isAInIos && !isBInIos)) { 154 return 0; 155 } 156 return isAInIos ? -1 : 1; 157 }); 158 159 if (!pbxprojPaths.length) { 160 throw new UnexpectedError( 161 `Failed to locate the ios/*.xcodeproj files relative to path "${projectRoot}".` 162 ); 163 } 164 return pbxprojPaths.map((value) => path.join(projectRoot, value)); 165} 166 167/** 168 * Get the pbxproj for the given path 169 */ 170export function getXcodeProjectPath(projectRoot: string): string { 171 const [using, ...extra] = getAllXcodeProjectPaths(projectRoot); 172 173 if (extra.length) { 174 warnMultipleFiles({ 175 tag: 'xcodeproj', 176 fileName: '*.xcodeproj', 177 projectRoot, 178 using, 179 extra, 180 }); 181 } 182 183 return using; 184} 185 186export function getAllPBXProjectPaths(projectRoot: string): string[] { 187 const projectPaths = getAllXcodeProjectPaths(projectRoot); 188 const paths = projectPaths 189 .map((value) => path.join(value, 'project.pbxproj')) 190 .filter((value) => existsSync(value)); 191 192 if (!paths.length) { 193 throw new UnexpectedError( 194 `Failed to locate the ios/*.xcodeproj/project.pbxproj files relative to path "${projectRoot}".` 195 ); 196 } 197 return paths; 198} 199 200export function getPBXProjectPath(projectRoot: string): string { 201 const [using, ...extra] = getAllPBXProjectPaths(projectRoot); 202 203 if (extra.length) { 204 warnMultipleFiles({ 205 tag: 'project-pbxproj', 206 fileName: 'project.pbxproj', 207 projectRoot, 208 using, 209 extra, 210 }); 211 } 212 213 return using; 214} 215 216export function getAllInfoPlistPaths(projectRoot: string): string[] { 217 const paths = globSync('ios/*/Info.plist', { 218 absolute: true, 219 cwd: projectRoot, 220 ignore: ignoredPaths, 221 }).sort( 222 // longer name means more suffixes, we want the shortest possible one to be first. 223 (a, b) => a.length - b.length 224 ); 225 226 if (!paths.length) { 227 throw new UnexpectedError( 228 `Failed to locate Info.plist files relative to path "${projectRoot}".` 229 ); 230 } 231 return paths; 232} 233 234export function getInfoPlistPath(projectRoot: string): string { 235 const [using, ...extra] = getAllInfoPlistPaths(projectRoot); 236 237 if (extra.length) { 238 warnMultipleFiles({ 239 tag: 'info-plist', 240 fileName: 'Info.plist', 241 projectRoot, 242 using, 243 extra, 244 }); 245 } 246 247 return using; 248} 249 250export function getAllEntitlementsPaths(projectRoot: string): string[] { 251 const paths = globSync('ios/*/*.entitlements', { 252 absolute: true, 253 cwd: projectRoot, 254 ignore: ignoredPaths, 255 }); 256 return paths; 257} 258 259/** 260 * @deprecated: use Entitlements.getEntitlementsPath instead 261 */ 262export function getEntitlementsPath(projectRoot: string): string | null { 263 return Entitlements.getEntitlementsPath(projectRoot); 264} 265 266export function getSupportingPath(projectRoot: string): string { 267 return path.resolve(projectRoot, 'ios', path.basename(getSourceRoot(projectRoot)), 'Supporting'); 268} 269 270export function getExpoPlistPath(projectRoot: string): string { 271 const supportingPath = getSupportingPath(projectRoot); 272 return path.join(supportingPath, 'Expo.plist'); 273} 274 275function warnMultipleFiles({ 276 tag, 277 fileName, 278 projectRoot, 279 using, 280 extra, 281}: { 282 tag: string; 283 fileName: string; 284 projectRoot?: string; 285 using: string; 286 extra: string[]; 287}) { 288 const usingPath = projectRoot ? path.relative(projectRoot, using) : using; 289 const extraPaths = projectRoot ? extra.map((v) => path.relative(projectRoot, v)) : extra; 290 addWarningIOS( 291 `paths-${tag}`, 292 `Found multiple ${fileName} file paths, using "${usingPath}". Ignored paths: ${JSON.stringify( 293 extraPaths 294 )}` 295 ); 296} 297