1import chalk from 'chalk'; 2import freeportAsync from 'freeport-async'; 3 4import * as Log from '../log'; 5import { env } from './env'; 6import { CommandError } from './errors'; 7 8/** Get a free port or assert a CLI command error. */ 9export async function getFreePortAsync(rangeStart: number): Promise<number> { 10 const port = await freeportAsync(rangeStart, { hostnames: [null, 'localhost'] }); 11 if (!port) { 12 throw new CommandError('NO_PORT_FOUND', 'No available port found'); 13 } 14 15 return port; 16} 17 18// TODO(Bacon): Revisit after all start and run code is merged. 19export async function choosePortAsync( 20 projectRoot: string, 21 { 22 defaultPort, 23 host, 24 reuseExistingPort, 25 }: { 26 defaultPort: number; 27 host?: string; 28 reuseExistingPort?: boolean; 29 } 30): Promise<number | null> { 31 const [{ getRunningProcess }, { confirmAsync }, isRoot, Log] = await Promise.all([ 32 import('./getRunningProcess'), 33 import('./prompts'), 34 import('is-root'), 35 import('../log'), 36 ]); 37 38 try { 39 const port = await freeportAsync(defaultPort, { hostnames: [host ?? null] }); 40 if (port === defaultPort) { 41 return port; 42 } 43 44 const isRestricted = process.platform !== 'win32' && defaultPort < 1024 && !isRoot.default(); 45 46 let message = isRestricted 47 ? `Admin permissions are required to run a server on a port below 1024` 48 : `Port ${chalk.bold(defaultPort)} is`; 49 50 const runningProcess = isRestricted ? null : getRunningProcess(defaultPort); 51 52 if (runningProcess) { 53 const pidTag = chalk.gray(`(pid ${runningProcess.pid})`); 54 if (runningProcess.directory === projectRoot) { 55 message += ` running this app in another window`; 56 if (reuseExistingPort) { 57 return null; 58 } 59 } else { 60 message += ` running ${chalk.cyan(runningProcess.command)} in another window`; 61 } 62 message += '\n' + chalk.gray(` ${runningProcess.directory} ${pidTag}`); 63 } 64 65 Log.log(`\u203A ${message}`); 66 const change = await confirmAsync({ 67 message: `Use port ${port} instead?`, 68 initial: true, 69 }); 70 return change ? port : null; 71 } catch (error: any) { 72 if (error.code === 'ABORTED') { 73 throw error; 74 } else if (error.code === 'NON_INTERACTIVE') { 75 Log.warn(chalk.yellow(error.message)); 76 return null; 77 } 78 throw error; 79 } 80} 81 82// TODO(Bacon): Revisit after all start and run code is merged. 83export async function resolvePortAsync( 84 projectRoot: string, 85 { 86 /** Should opt to reuse a port that is running the same project in another window. */ 87 reuseExistingPort, 88 /** Preferred port. */ 89 defaultPort, 90 /** Backup port for when the default isn't available. */ 91 fallbackPort, 92 }: { 93 reuseExistingPort?: boolean; 94 defaultPort?: string | number; 95 fallbackPort?: number; 96 } = {} 97): Promise<number | null> { 98 let port: number; 99 if (typeof defaultPort === 'string') { 100 port = parseInt(defaultPort, 10); 101 } else if (typeof defaultPort === 'number') { 102 port = defaultPort; 103 } else { 104 port = env.RCT_METRO_PORT || fallbackPort || 8081; 105 } 106 107 // Only check the port when the bundler is running. 108 const resolvedPort = await choosePortAsync(projectRoot, { 109 defaultPort: port, 110 reuseExistingPort, 111 }); 112 if (resolvedPort == null) { 113 Log.log('\u203A Skipping dev server'); 114 // Skip bundling if the port is null 115 } else { 116 // Use the new or resolved port 117 process.env.RCT_METRO_PORT = String(resolvedPort); 118 } 119 120 return resolvedPort; 121} 122