Skip to content

Commit a52255d

Browse files
chore: harden xtask for port checking
1 parent ddf70cc commit a52255d

4 files changed

Lines changed: 471 additions & 91 deletions

File tree

xtask/src/dev.rs

Lines changed: 88 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use std::fs;
99
use std::path::Path;
1010
use std::process::ExitCode;
1111

12-
use crate::process::{self, ManagedChild};
12+
use crate::process::{self, ManagedChild, ReservedPort};
1313
use crate::util::{collect_child_dirs, display_name};
1414

1515
pub 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

Comments
 (0)