use super::WrappedPackage;
const ECO: &str = "npm";
const VERBS: &[&str] = &["install", "i", "add"];
pub fn parse(argv: &[&str]) -> Vec<WrappedPackage> {
let Some(verb_pos) = find_verb_pos(argv) else {
return Vec::new();
};
let mut packages = Vec::new();
let mut iter = argv.iter().skip(verb_pos + 1);
while let Some(arg) = iter.next() {
if let Some(stripped) = arg.strip_prefix("--") {
if !stripped.contains('=') && VALUE_FLAGS_LONG.contains(&stripped) {
let _ = iter.next();
}
continue;
}
if arg.starts_with('-') {
continue;
}
if arg.is_empty() {
continue;
}
let (name, version) = split_pkg_spec(arg);
packages.push(WrappedPackage::new(ECO, name, version));
}
packages
}
const VALUE_FLAGS_LONG: &[&str] = &[
"registry",
"tag",
"workspace",
"workspaces",
"prefix",
"userconfig",
"globalconfig",
];
fn find_verb_pos(argv: &[&str]) -> Option<usize> {
argv.iter().position(|a| VERBS.contains(a))
}
pub fn split_pkg_spec(spec: &str) -> (String, String) {
if let Some(rest) = spec.strip_prefix('@') {
match rest.split_once('/') {
Some((scope, after_slash)) => match after_slash.split_once('@') {
Some((name, ver)) => (format!("@{}/{}", scope, name), ver.to_string()),
None => (format!("@{}/{}", scope, after_slash), "latest".to_string()),
},
None => (spec.to_string(), "latest".to_string()),
}
} else {
match spec.rsplit_once('@') {
Some((name, ver)) if !name.is_empty() && !ver.is_empty() => {
(name.to_string(), ver.to_string())
}
_ => (spec.to_string(), "latest".to_string()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn argv<'a>(args: &[&'a str]) -> Vec<&'a str> {
let mut v: Vec<&'a str> = vec!["npm"];
v.extend_from_slice(args);
v
}
#[test]
fn parses_install_single() {
let a = argv(&["install", "lodash"]);
let pkgs = parse(&a);
assert_eq!(pkgs.len(), 1);
assert_eq!(pkgs[0], WrappedPackage::new("npm", "lodash", "latest"));
}
#[test]
fn parses_install_pinned() {
let a = argv(&["install", "lodash@4.17.21"]);
let pkgs = parse(&a);
assert_eq!(pkgs.len(), 1);
assert_eq!(pkgs[0], WrappedPackage::new("npm", "lodash", "4.17.21"));
}
#[test]
fn parses_install_scoped_unpinned() {
let a = argv(&["install", "@my-org/pkg"]);
let pkgs = parse(&a);
assert_eq!(pkgs[0], WrappedPackage::new("npm", "@my-org/pkg", "latest"));
}
#[test]
fn parses_install_scoped_pinned() {
let a = argv(&["install", "@my-org/pkg@1.2.3"]);
let pkgs = parse(&a);
assert_eq!(pkgs[0], WrappedPackage::new("npm", "@my-org/pkg", "1.2.3"));
}
#[test]
fn parses_multiple_with_flags() {
let a = argv(&["install", "--save", "-D", "cors", "express@4.18.0"]);
let pkgs = parse(&a);
assert_eq!(pkgs.len(), 2);
assert_eq!(pkgs[0].name, "cors");
assert_eq!(pkgs[1], WrappedPackage::new("npm", "express", "4.18.0"));
}
#[test]
fn i_alias_works() {
let a = argv(&["i", "left-pad"]);
assert_eq!(parse(&a)[0].name, "left-pad");
}
#[test]
fn add_alias_works() {
let a = argv(&["add", "left-pad"]);
assert_eq!(parse(&a)[0].name, "left-pad");
}
#[test]
fn skips_value_bearing_long_flag() {
let a = argv(&["install", "--registry", "https://registry.npmjs.org", "cors"]);
let pkgs = parse(&a);
assert_eq!(pkgs.len(), 1);
assert_eq!(pkgs[0].name, "cors");
}
#[test]
fn no_install_verb_returns_empty() {
let a = argv(&["test"]);
assert!(parse(&a).is_empty());
}
#[test]
fn split_handles_bare() {
assert_eq!(split_pkg_spec("lodash"), ("lodash".to_string(), "latest".to_string()));
}
}