1import assert from 'assert';
2
3import { AbortCommandError, CommandError } from '../utils/errors';
4import { resolvePortAsync } from '../utils/port';
5
6export type Options = {
7  forceManifestType: 'classic' | 'expo-updates';
8  android: boolean;
9  web: boolean;
10  ios: boolean;
11  offline: boolean;
12  clear: boolean;
13  dev: boolean;
14  https: boolean;
15  maxWorkers: number;
16  port: number;
17  /** Should instruct the bundler to create minified bundles. */
18  minify: boolean;
19  devClient: boolean;
20  scheme: string;
21  host: 'localhost' | 'lan' | 'tunnel';
22};
23
24export async function resolveOptionsAsync(projectRoot: string, args: any): Promise<Options> {
25  const forceManifestType = args['--force-manifest-type'];
26  if (forceManifestType) {
27    assert.match(forceManifestType, /^(classic|expo-updates)$/);
28  }
29  const host = resolveHostType({
30    host: args['--host'],
31    offline: args['--offline'],
32    lan: args['--lan'],
33    localhost: args['--localhost'],
34    tunnel: args['--tunnel'],
35  });
36
37  const scheme = await resolveSchemeAsync(projectRoot, {
38    scheme: args['--scheme'],
39    devClient: args['--dev-client'],
40  });
41
42  return {
43    forceManifestType,
44
45    android: !!args['--android'],
46    web: !!args['--web'],
47    ios: !!args['--ios'],
48    offline: !!args['--offline'],
49
50    clear: !!args['--clear'],
51    dev: !args['--no-dev'],
52    https: !!args['--https'],
53    maxWorkers: args['--max-workers'],
54    port: args['--port'],
55    minify: !!args['--minify'],
56
57    devClient: !!args['--dev-client'],
58
59    scheme,
60    host,
61  };
62}
63
64export async function resolveSchemeAsync(
65  projectRoot: string,
66  options: { scheme?: string; devClient?: boolean }
67): Promise<string | null> {
68  const resolveFrom = await import('resolve-from').then((m) => m.default);
69
70  const isDevClientPackageInstalled = (() => {
71    try {
72      // we check if `expo-dev-launcher` is installed instead of `expo-dev-client`
73      // because someone could install only launcher.
74      resolveFrom(projectRoot, 'expo-dev-launcher');
75      return true;
76    } catch {
77      return false;
78    }
79  })();
80
81  if (typeof options.scheme === 'string') {
82    // Use the custom scheme
83    return options.scheme ?? null;
84  } else if (options.devClient || isDevClientPackageInstalled) {
85    const { getOptionalDevClientSchemeAsync } = await import('../utils/scheme');
86    // Attempt to find the scheme or warn the user how to setup a custom scheme
87    return await getOptionalDevClientSchemeAsync(projectRoot);
88  } else {
89    // Ensure this is reset when users don't use `--scheme`, `--dev-client` and don't have the `expo-dev-client` package installed.
90    return null;
91  }
92}
93
94/** Resolve and assert host type options. */
95export function resolveHostType(options: {
96  host?: string;
97  offline?: boolean;
98  lan?: boolean;
99  localhost?: boolean;
100  tunnel?: boolean;
101}): 'lan' | 'tunnel' | 'localhost' {
102  if (
103    [options.offline, options.host, options.lan, options.localhost, options.tunnel].filter((i) => i)
104      .length > 1
105  ) {
106    throw new CommandError(
107      'BAD_ARGS',
108      'Specify at most one of: --offline, --host, --tunnel, --lan, --localhost'
109    );
110  }
111
112  if (options.offline) {
113    // Force `lan` in offline mode.
114    return 'lan';
115  } else if (options.host) {
116    assert.match(options.host, /^(lan|tunnel|localhost)$/);
117    return options.host as 'lan' | 'tunnel' | 'localhost';
118  } else if (options.tunnel) {
119    return 'tunnel';
120  } else if (options.lan) {
121    return 'lan';
122  } else if (options.localhost) {
123    return 'localhost';
124  }
125  return 'lan';
126}
127
128/** Resolve the port options for all supported bundlers. */
129export async function resolvePortsAsync(
130  projectRoot: string,
131  options: Partial<Pick<Options, 'port' | 'devClient'>>,
132  settings: { webOnly?: boolean }
133) {
134  const multiBundlerSettings: { webpackPort?: number; metroPort?: number } = {};
135
136  if (settings.webOnly) {
137    const webpackPort = await resolvePortAsync(projectRoot, {
138      defaultPort: options.port,
139      // Default web port
140      fallbackPort: 19006,
141    });
142    if (!webpackPort) {
143      throw new AbortCommandError();
144    }
145    multiBundlerSettings.webpackPort = webpackPort;
146  } else {
147    const devClientDefaultPort = parseInt(process.env.RCT_METRO_PORT, 10) || 8081;
148    const expoGoDefaultPort = 19000;
149    const metroPort = await resolvePortAsync(projectRoot, {
150      defaultPort: options.port,
151      fallbackPort: options.devClient ? devClientDefaultPort : expoGoDefaultPort,
152    });
153    if (!metroPort) {
154      throw new AbortCommandError();
155    }
156    multiBundlerSettings.metroPort = metroPort;
157  }
158
159  return multiBundlerSettings;
160}
161