use std::path::{Path, PathBuf};
use std::process::Command;
use superui_cli::{
find_module_dts, gitignore_needs_entry, projected_modules, tsconfig_has_path, GITIGNORE_ENTRY,
TSCONFIG_TEMPLATE,
};
fn main() {
if let Err(e) = run() {
eprintln!("cargo-superui: {e}");
std::process::exit(1);
}
}
fn run() -> Result<(), Box<dyn std::error::Error>> {
let mut args: Vec<String> = std::env::args().skip(1).collect();
if args.first().map(String::as_str) == Some("superui") {
args.remove(0);
}
match args.first().map(String::as_str) {
Some("install") => install(&args[1..]),
None => Err("no command given; try `cargo superui install`".into()),
other => Err(format!("unknown command {other:?}; try `cargo superui install`").into()),
}
}
fn flag(args: &[String], name: &str) -> Option<String> {
args.iter().position(|a| a == name).and_then(|i| args.get(i + 1).cloned())
}
fn install(args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
let app_dir = match flag(args, "--path") {
Some(p) => PathBuf::from(p),
None => current_package_dir()?,
};
let metadata = run_cargo(&["metadata", "--format-version", "1"])?;
for m in projected_modules() {
let dts_src = match find_module_dts(&metadata, m.package, m.dts_filename) {
Some(p) => p,
None if m.required => {
return Err(format!(
"no `{}` dependency resolved — add it (or `superui`) to Cargo.toml",
m.package
)
.into());
}
None => {
println!("skip {} types — `{}` not in dependency graph", m.specifier, m.package);
continue;
}
};
let dts = std::fs::read_to_string(&dts_src)
.map_err(|e| format!("reading {}: {e}", dts_src.display()))?;
let module_dir = app_dir.join("superui_modules").join(m.subpath);
std::fs::create_dir_all(&module_dir)?;
let index = module_dir.join("index.d.ts");
std::fs::write(&index, &dts)?;
println!("wrote {}", index.display());
}
let tsconfig = app_dir.join("tsconfig.json");
if !tsconfig.exists() {
std::fs::write(&tsconfig, TSCONFIG_TEMPLATE)?;
println!("wrote {}", tsconfig.display());
} else {
let existing = std::fs::read_to_string(&tsconfig)?;
for m in projected_modules() {
if tsconfig_has_path(&existing, &m.marker()) {
println!("ok {} (already maps {})", tsconfig.display(), m.specifier);
} else {
println!(
"note {} exists but does not map {} — add to compilerOptions.paths:\n \"{}\": [\"{}\"]",
tsconfig.display(),
m.specifier,
m.specifier,
m.index_path()
);
}
}
}
let gitignore = app_dir.join(".gitignore");
let existing = std::fs::read_to_string(&gitignore).unwrap_or_default();
if gitignore_needs_entry(&existing) {
let mut next = existing;
if !next.is_empty() && !next.ends_with('\n') {
next.push('\n');
}
next.push_str(GITIGNORE_ENTRY);
next.push('\n');
std::fs::write(&gitignore, next)?;
println!("updated {}", gitignore.display());
}
println!("done: superui IDE types installed in {}", app_dir.display());
Ok(())
}
fn current_package_dir() -> Result<PathBuf, Box<dyn std::error::Error>> {
let out = run_cargo(&["locate-project", "--message-format", "plain"])?;
let manifest = out.trim();
let dir = Path::new(manifest)
.parent()
.ok_or("could not determine package directory")?;
Ok(dir.to_path_buf())
}
fn run_cargo(args: &[&str]) -> Result<String, Box<dyn std::error::Error>> {
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".into());
let out = Command::new(cargo).args(args).output()?;
if !out.status.success() {
return Err(format!(
"`cargo {}` failed: {}",
args.join(" "),
String::from_utf8_lossy(&out.stderr)
)
.into());
}
Ok(String::from_utf8(out.stdout)?)
}