@@ -9,7 +9,7 @@ use std::fs;
99use std:: path:: Path ;
1010use std:: process:: ExitCode ;
1111
12- use crate :: process:: { self , ManagedChild } ;
12+ use crate :: process:: { self , ManagedChild , ReservedPort } ;
1313use crate :: util:: { collect_child_dirs, display_name} ;
1414
1515pub fn run ( app : Option < & str > ) -> ExitCode {
@@ -29,8 +29,32 @@ pub fn run(app: Option<&str>) -> ExitCode {
2929 return ExitCode :: FAILURE ;
3030 }
3131
32- let port = read_port ( & app_dir) . unwrap_or_else ( || "?" . into ( ) ) ;
3332 let has_api = has_script ( & app_dir, "start:api" ) ;
33+ let server_port = read_port ( & app_dir) ;
34+ let api_port = if has_api {
35+ read_api_port ( & app_dir)
36+ } else {
37+ None
38+ } ;
39+ let reserved_ports = collect_reserved_ports ( server_port, api_port) ;
40+
41+ if let Err ( message) = process:: ensure_reserved_ports_available ( app_name, & reserved_ports) {
42+ eprintln ! ( "\n {} {}" , console:: style( "✘" ) . red( ) . bold( ) , message) ;
43+ eprintln ! (
44+ " {} Stop the process using the occupied port, or update {}" ,
45+ console:: style( "hint:" ) . dim( ) ,
46+ console:: style( app_dir. join( "package.json" ) . display( ) ) . bold( ) ,
47+ ) ;
48+ eprintln ! (
49+ " {} Stale dev servers from previous sessions can leave ports occupied.\n " ,
50+ console:: style( "hint:" ) . dim( ) ,
51+ ) ;
52+ return ExitCode :: FAILURE ;
53+ }
54+
55+ let port = server_port
56+ . map ( |value| value. to_string ( ) )
57+ . unwrap_or_else ( || "?" . into ( ) ) ;
3458
3559 // Header
3660 eprintln ! ( ) ;
@@ -47,11 +71,16 @@ pub fn run(app: Option<&str>) -> ExitCode {
4771 . bold( ) ,
4872 ) ;
4973 if has_api {
50- let api_port = read_api_port ( & app_dir) . unwrap_or_else ( || "?" . into ( ) ) ;
5174 eprintln ! (
5275 " {} API {}" ,
5376 console:: style( "▸" ) . dim( ) ,
54- console:: style( format!( "http://127.0.0.1:{api_port}/" ) ) . dim( ) ,
77+ console:: style( format!(
78+ "http://127.0.0.1:{}/" ,
79+ api_port
80+ . map( |value| value. to_string( ) )
81+ . unwrap_or_else( || "?" . into( ) )
82+ ) )
83+ . dim( ) ,
5584 ) ;
5685 }
5786 eprintln ! ( ) ;
@@ -122,36 +151,43 @@ fn print_usage() {
122151}
123152
124153/// Read the server port from package.json start:server script.
125- fn read_port ( app_dir : & Path ) -> Option < String > {
126- let content = fs:: read_to_string ( app_dir. join ( "package.json" ) ) . ok ( ) ?;
127- // Look for --port <number> in start:server script
128- let server_script = serde_json:: from_str :: < serde_json:: Value > ( & content)
129- . ok ( ) ?
130- . get ( "scripts" ) ?
131- . get ( "start:server" ) ?
132- . as_str ( ) ?
133- . to_string ( ) ;
134- server_script
135- . split_whitespace ( )
136- . zip ( server_script. split_whitespace ( ) . skip ( 1 ) )
137- . find ( |( flag, _) | * flag == "--port" )
138- . map ( |( _, port) | port. to_string ( ) )
154+ fn read_port ( app_dir : & Path ) -> Option < u16 > {
155+ read_script_flag_port ( app_dir, "start:server" , "--port" )
139156}
140157
141158/// Read the API port from package.json start:server script (--api-port).
142- fn read_api_port ( app_dir : & Path ) -> Option < String > {
159+ fn read_api_port ( app_dir : & Path ) -> Option < u16 > {
160+ read_script_flag_port ( app_dir, "start:server" , "--api-port" )
161+ }
162+
163+ fn read_script_flag_port ( app_dir : & Path , script_name : & str , flag : & str ) -> Option < u16 > {
143164 let content = fs:: read_to_string ( app_dir. join ( "package.json" ) ) . ok ( ) ?;
144- let server_script = serde_json:: from_str :: < serde_json:: Value > ( & content)
165+ let script = serde_json:: from_str :: < serde_json:: Value > ( & content)
145166 . ok ( ) ?
146167 . get ( "scripts" ) ?
147- . get ( "start:server" ) ?
168+ . get ( script_name ) ?
148169 . as_str ( ) ?
149170 . to_string ( ) ;
150- server_script
171+
172+ script
151173 . split_whitespace ( )
152- . zip ( server_script. split_whitespace ( ) . skip ( 1 ) )
153- . find ( |( flag, _) | * flag == "--api-port" )
154- . map ( |( _, port) | port. to_string ( ) )
174+ . zip ( script. split_whitespace ( ) . skip ( 1 ) )
175+ . find ( |( candidate_flag, _) | * candidate_flag == flag)
176+ . and_then ( |( _, port) | port. parse :: < u16 > ( ) . ok ( ) )
177+ }
178+
179+ fn collect_reserved_ports (
180+ server_port : Option < u16 > ,
181+ api_port : Option < u16 > ,
182+ ) -> Vec < ReservedPort < ' static > > {
183+ let mut ports = Vec :: with_capacity ( 2 ) ;
184+ if let Some ( port) = server_port {
185+ ports. push ( ReservedPort :: new ( "server" , port) ) ;
186+ }
187+ if let Some ( port) = api_port {
188+ ports. push ( ReservedPort :: new ( "api" , port) ) ;
189+ }
190+ ports
155191}
156192
157193/// Check whether `package.json` in `app_dir` contains a script with the given
@@ -186,3 +222,30 @@ fn available_apps() -> Result<String, String> {
186222 let names: Vec < String > = dirs. iter ( ) . map ( |d| display_name ( d) ) . collect ( ) ;
187223 Ok ( names. join ( ", " ) )
188224}
225+
226+ #[ cfg( test) ]
227+ mod tests {
228+ use super :: * ;
229+ use tempfile:: TempDir ;
230+
231+ fn create_app_dir ( package_json : & str ) -> TempDir {
232+ let dir = TempDir :: new ( ) . unwrap ( ) ;
233+ fs:: write ( dir. path ( ) . join ( "package.json" ) , package_json) . unwrap ( ) ;
234+ dir
235+ }
236+
237+ #[ test]
238+ fn test_read_ports_from_start_server_script ( ) {
239+ let app = create_app_dir (
240+ r#"{
241+ "scripts": {
242+ "start:server": "cargo run -p microsoft-webui-cli -- serve ./src --port 3003 --api-port 3013 --watch",
243+ "start:api": "node dist/api.js"
244+ }
245+ }"# ,
246+ ) ;
247+
248+ assert_eq ! ( read_port( app. path( ) ) , Some ( 3003 ) ) ;
249+ assert_eq ! ( read_api_port( app. path( ) ) , Some ( 3013 ) ) ;
250+ }
251+ }
0 commit comments