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