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