xref: /expo/packages/@expo/cli/src/utils/scheme.ts (revision 8a424beb)
18d307f52SEvan Baconimport { getConfig } from '@expo/config';
28d307f52SEvan Baconimport { AndroidConfig, IOSConfig } from '@expo/config-plugins';
3def098a4SEvan Baconimport { getInfoPlistPathFromPbxproj } from '@expo/config-plugins/build/ios/utils/getInfoPlistPath';
48d307f52SEvan Baconimport plist from '@expo/plist';
58d307f52SEvan Baconimport fs from 'fs';
6def098a4SEvan Baconimport path from 'path';
78d307f52SEvan Baconimport resolveFrom from 'resolve-from';
88d307f52SEvan Bacon
9*8a424bebSJames Ideimport { intersecting } from './array';
108d307f52SEvan Baconimport * as Log from '../log';
118d307f52SEvan Baconimport {
128d307f52SEvan Bacon  hasRequiredAndroidFilesAsync,
138d307f52SEvan Bacon  hasRequiredIOSFilesAsync,
148d307f52SEvan Bacon} from '../prebuild/clearNativeFolder';
158d307f52SEvan Bacon
16def098a4SEvan Baconconst debug = require('debug')('expo:utils:scheme') as typeof console.log;
17def098a4SEvan Bacon
188d307f52SEvan Bacon// sort longest to ensure uniqueness.
198d307f52SEvan Bacon// this might be undesirable as it causes the QR code to be longer.
208d307f52SEvan Baconfunction sortLongest(obj: string[]): string[] {
218d307f52SEvan Bacon  return obj.sort((a, b) => b.length - a.length);
228d307f52SEvan Bacon}
238d307f52SEvan Bacon
240b50ef93SCedric van Putten/**
250b50ef93SCedric van Putten * Resolve the scheme for the dev client using two methods:
260b50ef93SCedric van Putten *   - filter on known Expo schemes, starting with `exp+`, avoiding 3rd party schemes.
270b50ef93SCedric van Putten *   - filter on longest to ensure uniqueness.
280b50ef93SCedric van Putten */
290b50ef93SCedric van Puttenfunction resolveExpoOrLongestScheme(schemes: string[]): string[] {
300b50ef93SCedric van Putten  const expoOnlySchemes = schemes.filter((scheme) => scheme.startsWith('exp+'));
310b50ef93SCedric van Putten  return expoOnlySchemes.length > 0 ? sortLongest(expoOnlySchemes) : sortLongest(schemes);
320b50ef93SCedric van Putten}
330b50ef93SCedric van Putten
348d307f52SEvan Bacon// TODO: Revisit and test after run code is merged.
35def098a4SEvan Baconexport async function getSchemesForIosAsync(projectRoot: string): Promise<string[]> {
368d307f52SEvan Bacon  try {
37def098a4SEvan Bacon    const infoPlistBuildProperty = getInfoPlistPathFromPbxproj(projectRoot);
38def098a4SEvan Bacon    debug(`ios application Info.plist path:`, infoPlistBuildProperty);
39def098a4SEvan Bacon    if (infoPlistBuildProperty) {
40def098a4SEvan Bacon      const configPath = path.join(projectRoot, 'ios', infoPlistBuildProperty);
418d307f52SEvan Bacon      const rawPlist = fs.readFileSync(configPath, 'utf8');
428d307f52SEvan Bacon      const plistObject = plist.parse(rawPlist);
43def098a4SEvan Bacon      const schemes = IOSConfig.Scheme.getSchemesFromPlist(plistObject);
44def098a4SEvan Bacon      debug(`ios application schemes:`, schemes);
450b50ef93SCedric van Putten      return resolveExpoOrLongestScheme(schemes);
46def098a4SEvan Bacon    }
47def098a4SEvan Bacon  } catch (error) {
48def098a4SEvan Bacon    debug(`expected error collecting ios application schemes for the main target:`, error);
49def098a4SEvan Bacon  }
508d307f52SEvan Bacon  // No ios folder or some other error
518d307f52SEvan Bacon  return [];
528d307f52SEvan Bacon}
538d307f52SEvan Bacon
548d307f52SEvan Bacon// TODO: Revisit and test after run code is merged.
55def098a4SEvan Baconexport async function getSchemesForAndroidAsync(projectRoot: string): Promise<string[]> {
568d307f52SEvan Bacon  try {
578d307f52SEvan Bacon    const configPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectRoot);
588d307f52SEvan Bacon    const manifest = await AndroidConfig.Manifest.readAndroidManifestAsync(configPath);
59def098a4SEvan Bacon    const schemes = await AndroidConfig.Scheme.getSchemesFromManifest(manifest);
60def098a4SEvan Bacon    debug(`android application schemes:`, schemes);
610b50ef93SCedric van Putten    return resolveExpoOrLongestScheme(schemes);
62def098a4SEvan Bacon  } catch (error) {
63def098a4SEvan Bacon    debug(`expected error collecting android application schemes for the main activity:`, error);
648d307f52SEvan Bacon    // No android folder or some other error
658d307f52SEvan Bacon    return [];
668d307f52SEvan Bacon  }
678d307f52SEvan Bacon}
688d307f52SEvan Bacon
698d307f52SEvan Bacon// TODO: Revisit and test after run code is merged.
708d307f52SEvan Baconasync function getManagedDevClientSchemeAsync(projectRoot: string): Promise<string | null> {
718d307f52SEvan Bacon  const { exp } = getConfig(projectRoot);
728d307f52SEvan Bacon  try {
738d307f52SEvan Bacon    const getDefaultScheme = require(resolveFrom(projectRoot, 'expo-dev-client/getDefaultScheme'));
748d307f52SEvan Bacon    const scheme = getDefaultScheme(exp);
758d307f52SEvan Bacon    return scheme;
7698ecfc87SJames Ide  } catch {
778d307f52SEvan Bacon    Log.warn(
788d307f52SEvan Bacon      '\nDevelopment build: Unable to get the default URI scheme for the project. Please make sure the expo-dev-client package is installed.'
798d307f52SEvan Bacon    );
808d307f52SEvan Bacon    return null;
818d307f52SEvan Bacon  }
828d307f52SEvan Bacon}
838d307f52SEvan Bacon
848d307f52SEvan Bacon// TODO: Revisit and test after run code is merged.
858d307f52SEvan Baconexport async function getOptionalDevClientSchemeAsync(projectRoot: string): Promise<string | null> {
868d307f52SEvan Bacon  const [hasIos, hasAndroid] = await Promise.all([
878d307f52SEvan Bacon    hasRequiredIOSFilesAsync(projectRoot),
888d307f52SEvan Bacon    hasRequiredAndroidFilesAsync(projectRoot),
898d307f52SEvan Bacon  ]);
908d307f52SEvan Bacon
918d307f52SEvan Bacon  const [ios, android] = await Promise.all([
928d307f52SEvan Bacon    getSchemesForIosAsync(projectRoot),
938d307f52SEvan Bacon    getSchemesForAndroidAsync(projectRoot),
948d307f52SEvan Bacon  ]);
958d307f52SEvan Bacon
968d307f52SEvan Bacon  // Allow managed projects
978d307f52SEvan Bacon  if (!hasIos && !hasAndroid) {
988d307f52SEvan Bacon    return getManagedDevClientSchemeAsync(projectRoot);
998d307f52SEvan Bacon  }
1008d307f52SEvan Bacon
1018d307f52SEvan Bacon  let matching: string;
1028d307f52SEvan Bacon  // Allow for only one native project to exist.
1038d307f52SEvan Bacon  if (!hasIos) {
1048d307f52SEvan Bacon    matching = android[0];
1058d307f52SEvan Bacon  } else if (!hasAndroid) {
1068d307f52SEvan Bacon    matching = ios[0];
1078d307f52SEvan Bacon  } else {
1088d307f52SEvan Bacon    [matching] = intersecting(ios, android);
1098d307f52SEvan Bacon  }
1108d307f52SEvan Bacon  return matching ?? null;
1118d307f52SEvan Bacon}
112