Skip to main content

npmgen_cli/
lib.rs

1//! CLI driver shared by the `npmgen` and `cargo-npmgen` binaries.
2
3mod cli;
4
5use std::ffi::OsString;
6
7use clap::Parser;
8use tracing_subscriber::EnvFilter;
9
10use cli::Cli;
11
12/// Default log filter when `RUST_LOG` is unset.
13const DEFAULT_LOG_FILTER: &str = "info";
14
15/// Parse arguments and run a generation. Both binaries delegate here, so
16/// `npmgen …`, `cargo-npmgen …`, and `cargo npmgen …` behave identically.
17pub fn main() {
18    init_tracing();
19    let cli = Cli::parse_from(strip_cargo_subcommand(std::env::args_os()));
20    if let Err(error) = cli.run() {
21        tracing::error!("npmgen failed: {}", error_chain(&error));
22        std::process::exit(1);
23    }
24}
25
26/// Render an error and its full source chain (`top: cause: root-cause`), so the
27/// underlying cargo/IO/build failure is not hidden behind the facade message.
28fn error_chain(error: &dyn std::error::Error) -> String {
29    let mut message = error.to_string();
30    let mut source = error.source();
31    while let Some(cause) = source {
32        message.push_str(": ");
33        message.push_str(&cause.to_string());
34        source = cause.source();
35    }
36    message
37}
38
39fn init_tracing() {
40    // RUST_LOG is tracing's own observability convention, not application config,
41    // so it is read here rather than routed through the clap argument surface.
42    tracing_subscriber::fmt()
43        .with_env_filter(
44            EnvFilter::try_from_default_env()
45                .unwrap_or_else(|_| EnvFilter::new(DEFAULT_LOG_FILTER)),
46        )
47        .with_target(false)
48        .init();
49}
50
51/// When invoked as `cargo npmgen`, cargo runs `cargo-npmgen npmgen …`; drop the
52/// injected subcommand so the same parser serves every invocation.
53fn strip_cargo_subcommand(args: impl Iterator<Item = OsString>) -> Vec<OsString> {
54    let mut args: Vec<OsString> = args.collect();
55    if args.get(1).and_then(|arg| arg.to_str()) == Some("npmgen") {
56        args.remove(1);
57    }
58    args
59}
60
61#[cfg(test)]
62mod tests {
63    use super::strip_cargo_subcommand;
64    use std::ffi::OsString;
65
66    fn argv(parts: &[&str]) -> Vec<OsString> {
67        parts.iter().map(OsString::from).collect()
68    }
69
70    #[test]
71    fn drops_cargo_injected_subcommand() {
72        let stripped =
73            strip_cargo_subcommand(argv(&["cargo-npmgen", "npmgen", "--out", "x"]).into_iter());
74        assert_eq!(stripped, argv(&["cargo-npmgen", "--out", "x"]));
75    }
76
77    #[test]
78    fn leaves_direct_invocations_untouched() {
79        let standalone = strip_cargo_subcommand(argv(&["npmgen", "--out", "x"]).into_iter());
80        assert_eq!(standalone, argv(&["npmgen", "--out", "x"]));
81
82        let direct = strip_cargo_subcommand(argv(&["cargo-npmgen", "--out", "x"]).into_iter());
83        assert_eq!(direct, argv(&["cargo-npmgen", "--out", "x"]));
84    }
85}