1import assert from 'assert';
2
3import { hasDirectDevClientDependency } from '../utils/analytics/getDevClientProperties';
4import { AbortCommandError, CommandError } from '../utils/errors';
5import { resolvePortAsync } from '../utils/port';
6
7export type Options = {
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  if (args['--dev-client'] && args['--go']) {
27    throw new CommandError('BAD_ARGS', 'Cannot use both --dev-client and --go together.');
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  // User can force the default target by passing either `--dev-client` or `--go`. They can also
38  // swap between them during development by pressing `s`.
39  const isUserDefinedDevClient =
40    !!args['--dev-client'] || (args['--go'] == null ? false : !args['--go']);
41
42  // If the user didn't specify `--dev-client` or `--go` we check if they have the dev client package
43  // in their package.json.
44  const isAutoDevClient =
45    args['--dev-client'] == null &&
46    args['--go'] == null &&
47    hasDirectDevClientDependency(projectRoot);
48
49  const isDevClient = isAutoDevClient || isUserDefinedDevClient;
50
51  const scheme = await resolveSchemeAsync(projectRoot, {
52    scheme: args['--scheme'],
53    devClient: isDevClient,
54  });
55
56  return {
57    privateKeyPath: args['--private-key-path'] ?? null,
58
59    android: !!args['--android'],
60    web: !!args['--web'],
61    ios: !!args['--ios'],
62    offline: !!args['--offline'],
63
64    clear: !!args['--clear'],
65    dev: !args['--no-dev'],
66    https: !!args['--https'],
67    maxWorkers: args['--max-workers'],
68    port: args['--port'],
69    minify: !!args['--minify'],
70
71    devClient: isDevClient,
72
73    scheme,
74    host,
75  };
76}
77
78export async function resolveSchemeAsync(
79  projectRoot: string,
80  options: { scheme?: string; devClient?: boolean }
81): Promise<string | null> {
82  const resolveFrom = require('resolve-from') as typeof import('resolve-from');
83
84  const isDevClientPackageInstalled = (() => {
85    try {
86      // we check if `expo-dev-launcher` is installed instead of `expo-dev-client`
87      // because someone could install only launcher.
88      resolveFrom(projectRoot, 'expo-dev-launcher');
89      return true;
90    } catch {
91      return false;
92    }
93  })();
94
95  if (typeof options.scheme === 'string') {
96    // Use the custom scheme
97    return options.scheme ?? null;
98  } else if (options.devClient || isDevClientPackageInstalled) {
99    const { getOptionalDevClientSchemeAsync } =
100      require('../utils/scheme') as typeof import('../utils/scheme');
101    // Attempt to find the scheme or warn the user how to setup a custom scheme
102    return await getOptionalDevClientSchemeAsync(projectRoot);
103  } else {
104    // Ensure this is reset when users don't use `--scheme`, `--dev-client` and don't have the `expo-dev-client` package installed.
105    return null;
106  }
107}
108
109/** Resolve and assert host type options. */
110export function resolveHostType(options: {
111  host?: string;
112  offline?: boolean;
113  lan?: boolean;
114  localhost?: boolean;
115  tunnel?: boolean;
116}): 'lan' | 'tunnel' | 'localhost' {
117  if (
118    [options.offline, options.host, options.lan, options.localhost, options.tunnel].filter((i) => i)
119      .length > 1
120  ) {
121    throw new CommandError(
122      'BAD_ARGS',
123      'Specify at most one of: --offline, --host, --tunnel, --lan, --localhost'
124    );
125  }
126
127  if (options.offline) {
128    // Force `lan` in offline mode.
129    return 'lan';
130  } else if (options.host) {
131    assert.match(options.host, /^(lan|tunnel|localhost)$/);
132    return options.host as 'lan' | 'tunnel' | 'localhost';
133  } else if (options.tunnel) {
134    return 'tunnel';
135  } else if (options.lan) {
136    return 'lan';
137  } else if (options.localhost) {
138    return 'localhost';
139  }
140  return 'lan';
141}
142
143/** Resolve the port options for all supported bundlers. */
144export async function resolvePortsAsync(
145  projectRoot: string,
146  options: Partial<Pick<Options, 'port' | 'devClient'>>,
147  settings: { webOnly?: boolean }
148) {
149  const multiBundlerSettings: { webpackPort?: number; metroPort?: number } = {};
150
151  if (settings.webOnly) {
152    const webpackPort = await resolvePortAsync(projectRoot, {
153      defaultPort: options.port,
154      // Default web port
155      fallbackPort: 19006,
156    });
157    if (!webpackPort) {
158      throw new AbortCommandError();
159    }
160    multiBundlerSettings.webpackPort = webpackPort;
161  } else {
162    const fallbackPort = process.env.RCT_METRO_PORT
163      ? parseInt(process.env.RCT_METRO_PORT, 10)
164      : 8081;
165    const metroPort = await resolvePortAsync(projectRoot, {
166      defaultPort: options.port,
167      fallbackPort,
168    });
169    if (!metroPort) {
170      throw new AbortCommandError();
171    }
172    multiBundlerSettings.metroPort = metroPort;
173  }
174
175  return multiBundlerSettings;
176}
177