use heck::AsPascalCase;
use crate::sdk::{
collect_choice_types, collect_type_imports, escape_jsdoc, escape_ts_string, generated_header,
CodeWriter,
};
use crate::spec::arg::SpecDoubleDashChoices;
use crate::spec::cmd::SpecCommand;
use crate::{Spec, SpecArg, SpecFlag};
use super::types::{flag_property_name, sanitize_ident};
pub fn render(spec: &Spec, package_name: &str, source_file: &Option<String>) -> String {
let mut w = CodeWriter::new();
w.line(&generated_header("//", source_file));
w.line("import { CliRunner, CliResult } from \"./runtime\";");
let choice_types = collect_choice_types(&spec.cmd);
let type_imports = collect_type_imports(&spec.cmd, package_name, &choice_types);
let has_global_flags = spec.cmd.flags.iter().any(|f| f.global && !f.hide);
if has_global_flags {
let mut all_imports = type_imports;
all_imports.push("GlobalFlags".to_string());
all_imports.sort();
all_imports.dedup();
w.line(&format!(
"import {{ {} }} from \"./types\";",
all_imports.join(", ")
));
} else if !type_imports.is_empty() {
w.line(&format!(
"import {{ {} }} from \"./types\";",
type_imports.join(", ")
));
}
w.line("");
let global_flags: Vec<&SpecFlag> = spec
.cmd
.flags
.iter()
.filter(|f| f.global && !f.hide)
.collect();
let class_name = AsPascalCase(package_name).to_string();
render_class(
&spec.cmd,
&class_name,
true,
&global_flags,
&spec.bin,
&mut w,
);
w.finish()
}
fn subcmd_path(cmd: &SpecCommand) -> String {
cmd.full_cmd
.iter()
.map(|s| format!("\"{}\"", escape_ts_string(s)))
.collect::<Vec<_>>()
.join(", ")
}
fn render_class(
cmd: &SpecCommand,
class_name: &str,
is_root: bool,
global_flags: &[&SpecFlag],
bin_name: &str,
w: &mut CodeWriter,
) {
let visible_subcmds: Vec<_> = cmd.subcommands.iter().filter(|(_, c)| !c.hide).collect();
let visible_args: Vec<&SpecArg> = cmd.args.iter().filter(|a| !a.hide).collect();
let visible_flags: Vec<&SpecFlag> = cmd.flags.iter().filter(|f| !f.hide).collect();
let has_args = !visible_args.is_empty();
let has_flags = !visible_flags.is_empty() || !global_flags.is_empty();
let mut class_doc = Vec::new();
if let Some(help) = &cmd.help {
class_doc.push(help.clone());
} else if let Some(about) = &cmd.help_long {
class_doc.push(about.clone());
}
if let Some(deprecated) = &cmd.deprecated {
class_doc.push(format!("@deprecated {deprecated}"));
}
if !cmd.aliases.is_empty() {
class_doc.push(format!("Aliases: {}", cmd.aliases.join(", ")));
}
if !class_doc.is_empty() {
let class_doc: Vec<String> = class_doc.iter().map(|s| escape_jsdoc(s)).collect();
if class_doc.len() == 1 {
w.line(&format!("/** {} */", class_doc[0]));
} else {
w.line("/**");
for line in &class_doc {
w.line(&format!(" * {line}"));
}
w.line(" */");
}
}
w.line(&format!("export class {class_name} {{"));
w.indent();
w.line("private runner: CliRunner;");
for (name, subcmd) in &visible_subcmds {
let sub_class = AsPascalCase(name).to_string();
let prop = sanitize_ident(name);
let mut doc_parts = Vec::new();
if let Some(help) = &subcmd.help {
doc_parts.push(help.clone());
}
if let Some(dep) = &subcmd.deprecated {
doc_parts.push(format!("@deprecated {dep}"));
}
if !doc_parts.is_empty() {
w.line(&format!("/** {} */", escape_jsdoc(&doc_parts.join(". "))));
}
w.line(&format!("readonly {prop}: {sub_class};"));
}
for (name, subcmd) in &visible_subcmds {
for alias in &subcmd.aliases {
let alias_prop = sanitize_ident(alias);
let target_prop = sanitize_ident(name);
let sub_class = AsPascalCase(name).to_string();
w.line(&format!("/** Alias for `{}` */", escape_jsdoc(name)));
w.line(&format!(
"get {alias_prop}(): {sub_class} {{ return this.{target_prop}; }}"
));
}
}
w.line("");
if is_root {
w.line("constructor(binPath?: string) {");
w.indent();
w.line(&format!(
"this.runner = new CliRunner(binPath ?? \"{}\");",
escape_ts_string(bin_name)
));
} else {
w.line("constructor(runner: CliRunner) {");
w.indent();
w.line("this.runner = runner;");
}
for (name, _) in &visible_subcmds {
let sub_class = AsPascalCase(name).to_string();
let prop = sanitize_ident(name);
w.line(&format!("this.{prop} = new {sub_class}(this.runner);"));
}
w.dedent();
w.line("}");
let args_param = if has_args {
format!("args: {class_name}Args")
} else {
String::new()
};
let flags_type = if !global_flags.is_empty() && !visible_flags.is_empty() {
format!("{class_name}Flags")
} else if !global_flags.is_empty() && visible_flags.is_empty() {
"GlobalFlags".to_string()
} else if !visible_flags.is_empty() {
format!("{class_name}Flags")
} else {
String::new()
};
let flags_param = if !flags_type.is_empty() {
if has_args {
format!(", flags?: {flags_type}")
} else {
format!("flags?: {flags_type}")
}
} else {
String::new()
};
let mut exec_doc = Vec::new();
if !cmd.usage.is_empty() {
exec_doc.push(cmd.usage.clone());
}
for example in &cmd.examples {
let label = example.header.as_deref().unwrap_or("Example");
let lang = if example.lang.is_empty() {
""
} else {
&example.lang
};
exec_doc.push(format!(
"@example {label}\n```{lang}\n{code}\n```",
code = example.code
));
}
if !exec_doc.is_empty() {
let exec_doc: Vec<String> = exec_doc.iter().map(|s| escape_jsdoc(s)).collect();
if exec_doc.len() == 1 && !exec_doc[0].contains('\n') {
w.line(&format!("/** {} */", exec_doc[0]));
} else {
w.line("/**");
for part in &exec_doc {
for line in part.split('\n') {
w.line(&format!(" * {line}"));
}
}
w.line(" */");
}
}
if has_args || has_flags {
w.line(&format!(
"async exec({args_param}{flags_param}): Promise<CliResult> {{"
));
w.indent();
let path = subcmd_path(cmd);
w.line(&format!("const cmdArgs: string[] = [{path}];"));
if has_args {
let has_required_double_dash = visible_args
.iter()
.any(|a| matches!(a.double_dash, SpecDoubleDashChoices::Required));
let has_automatic_double_dash = visible_args
.iter()
.any(|a| matches!(a.double_dash, SpecDoubleDashChoices::Automatic));
for arg in &visible_args {
if matches!(arg.double_dash, SpecDoubleDashChoices::Required) {
continue;
}
let ident = sanitize_ident(&arg.name);
if arg.var {
w.line(&format!(
"if (args.{ident} !== undefined) {{ cmdArgs.push(...args.{ident}); }}"
));
} else {
w.line(&format!(
"if (args.{ident} !== undefined) {{ cmdArgs.push(String(args.{ident})); }}"
));
}
}
if has_required_double_dash {
w.line("cmdArgs.push(\"--\");");
for arg in &visible_args {
if !matches!(arg.double_dash, SpecDoubleDashChoices::Required) {
continue;
}
let ident = sanitize_ident(&arg.name);
if arg.var {
w.line(&format!(
"if (args.{ident} !== undefined) {{ cmdArgs.push(...args.{ident}); }}"
));
} else {
w.line(&format!(
"if (args.{ident} !== undefined) {{ cmdArgs.push(String(args.{ident})); }}"
));
}
}
} else if has_automatic_double_dash {
w.line(
"// double_dash=automatic: \"--\" is implied after the first positional arg",
);
}
}
if has_flags {
w.line("const flagArgs = this.buildFlagArgs(flags);");
w.line("return this.runner.run([...cmdArgs, ...flagArgs]);");
} else {
w.line("return this.runner.run(cmdArgs);");
}
w.dedent();
w.line("}");
if has_flags {
w.line("");
w.line(&format!(
"private buildFlagArgs(flags?: {flags_type}): string[] {{"
));
w.indent();
w.line("const result: string[] = [];");
w.line("if (!flags) return result;");
for flag in global_flags {
render_flag_build(flag, w);
}
for flag in &visible_flags {
if !global_flags.iter().any(|gf| gf.name == flag.name) {
render_flag_build(flag, w);
}
}
w.line("return result;");
w.dedent();
w.line("}");
}
} else {
w.line("async exec(): Promise<CliResult> {");
w.indent();
let path = subcmd_path(cmd);
w.line(&format!("return this.runner.run([{path}]);"));
w.dedent();
w.line("}");
}
w.dedent();
w.line("}");
for (name, subcmd) in &visible_subcmds {
w.line("");
let sub_class = AsPascalCase(name).to_string();
render_class(subcmd, &sub_class, false, global_flags, bin_name, w);
}
}
fn render_flag_build(flag: &SpecFlag, w: &mut CodeWriter) {
let prop_name = flag_property_name(flag);
let flag_arg_name = if let Some(long) = flag.long.first() {
format!("--{}", escape_ts_string(long))
} else if let Some(short) = flag.short.first() {
format!("-{short}")
} else {
format!("--{}", escape_ts_string(&flag.name))
};
if flag.arg.is_some() {
if flag.var {
w.line(&format!(
"if (flags.{prop_name} !== undefined) {{ for (const v of flags.{prop_name}) {{ result.push(\"{flag_arg_name}\", String(v)); }} }}"
));
} else {
w.line(&format!(
"if (flags.{prop_name} !== undefined) {{ result.push(\"{flag_arg_name}\", String(flags.{prop_name})); }}"
));
}
} else if flag.count {
w.line(&format!(
"if (flags.{prop_name} !== undefined && flags.{prop_name} > 0) {{ for (let i = 0; i < flags.{prop_name}; i++) {{ result.push(\"{flag_arg_name}\"); }} }}"
));
} else if flag.var {
w.line(&format!(
"if (flags.{prop_name} !== undefined) {{ for (const v of flags.{prop_name}) {{ if (v) result.push(\"{flag_arg_name}\"); }} }}"
));
} else {
w.line(&format!(
"if (flags.{prop_name}) {{ result.push(\"{flag_arg_name}\"); }}"
));
if let Some(negate) = &flag.negate {
w.line(&format!(
"else if (flags.{prop_name} === false) {{ result.push(\"{}\"); }}",
escape_ts_string(negate)
));
}
}
}