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