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 } else { 64 message += ' being used by another process'; 65 } 66 67 Log.log(`\u203A ${message}`); 68 const change = await confirmAsync({ 69 message: `Use port ${port} instead?`, 70 initial: true, 71 }); 72 return change ? port : null; 73 } catch (error: any) { 74 if (error.code === 'ABORTED') { 75 throw error; 76 } else if (error.code === 'NON_INTERACTIVE') { 77 Log.warn(chalk.yellow(error.message)); 78 return null; 79 } 80 throw error; 81 } 82} 83 84// TODO(Bacon): Revisit after all start and run code is merged. 85export async function resolvePortAsync( 86 projectRoot: string, 87 { 88 /** Should opt to reuse a port that is running the same project in another window. */ 89 reuseExistingPort, 90 /** Preferred port. */ 91 defaultPort, 92 /** Backup port for when the default isn't available. */ 93 fallbackPort, 94 }: { 95 reuseExistingPort?: boolean; 96 defaultPort?: string | number; 97 fallbackPort?: number; 98 } = {} 99): Promise<number | null> { 100 let port: number; 101 if (typeof defaultPort === 'string') { 102 port = parseInt(defaultPort, 10); 103 } else if (typeof defaultPort === 'number') { 104 port = defaultPort; 105 } else { 106 port = env.RCT_METRO_PORT || fallbackPort || 8081; 107 } 108 109 // Only check the port when the bundler is running. 110 const resolvedPort = await choosePortAsync(projectRoot, { 111 defaultPort: port, 112 reuseExistingPort, 113 }); 114 if (resolvedPort == null) { 115 Log.log('\u203A Skipping dev server'); 116 // Skip bundling if the port is null 117 } else { 118 // Use the new or resolved port 119 process.env.RCT_METRO_PORT = String(resolvedPort); 120 } 121 122 return resolvedPort; 123} 124