1import { getConfig } from '@expo/config'; 2import { AndroidConfig, IOSConfig } from '@expo/config-plugins'; 3import { getInfoPlistPathFromPbxproj } from '@expo/config-plugins/build/ios/utils/getInfoPlistPath'; 4import plist from '@expo/plist'; 5import fs from 'fs'; 6import path from 'path'; 7import resolveFrom from 'resolve-from'; 8 9import { intersecting } from './array'; 10import * as Log from '../log'; 11import { 12 hasRequiredAndroidFilesAsync, 13 hasRequiredIOSFilesAsync, 14} from '../prebuild/clearNativeFolder'; 15 16const debug = require('debug')('expo:utils:scheme') as typeof console.log; 17 18// sort longest to ensure uniqueness. 19// this might be undesirable as it causes the QR code to be longer. 20function sortLongest(obj: string[]): string[] { 21 return obj.sort((a, b) => b.length - a.length); 22} 23 24/** 25 * Resolve the scheme for the dev client using two methods: 26 * - filter on known Expo schemes, starting with `exp+`, avoiding 3rd party schemes. 27 * - filter on longest to ensure uniqueness. 28 */ 29function resolveExpoOrLongestScheme(schemes: string[]): string[] { 30 const expoOnlySchemes = schemes.filter((scheme) => scheme.startsWith('exp+')); 31 return expoOnlySchemes.length > 0 ? sortLongest(expoOnlySchemes) : sortLongest(schemes); 32} 33 34// TODO: Revisit and test after run code is merged. 35export async function getSchemesForIosAsync(projectRoot: string): Promise<string[]> { 36 try { 37 const infoPlistBuildProperty = getInfoPlistPathFromPbxproj(projectRoot); 38 debug(`ios application Info.plist path:`, infoPlistBuildProperty); 39 if (infoPlistBuildProperty) { 40 const configPath = path.join(projectRoot, 'ios', infoPlistBuildProperty); 41 const rawPlist = fs.readFileSync(configPath, 'utf8'); 42 const plistObject = plist.parse(rawPlist); 43 const schemes = IOSConfig.Scheme.getSchemesFromPlist(plistObject); 44 debug(`ios application schemes:`, schemes); 45 return resolveExpoOrLongestScheme(schemes); 46 } 47 } catch (error) { 48 debug(`expected error collecting ios application schemes for the main target:`, error); 49 } 50 // No ios folder or some other error 51 return []; 52} 53 54// TODO: Revisit and test after run code is merged. 55export async function getSchemesForAndroidAsync(projectRoot: string): Promise<string[]> { 56 try { 57 const configPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectRoot); 58 const manifest = await AndroidConfig.Manifest.readAndroidManifestAsync(configPath); 59 const schemes = await AndroidConfig.Scheme.getSchemesFromManifest(manifest); 60 debug(`android application schemes:`, schemes); 61 return resolveExpoOrLongestScheme(schemes); 62 } catch (error) { 63 debug(`expected error collecting android application schemes for the main activity:`, error); 64 // No android folder or some other error 65 return []; 66 } 67} 68 69// TODO: Revisit and test after run code is merged. 70async function getManagedDevClientSchemeAsync(projectRoot: string): Promise<string | null> { 71 const { exp } = getConfig(projectRoot); 72 try { 73 const getDefaultScheme = require(resolveFrom(projectRoot, 'expo-dev-client/getDefaultScheme')); 74 const scheme = getDefaultScheme(exp); 75 return scheme; 76 } catch { 77 Log.warn( 78 '\nDevelopment build: Unable to get the default URI scheme for the project. Please make sure the expo-dev-client package is installed.' 79 ); 80 return null; 81 } 82} 83 84// TODO: Revisit and test after run code is merged. 85export async function getOptionalDevClientSchemeAsync(projectRoot: string): Promise<string | null> { 86 const [hasIos, hasAndroid] = await Promise.all([ 87 hasRequiredIOSFilesAsync(projectRoot), 88 hasRequiredAndroidFilesAsync(projectRoot), 89 ]); 90 91 const [ios, android] = await Promise.all([ 92 getSchemesForIosAsync(projectRoot), 93 getSchemesForAndroidAsync(projectRoot), 94 ]); 95 96 // Allow managed projects 97 if (!hasIos && !hasAndroid) { 98 return getManagedDevClientSchemeAsync(projectRoot); 99 } 100 101 let matching: string; 102 // Allow for only one native project to exist. 103 if (!hasIos) { 104 matching = android[0]; 105 } else if (!hasAndroid) { 106 matching = ios[0]; 107 } else { 108 [matching] = intersecting(ios, android); 109 } 110 return matching ?? null; 111} 112