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