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