18d307f52SEvan Baconimport assert from 'assert';
28d307f52SEvan Bacon
3*8df9096fSEvan Baconimport { hasDirectDevClientDependency } from '../utils/analytics/getDevClientProperties';
48d307f52SEvan Baconimport { AbortCommandError, CommandError } from '../utils/errors';
58d307f52SEvan Baconimport { resolvePortAsync } from '../utils/port';
68d307f52SEvan Bacon
78d307f52SEvan Baconexport type Options = {
8e377ff85SWill Schurman  privateKeyPath: string | null;
98d307f52SEvan Bacon  android: boolean;
108d307f52SEvan Bacon  web: boolean;
118d307f52SEvan Bacon  ios: boolean;
128d307f52SEvan Bacon  offline: boolean;
138d307f52SEvan Bacon  clear: boolean;
148d307f52SEvan Bacon  dev: boolean;
158d307f52SEvan Bacon  https: boolean;
168d307f52SEvan Bacon  maxWorkers: number;
178d307f52SEvan Bacon  port: number;
188d307f52SEvan Bacon  /** Should instruct the bundler to create minified bundles. */
198d307f52SEvan Bacon  minify: boolean;
208d307f52SEvan Bacon  devClient: boolean;
2129975bfdSEvan Bacon  scheme: string | null;
228d307f52SEvan Bacon  host: 'localhost' | 'lan' | 'tunnel';
238d307f52SEvan Bacon};
248d307f52SEvan Bacon
258d307f52SEvan Baconexport async function resolveOptionsAsync(projectRoot: string, args: any): Promise<Options> {
2641c91838SEvan Bacon  if (args['--dev-client'] && args['--go']) {
2741c91838SEvan Bacon    throw new CommandError('BAD_ARGS', 'Cannot use both --dev-client and --go together.');
2841c91838SEvan Bacon  }
298d307f52SEvan Bacon  const host = resolveHostType({
308d307f52SEvan Bacon    host: args['--host'],
318d307f52SEvan Bacon    offline: args['--offline'],
328d307f52SEvan Bacon    lan: args['--lan'],
338d307f52SEvan Bacon    localhost: args['--localhost'],
348d307f52SEvan Bacon    tunnel: args['--tunnel'],
358d307f52SEvan Bacon  });
368d307f52SEvan Bacon
37*8df9096fSEvan Bacon  // User can force the default target by passing either `--dev-client` or `--go`. They can also
38*8df9096fSEvan Bacon  // swap between them during development by pressing `s`.
39*8df9096fSEvan Bacon  const isUserDefinedDevClient =
40*8df9096fSEvan Bacon    !!args['--dev-client'] || (args['--go'] == null ? false : !args['--go']);
41*8df9096fSEvan Bacon
42*8df9096fSEvan Bacon  // If the user didn't specify `--dev-client` or `--go` we check if they have the dev client package
43*8df9096fSEvan Bacon  // in their package.json.
44*8df9096fSEvan Bacon  const isAutoDevClient =
45*8df9096fSEvan Bacon    args['--dev-client'] == null &&
46*8df9096fSEvan Bacon    args['--go'] == null &&
47*8df9096fSEvan Bacon    hasDirectDevClientDependency(projectRoot);
48*8df9096fSEvan Bacon
49*8df9096fSEvan Bacon  const isDevClient = isAutoDevClient || isUserDefinedDevClient;
5041c91838SEvan Bacon
518d307f52SEvan Bacon  const scheme = await resolveSchemeAsync(projectRoot, {
528d307f52SEvan Bacon    scheme: args['--scheme'],
5341c91838SEvan Bacon    devClient: isDevClient,
548d307f52SEvan Bacon  });
558d307f52SEvan Bacon
568d307f52SEvan Bacon  return {
57c14835f6SWill Schurman    privateKeyPath: args['--private-key-path'] ?? null,
588d307f52SEvan Bacon
598d307f52SEvan Bacon    android: !!args['--android'],
608d307f52SEvan Bacon    web: !!args['--web'],
618d307f52SEvan Bacon    ios: !!args['--ios'],
628d307f52SEvan Bacon    offline: !!args['--offline'],
638d307f52SEvan Bacon
648d307f52SEvan Bacon    clear: !!args['--clear'],
658d307f52SEvan Bacon    dev: !args['--no-dev'],
668d307f52SEvan Bacon    https: !!args['--https'],
678d307f52SEvan Bacon    maxWorkers: args['--max-workers'],
688d307f52SEvan Bacon    port: args['--port'],
698d307f52SEvan Bacon    minify: !!args['--minify'],
708d307f52SEvan Bacon
7141c91838SEvan Bacon    devClient: isDevClient,
728d307f52SEvan Bacon
738d307f52SEvan Bacon    scheme,
748d307f52SEvan Bacon    host,
758d307f52SEvan Bacon  };
768d307f52SEvan Bacon}
778d307f52SEvan Bacon
788d307f52SEvan Baconexport async function resolveSchemeAsync(
798d307f52SEvan Bacon  projectRoot: string,
808d307f52SEvan Bacon  options: { scheme?: string; devClient?: boolean }
818d307f52SEvan Bacon): Promise<string | null> {
8241c91838SEvan Bacon  const resolveFrom = require('resolve-from') as typeof import('resolve-from');
838d307f52SEvan Bacon
848d307f52SEvan Bacon  const isDevClientPackageInstalled = (() => {
858d307f52SEvan Bacon    try {
868d307f52SEvan Bacon      // we check if `expo-dev-launcher` is installed instead of `expo-dev-client`
878d307f52SEvan Bacon      // because someone could install only launcher.
888d307f52SEvan Bacon      resolveFrom(projectRoot, 'expo-dev-launcher');
898d307f52SEvan Bacon      return true;
908d307f52SEvan Bacon    } catch {
918d307f52SEvan Bacon      return false;
928d307f52SEvan Bacon    }
938d307f52SEvan Bacon  })();
948d307f52SEvan Bacon
958d307f52SEvan Bacon  if (typeof options.scheme === 'string') {
968d307f52SEvan Bacon    // Use the custom scheme
978d307f52SEvan Bacon    return options.scheme ?? null;
988d307f52SEvan Bacon  } else if (options.devClient || isDevClientPackageInstalled) {
9941c91838SEvan Bacon    const { getOptionalDevClientSchemeAsync } =
10041c91838SEvan Bacon      require('../utils/scheme') as typeof import('../utils/scheme');
1018d307f52SEvan Bacon    // Attempt to find the scheme or warn the user how to setup a custom scheme
1028d307f52SEvan Bacon    return await getOptionalDevClientSchemeAsync(projectRoot);
1038d307f52SEvan Bacon  } else {
1048d307f52SEvan Bacon    // Ensure this is reset when users don't use `--scheme`, `--dev-client` and don't have the `expo-dev-client` package installed.
1058d307f52SEvan Bacon    return null;
1068d307f52SEvan Bacon  }
1078d307f52SEvan Bacon}
1088d307f52SEvan Bacon
1098d307f52SEvan Bacon/** Resolve and assert host type options. */
1108d307f52SEvan Baconexport function resolveHostType(options: {
1118d307f52SEvan Bacon  host?: string;
1128d307f52SEvan Bacon  offline?: boolean;
1138d307f52SEvan Bacon  lan?: boolean;
1148d307f52SEvan Bacon  localhost?: boolean;
1158d307f52SEvan Bacon  tunnel?: boolean;
1168d307f52SEvan Bacon}): 'lan' | 'tunnel' | 'localhost' {
1178d307f52SEvan Bacon  if (
1188d307f52SEvan Bacon    [options.offline, options.host, options.lan, options.localhost, options.tunnel].filter((i) => i)
1198d307f52SEvan Bacon      .length > 1
1208d307f52SEvan Bacon  ) {
1218d307f52SEvan Bacon    throw new CommandError(
1228d307f52SEvan Bacon      'BAD_ARGS',
1238d307f52SEvan Bacon      'Specify at most one of: --offline, --host, --tunnel, --lan, --localhost'
1248d307f52SEvan Bacon    );
1258d307f52SEvan Bacon  }
1268d307f52SEvan Bacon
1278d307f52SEvan Bacon  if (options.offline) {
1288d307f52SEvan Bacon    // Force `lan` in offline mode.
1298d307f52SEvan Bacon    return 'lan';
1308d307f52SEvan Bacon  } else if (options.host) {
1318d307f52SEvan Bacon    assert.match(options.host, /^(lan|tunnel|localhost)$/);
1328d307f52SEvan Bacon    return options.host as 'lan' | 'tunnel' | 'localhost';
1338d307f52SEvan Bacon  } else if (options.tunnel) {
1348d307f52SEvan Bacon    return 'tunnel';
1358d307f52SEvan Bacon  } else if (options.lan) {
1368d307f52SEvan Bacon    return 'lan';
1378d307f52SEvan Bacon  } else if (options.localhost) {
1388d307f52SEvan Bacon    return 'localhost';
1398d307f52SEvan Bacon  }
1408d307f52SEvan Bacon  return 'lan';
1418d307f52SEvan Bacon}
1428d307f52SEvan Bacon
1438d307f52SEvan Bacon/** Resolve the port options for all supported bundlers. */
1448d307f52SEvan Baconexport async function resolvePortsAsync(
1458d307f52SEvan Bacon  projectRoot: string,
1468d307f52SEvan Bacon  options: Partial<Pick<Options, 'port' | 'devClient'>>,
1478d307f52SEvan Bacon  settings: { webOnly?: boolean }
1488d307f52SEvan Bacon) {
1498d307f52SEvan Bacon  const multiBundlerSettings: { webpackPort?: number; metroPort?: number } = {};
1508d307f52SEvan Bacon
1518d307f52SEvan Bacon  if (settings.webOnly) {
1528d307f52SEvan Bacon    const webpackPort = await resolvePortAsync(projectRoot, {
1538d307f52SEvan Bacon      defaultPort: options.port,
1548d307f52SEvan Bacon      // Default web port
1558d307f52SEvan Bacon      fallbackPort: 19006,
1568d307f52SEvan Bacon    });
1578d307f52SEvan Bacon    if (!webpackPort) {
1588d307f52SEvan Bacon      throw new AbortCommandError();
1598d307f52SEvan Bacon    }
1608d307f52SEvan Bacon    multiBundlerSettings.webpackPort = webpackPort;
1618d307f52SEvan Bacon  } else {
16247d62600SKudo Chien    const fallbackPort = process.env.RCT_METRO_PORT
16329975bfdSEvan Bacon      ? parseInt(process.env.RCT_METRO_PORT, 10)
16429975bfdSEvan Bacon      : 8081;
1658d307f52SEvan Bacon    const metroPort = await resolvePortAsync(projectRoot, {
1668d307f52SEvan Bacon      defaultPort: options.port,
16747d62600SKudo Chien      fallbackPort,
1688d307f52SEvan Bacon    });
1698d307f52SEvan Bacon    if (!metroPort) {
1708d307f52SEvan Bacon      throw new AbortCommandError();
1718d307f52SEvan Bacon    }
1728d307f52SEvan Bacon    multiBundlerSettings.metroPort = metroPort;
1738d307f52SEvan Bacon  }
1748d307f52SEvan Bacon
1758d307f52SEvan Bacon  return multiBundlerSettings;
1768d307f52SEvan Bacon}
177