From cf21c75e8cee0473e38a376deaa961c685079c5e Mon Sep 17 00:00:00 2001 From: JongKyung Lee Date: Tue, 21 Jul 2026 17:41:32 +0900 Subject: [PATCH] refactor(cli): unify help document representation --- crates/vite_global_cli/src/help.rs | 232 +++++++++++------------------ 1 file changed, 83 insertions(+), 149 deletions(-) diff --git a/crates/vite_global_cli/src/help.rs b/crates/vite_global_cli/src/help.rs index fc773ac60a..e27f504de1 100644 --- a/crates/vite_global_cli/src/help.rs +++ b/crates/vite_global_cli/src/help.rs @@ -1,60 +1,40 @@ //! Unified help rendering for the global CLI. -use std::{fmt::Write as _, io::IsTerminal}; +use std::{borrow::Cow, fmt::Write as _, io::IsTerminal}; use clap::{CommandFactory, error::ErrorKind}; use owo_colors::OwoColorize; #[derive(Clone, Debug)] pub struct HelpDoc { - pub usage: &'static str, - pub summary: Vec<&'static str>, + pub usage: Cow<'static, str>, + pub summary: Vec>, pub sections: Vec, - pub documentation_url: Option<&'static str>, + pub documentation_url: Option>, } #[derive(Clone, Debug)] pub enum HelpSection { - Rows { title: &'static str, rows: Vec }, - Lines { title: &'static str, lines: Vec<&'static str> }, + Rows { title: Cow<'static, str>, rows: Vec }, + Lines { title: Cow<'static, str>, lines: Vec> }, } #[derive(Clone, Debug)] pub struct HelpRow { - pub label: &'static str, - pub description: Vec<&'static str>, -} - -#[derive(Clone, Debug)] -struct OwnedHelpDoc { - usage: String, - summary: Vec, - sections: Vec, - documentation_url: Option, -} - -#[derive(Clone, Debug)] -enum OwnedHelpSection { - Rows { title: String, rows: Vec }, - Lines { title: String, lines: Vec }, -} - -#[derive(Clone, Debug)] -struct OwnedHelpRow { - label: String, - description: Vec, + pub label: Cow<'static, str>, + pub description: Vec>, } fn row(label: &'static str, description: &'static str) -> HelpRow { - HelpRow { label, description: vec![description] } + HelpRow { label: label.into(), description: vec![description.into()] } } fn section_rows(title: &'static str, rows: Vec) -> HelpSection { - HelpSection::Rows { title, rows } + HelpSection::Rows { title: title.into(), rows } } fn section_lines(title: &'static str, lines: Vec<&'static str>) -> HelpSection { - HelpSection::Lines { title, lines } + HelpSection::Lines { title: title.into(), lines: lines.into_iter().map(Into::into).collect() } } fn documentation_url_for_command_path(command_path: &[&str]) -> Option<&'static str> { @@ -123,29 +103,6 @@ fn render_rows(rows: &[HelpRow]) -> Vec { return vec![]; } - let label_width = rows.iter().map(|row| row.label.len()).max().unwrap_or(0); - let mut output = Vec::new(); - - for row in rows { - let mut description_iter = row.description.iter(); - if let Some(first) = description_iter.next() { - output.push(format!(" {:label_width$} {}", row.label, first)); - for line in description_iter { - output.push(format!(" {:label_width$} {}", "", line)); - } - } else { - output.push(format!(" {}", row.label)); - } - } - - output -} - -fn render_owned_rows(rows: &[OwnedHelpRow]) -> Vec { - if rows.is_empty() { - return vec![]; - } - let label_width = rows.iter().map(|row| row.label.chars().count()).max().unwrap_or(0); let mut output = Vec::new(); @@ -183,7 +140,7 @@ fn render_muted_comment_suffix(line: &str) -> String { pub fn render_help_doc(doc: &HelpDoc) -> String { let mut output = String::new(); - let _ = writeln!(output, "{} {}", render_heading("Usage"), render_usage_value(doc.usage)); + let _ = writeln!(output, "{} {}", render_heading("Usage"), render_usage_value(&doc.usage)); if !doc.summary.is_empty() { let _ = writeln!(output); @@ -210,44 +167,7 @@ pub fn render_help_doc(doc: &HelpDoc) -> String { } } - if let Some(documentation_url) = doc.documentation_url { - write_documentation_footer(&mut output, documentation_url); - } - - output -} - -fn render_owned_help_doc(doc: &OwnedHelpDoc) -> String { - let mut output = String::new(); - - let _ = writeln!(output, "{} {}", render_heading("Usage"), render_usage_value(&doc.usage)); - - if !doc.summary.is_empty() { - let _ = writeln!(output); - for line in &doc.summary { - let _ = writeln!(output, "{line}"); - } - } - - for section in &doc.sections { - let _ = writeln!(output); - match section { - OwnedHelpSection::Rows { title, rows } => { - let _ = writeln!(output, "{}", render_heading(title)); - for line in render_owned_rows(rows) { - let _ = writeln!(output, "{line}"); - } - } - OwnedHelpSection::Lines { title, lines } => { - let _ = writeln!(output, "{}", render_heading(title)); - for line in lines { - let _ = writeln!(output, "{}", render_muted_comment_suffix(line)); - } - } - } - } - - if let Some(documentation_url) = &doc.documentation_url { + if let Some(documentation_url) = doc.documentation_url.as_deref() { write_documentation_footer(&mut output, documentation_url); } @@ -305,18 +225,18 @@ fn split_label_and_description(content: &str) -> Option<(String, String)> { None } -fn parse_rows(lines: &[String]) -> Vec { +fn parse_rows(lines: &[String]) -> Vec { parse_rows_with_alias_normalization(lines, false) } -fn parse_command_rows(lines: &[String]) -> Vec { +fn parse_command_rows(lines: &[String]) -> Vec { parse_rows_with_alias_normalization(lines, true) } fn parse_rows_with_alias_normalization( lines: &[String], normalize_alias_suffixes: bool, -) -> Vec { +) -> Vec { let mut rows = Vec::new(); for line in lines { @@ -336,23 +256,23 @@ fn parse_rows_with_alias_normalization( } else { (label, description) }; - rows.push(OwnedHelpRow { label, description: vec![description] }); + rows.push(HelpRow { label: label.into(), description: vec![description.into()] }); continue; } if leading >= 4 && content.starts_with('-') { - rows.push(OwnedHelpRow { label: content.to_string(), description: vec![] }); + rows.push(HelpRow { label: content.to_string().into(), description: vec![] }); continue; } if leading >= 4 { if let Some(last) = rows.last_mut() { - last.description.push(content.to_string()); + last.description.push(content.to_string().into()); continue; } } - rows.push(OwnedHelpRow { label: content.to_string(), description: vec![] }); + rows.push(HelpRow { label: content.to_string().into(), description: vec![] }); } rows @@ -396,7 +316,7 @@ fn strip_ansi(value: &str) -> String { output } -fn parse_clap_help_to_doc(raw_help: &str) -> Option { +fn parse_clap_help_to_doc(raw_help: &str) -> Option { let normalized = raw_help.replace("\r\n", "\n"); let lines: Vec = normalized.lines().map(strip_ansi).collect(); let usage_index = lines.iter().position(|line| line.starts_with("Usage: "))?; @@ -449,19 +369,28 @@ fn parse_clap_help_to_doc(raw_help: &str) -> Option { } else { parse_rows(&body) }; - sections.push(OwnedHelpSection::Rows { title, rows }); + sections.push(HelpSection::Rows { title: title.into(), rows }); } else { - let lines = body.into_iter().filter(|line| !line.trim().is_empty()).collect::>(); - sections.push(OwnedHelpSection::Lines { title, lines }); + let lines = body + .into_iter() + .filter(|line| !line.trim().is_empty()) + .map(Into::into) + .collect::>(); + sections.push(HelpSection::Lines { title: title.into(), lines }); } } - Some(OwnedHelpDoc { usage, summary, sections, documentation_url: None }) + Some(HelpDoc { + usage: usage.into(), + summary: summary.into_iter().map(Into::into).collect(), + sections, + documentation_url: None, + }) } pub fn top_level_help_doc() -> HelpDoc { HelpDoc { - usage: "vp [COMMAND]", + usage: "vp [COMMAND]".into(), summary: Vec::new(), sections: vec![ section_rows( @@ -531,14 +460,14 @@ pub fn top_level_help_doc() -> HelpDoc { ], ), ], - documentation_url: documentation_url_for_command_path(&[]), + documentation_url: documentation_url_for_command_path(&[]).map(Into::into), } } fn env_help_doc() -> HelpDoc { HelpDoc { - usage: "vp env [COMMAND]", - summary: vec!["Manage Node.js versions"], + usage: "vp env [COMMAND]".into(), + summary: vec!["Manage Node.js versions".into()], sections: vec![ section_rows( "Setup", @@ -619,15 +548,18 @@ fn env_help_doc() -> HelpDoc { ], ), ], - documentation_url: documentation_url_for_command_path(&["env"]), + documentation_url: documentation_url_for_command_path(&["env"]).map(Into::into), } } fn delegated_help_doc(command: &str) -> Option { match command { "dev" => Some(HelpDoc { - usage: "vp dev [ROOT] [OPTIONS]", - summary: vec!["Run the development server.", "Options are forwarded to Vite."], + usage: "vp dev [ROOT] [OPTIONS]".into(), + summary: vec![ + "Run the development server.".into(), + "Options are forwarded to Vite.".into(), + ], sections: vec![ section_rows( "Arguments", @@ -651,11 +583,11 @@ fn delegated_help_doc(command: &str) -> Option { vec![" vp dev", " vp dev --open", " vp dev --host localhost --port 5173"], ), ], - documentation_url: documentation_url_for_command_path(&["dev"]), + documentation_url: documentation_url_for_command_path(&["dev"]).map(Into::into), }), "build" => Some(HelpDoc { - usage: "vp build [ROOT] [OPTIONS]", - summary: vec!["Build for production.", "Options are forwarded to Vite."], + usage: "vp build [ROOT] [OPTIONS]".into(), + summary: vec!["Build for production.".into(), "Options are forwarded to Vite.".into()], sections: vec![ section_rows( "Arguments", @@ -679,11 +611,14 @@ fn delegated_help_doc(command: &str) -> Option { vec![" vp build", " vp build --watch", " vp build --sourcemap"], ), ], - documentation_url: documentation_url_for_command_path(&["build"]), + documentation_url: documentation_url_for_command_path(&["build"]).map(Into::into), }), "preview" => Some(HelpDoc { - usage: "vp preview [ROOT] [OPTIONS]", - summary: vec!["Preview production build.", "Options are forwarded to Vite."], + usage: "vp preview [ROOT] [OPTIONS]".into(), + summary: vec![ + "Preview production build.".into(), + "Options are forwarded to Vite.".into(), + ], sections: vec![ section_rows( "Arguments", @@ -704,11 +639,11 @@ fn delegated_help_doc(command: &str) -> Option { ), section_lines("Examples", vec![" vp preview", " vp preview --port 4173"]), ], - documentation_url: documentation_url_for_command_path(&["preview"]), + documentation_url: documentation_url_for_command_path(&["preview"]).map(Into::into), }), "test" => Some(HelpDoc { - usage: "vp test [COMMAND] [FILTERS] [OPTIONS]", - summary: vec!["Run tests.", "Options are forwarded to Vitest."], + usage: "vp test [COMMAND] [FILTERS] [OPTIONS]".into(), + summary: vec!["Run tests.".into(), "Options are forwarded to Vitest.".into()], sections: vec![ section_rows( "Commands", @@ -743,11 +678,11 @@ fn delegated_help_doc(command: &str) -> Option { ], ), ], - documentation_url: documentation_url_for_command_path(&["test"]), + documentation_url: documentation_url_for_command_path(&["test"]).map(Into::into), }), "lint" => Some(HelpDoc { - usage: "vp lint [PATH]... [OPTIONS]", - summary: vec!["Lint code.", "Options are forwarded to Oxlint."], + usage: "vp lint [PATH]... [OPTIONS]".into(), + summary: vec!["Lint code.".into(), "Options are forwarded to Oxlint.".into()], sections: vec![ section_rows( "Options", @@ -769,11 +704,11 @@ fn delegated_help_doc(command: &str) -> Option { ], ), ], - documentation_url: documentation_url_for_command_path(&["lint"]), + documentation_url: documentation_url_for_command_path(&["lint"]).map(Into::into), }), "fmt" => Some(HelpDoc { - usage: "vp fmt [PATH]... [OPTIONS]", - summary: vec!["Format code.", "Options are forwarded to Oxfmt."], + usage: "vp fmt [PATH]... [OPTIONS]".into(), + summary: vec!["Format code.".into(), "Options are forwarded to Oxfmt.".into()], sections: vec![ section_rows( "Options", @@ -791,11 +726,11 @@ fn delegated_help_doc(command: &str) -> Option { vec![" vp fmt", " vp fmt src --check", " vp fmt . --write"], ), ], - documentation_url: documentation_url_for_command_path(&["fmt"]), + documentation_url: documentation_url_for_command_path(&["fmt"]).map(Into::into), }), "check" => Some(HelpDoc { - usage: "vp check [OPTIONS] [PATHS]...", - summary: vec!["Run format, lint, and type checks."], + usage: "vp check [OPTIONS] [PATHS]...".into(), + summary: vec!["Run format, lint, and type checks.".into()], sections: vec![ section_rows( "Options", @@ -818,11 +753,11 @@ fn delegated_help_doc(command: &str) -> Option { vec![" vp check", " vp check --fix", " vp check --no-lint src/index.ts"], ), ], - documentation_url: documentation_url_for_command_path(&["check"]), + documentation_url: documentation_url_for_command_path(&["check"]).map(Into::into), }), "pack" => Some(HelpDoc { - usage: "vp pack [...FILES] [OPTIONS]", - summary: vec!["Build library.", "Options are forwarded to tsdown."], + usage: "vp pack [...FILES] [OPTIONS]".into(), + summary: vec!["Build library.".into(), "Options are forwarded to tsdown.".into()], sections: vec![ section_rows( "Options", @@ -841,11 +776,11 @@ fn delegated_help_doc(command: &str) -> Option { vec![" vp pack", " vp pack src/index.ts --dts", " vp pack --watch"], ), ], - documentation_url: documentation_url_for_command_path(&["pack"]), + documentation_url: documentation_url_for_command_path(&["pack"]).map(Into::into), }), "run" => Some(HelpDoc { - usage: "vp run [OPTIONS] [TASK_SPECIFIER] [ADDITIONAL_ARGS]...", - summary: vec!["Run tasks."], + usage: "vp run [OPTIONS] [TASK_SPECIFIER] [ADDITIONAL_ARGS]...".into(), + summary: vec!["Run tasks.".into()], sections: vec![ section_rows( "Arguments", @@ -907,11 +842,11 @@ fn delegated_help_doc(command: &str) -> Option { ], ), ], - documentation_url: documentation_url_for_command_path(&["run"]), + documentation_url: documentation_url_for_command_path(&["run"]).map(Into::into), }), "exec" => Some(HelpDoc { - usage: "vp exec [OPTIONS] [COMMAND]...", - summary: vec!["Execute a command from local node_modules/.bin."], + usage: "vp exec [OPTIONS] [COMMAND]...".into(), + summary: vec!["Execute a command from local node_modules/.bin.".into()], sections: vec![ section_rows( "Arguments", @@ -965,16 +900,16 @@ fn delegated_help_doc(command: &str) -> Option { ], ), ], - documentation_url: documentation_url_for_command_path(&["exec"]), + documentation_url: documentation_url_for_command_path(&["exec"]).map(Into::into), }), "cache" => Some(HelpDoc { - usage: "vp cache ", - summary: vec!["Manage the task cache."], + usage: "vp cache ".into(), + summary: vec!["Manage the task cache.".into()], sections: vec![ section_rows("Commands", vec![row("clean", "Clean up all the cache")]), section_rows("Options", vec![row("-h, --help", "Print help")]), ], - documentation_url: documentation_url_for_command_path(&["cache"]), + documentation_url: documentation_url_for_command_path(&["cache"]).map(Into::into), }), _ => None, } @@ -1130,14 +1065,13 @@ pub fn print_unified_clap_help_for_path(command_path: &[&str]) -> bool { let Some(doc) = parse_clap_help_to_doc(&raw_help) else { return false; }; - let doc = OwnedHelpDoc { - documentation_url: documentation_url_for_command_path(command_path) - .map(ToString::to_string), + let doc = HelpDoc { + documentation_url: documentation_url_for_command_path(command_path).map(Into::into), ..doc }; vite_shared::header::print_header(); - println!("{}", render_owned_help_doc(&doc)); + println!("{}", render_help_doc(&doc)); true } @@ -1349,10 +1283,10 @@ Options: #[test] fn render_help_doc_appends_documentation_footer() { let output = render_help_doc(&HelpDoc { - usage: "vp demo", + usage: "vp demo".into(), summary: vec![], sections: vec![], - documentation_url: Some("https://viteplus.dev/guide/demo"), + documentation_url: Some("https://viteplus.dev/guide/demo".into()), }); assert!(strip_ansi(&output).contains("Documentation: https://viteplus.dev/guide/demo"));