xref: /expo/packages/@expo/cli/src/utils/scheme.ts (revision 8a424beb)
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