extern crate atty;
extern crate clap;
extern crate regex;
use clap::{App, Arg, ArgMatches, SubCommand};
use regex::Regex;
use std::error::Error;
use std::{env, fs, io, process};
fn check_lifecycle() {
let arg_string = env::args()
.skip(1)
.fold(String::new(), |mut arg_string, arg| {
arg_string.push_str(" ");
arg_string.push_str(&arg);
arg_string
});
let install_lifecycles = ["install".to_owned(), "postinstall".to_owned()];
match env::var("npm_lifecycle_event") {
Ok(value) => {
if install_lifecycles.contains(&value) {
println!(
"
Hey! it looks like you're running lockspot in the \"postinstall\" or \"install\"
hook! this can create some confusion, because the package-lock may not have been
generated yet!
to prevent this, you can use the \"postshrinkwrap\" lifecycle hook instead, like
this:
\"scripts\": {{
\"postshrinkwrap\": \"lockspot{}\"
}}
See this issue for further info: https://github.com/npm/npm/issues/18798",
arg_string
);
}
}
_ => {}
}
}
fn get_lockfile(filename: &str) -> Result<lockspot::PackageLock, Box<dyn Error>> {
let piping = !atty::is(atty::Stream::Stdin);
let lock: lockspot::PackageLock = if piping || filename == "-" {
serde_json::from_reader(io::stdin())?
} else {
let file = fs::File::open(filename)?;
serde_json::from_reader(file)?
};
return Ok(lock);
}
fn try_regex(regex: &str) -> Option<Regex> {
match Regex::new(regex) {
Ok(r) => Some(r),
_ => None,
}
}
fn parse_flat_options(args: ArgMatches) -> lockspot::Options {
if args.is_present("origami") {
let origami_regex = Regex::new(r"@financial-times/o-.*").unwrap();
lockspot::Options {
patterns: vec![Some(origami_regex)],
production: true,
min: None,
sort: None,
}
} else {
let patterns: lockspot::Patterns = match args.values_of("pattern") {
Some(pattern) => pattern.map(try_regex).collect::<lockspot::Patterns>(),
None => vec![],
};
let production = args.is_present("production");
lockspot::Options {
patterns,
production,
min: None,
sort: None,
}
}
}
fn flat(args: ArgMatches) -> Result<(), Box<dyn Error>> {
let filename = match args.value_of("file") {
Some(name) => name,
None => "package-lock.json",
};
let options = parse_flat_options(args.to_owned());
let lock = get_lockfile(filename)?;
if !lockspot::validate(&lock) {
panic!("not a valid lockfile! (only lockfile with `version: 1` is supported)");
}
if !lockspot::check_for_flatness(lock, options) {
process::exit(1)
} else {
process::exit(0)
}
}
fn parse_depcount_options(args: ArgMatches) -> lockspot::Options {
let patterns: lockspot::Patterns = match args.values_of("pattern") {
Some(pattern) => pattern.map(try_regex).collect::<lockspot::Patterns>(),
None => vec![],
};
let sort: lockspot::SortType = match args.value_of("sort") {
Some("dont") => lockspot::SortType::Dont,
Some("name") => lockspot::SortType::Name,
Some("count") => lockspot::SortType::Count,
Some(_) => panic!("got an invalid sort, somehow"),
None => lockspot::SortType::Dont,
};
let min: Option<usize> = match args.value_of("min") {
Some(number) => match number.parse::<usize>() {
Ok(number) => Some(number),
Err(_) => None,
},
None => None,
};
let production = args.is_present("production");
lockspot::Options {
patterns,
production,
min,
sort: Some(sort),
}
}
fn depcount(args: ArgMatches) -> Result<(), Box<dyn Error>> {
let filename = match args.value_of("file") {
Some(name) => name,
None => "package-lock.json",
};
let options = parse_depcount_options(args.to_owned());
let lock = get_lockfile(filename)?;
if !lockspot::validate(&lock) {
panic!("not a valid lockfile! (only lockfile with `version: 1` is supported)");
}
println!("{}", lockspot::get_depcount(lock, options));
Ok(())
}
fn get_args() -> clap::App<'static, 'static> {
App::new("lockspot")
.version("0.0.0")
.author("chee <chee@snoot.club>")
.about("let's ask package-lock")
.subcommands(vec![
SubCommand::with_name("flat")
.about("exit badly if the tree cannot be flat")
.args(&vec![
Arg::with_name("origami").long("origami"),
Arg::with_name("pattern")
.help("only count packages whose name match one of these patterns")
.long("pattern")
.short("r")
.multiple(true)
.takes_value(true),
Arg::with_name("production")
.help("only count the production (non-dev) tree")
.long("production")
.short("p")
.alias("prod"),
Arg::with_name("file")
.help("the package-lock.json to operate on")
.long("file")
.takes_value(true)
])])
.subcommands(vec![
SubCommand::with_name("depcount")
.about("count the number of different versions of each dependency in the tree")
.args(&vec![
Arg::with_name("min")
.help("only print dependencies that have more than this number of versions in the tree")
.long("min")
.default_value("1")
.takes_value(true),
Arg::with_name("production")
.help("only count the production (non-dev) tree")
.long("production")
.short("p")
.alias("prod"),
Arg::with_name("pattern")
.help("only count packages whose name match one of these patterns")
.long("pattern")
.short("r")
.multiple(true)
.takes_value(true),
Arg::with_name("sort")
.help("how to sort the dependencies. `dont` is the default.")
.long("sort")
.possible_values(&["dont", "count", "name"])
.takes_value(true)
.default_value("dont"),
Arg::with_name("file")
.help("the package-lock.json to operate on")
.long("file")
.takes_value(true)
])
])
}
fn main() -> Result<(), Box<dyn Error>> {
check_lifecycle();
let args = get_args().get_matches();
if let Some(subcommand) = args.subcommand {
let name = subcommand.name.as_str();
match name {
"depcount" => depcount(subcommand.matches)?,
"flat" => flat(subcommand.matches)?,
_ => {}
}
} else {
get_args().print_help()?
}
Ok(())
}