use super::layout::package_name_from_install_path;
use super::source::local_git_source_from_resolved;
use super::*;
use crate::{DepType, DirectDep, Error, GitSource, LocalSource, LockedPackage, LockfileGraph};
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
#[test]
fn test_package_name_from_install_path() {
assert_eq!(
package_name_from_install_path("node_modules/foo"),
Some("foo".to_string())
);
assert_eq!(
package_name_from_install_path("node_modules/@scope/pkg"),
Some("@scope/pkg".to_string())
);
assert_eq!(
package_name_from_install_path("node_modules/foo/node_modules/bar"),
Some("bar".to_string())
);
assert_eq!(
package_name_from_install_path("node_modules/foo/node_modules/@scope/pkg"),
Some("@scope/pkg".to_string())
);
}
#[test]
fn test_parse_simple() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let content = r#"{
"name": "test",
"version": "1.0.0",
"lockfileVersion": 3,
"packages": {
"": {
"name": "test",
"version": "1.0.0",
"dependencies": { "foo": "^1.0.0" },
"devDependencies": { "bar": "^2.0.0" }
},
"node_modules/foo": {
"version": "1.2.3",
"integrity": "sha512-aaa",
"dependencies": { "nested": "^3.0.0" }
},
"node_modules/nested": {
"version": "3.1.0",
"integrity": "sha512-bbb"
},
"node_modules/bar": {
"version": "2.5.0",
"integrity": "sha512-ccc",
"dev": true
}
}
}"#;
std::fs::write(tmp.path(), content).unwrap();
let graph = parse(tmp.path()).unwrap();
assert_eq!(graph.packages.len(), 3);
assert!(graph.packages.contains_key("foo@1.2.3"));
assert!(graph.packages.contains_key("nested@3.1.0"));
assert!(graph.packages.contains_key("bar@2.5.0"));
let foo = &graph.packages["foo@1.2.3"];
assert_eq!(foo.integrity.as_deref(), Some("sha512-aaa"));
assert_eq!(
foo.dependencies.get("nested").map(String::as_str),
Some("3.1.0")
);
let root = graph.importers.get(".").unwrap();
assert_eq!(root.len(), 2);
assert!(
root.iter()
.any(|d| d.name == "foo" && d.dep_type == DepType::Production)
);
assert!(
root.iter()
.any(|d| d.name == "bar" && d.dep_type == DepType::Dev)
);
}
#[test]
fn test_parse_git_resolved_package() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let sha = "abcdef1234567890abcdef1234567890abcdef12";
let content = format!(
r#"{{
"name": "test",
"version": "1.0.0",
"lockfileVersion": 2,
"packages": {{
"": {{
"name": "test",
"version": "1.0.0",
"dependencies": {{ "git-only": "github:owner/repo#{sha}" }}
}},
"node_modules/git-only": {{
"version": "1.2.3",
"resolved": "git+ssh://git@github.com/owner/repo.git#{sha}",
"integrity": "sha512-aaa"
}}
}}
}}"#
);
std::fs::write(tmp.path(), content).unwrap();
let graph = parse(tmp.path()).unwrap();
let root = &graph.importers["."];
assert_eq!(root.len(), 1);
assert_eq!(root[0].name, "git-only");
assert!(!graph.packages.contains_key("git-only@1.2.3"));
let pkg = &graph.packages[&root[0].dep_path];
assert_eq!(pkg.name, "git-only");
assert_eq!(pkg.version, "1.2.3");
assert_eq!(pkg.integrity.as_deref(), Some("sha512-aaa"));
assert!(pkg.tarball_url.is_none());
let Some(LocalSource::Git(git)) = &pkg.local_source else {
panic!("expected git local source, got {:?}", pkg.local_source);
};
assert_eq!(git.url, "ssh://git@github.com/owner/repo.git");
assert_eq!(git.committish.as_deref(), Some(sha));
assert_eq!(git.resolved, sha);
}
#[test]
fn test_unpinned_git_resolved_url_is_not_locked_git_source() {
assert!(local_git_source_from_resolved("git+https://github.com/owner/repo.git").is_none());
}
#[test]
fn test_write_preserves_git_resolved_url() {
let sha = "abcdef1234567890abcdef1234567890abcdef12";
let mut graph = LockfileGraph::default();
let local = LocalSource::Git(GitSource {
url: "ssh://git@github.com/owner/repo.git".to_string(),
committish: Some(sha.to_string()),
resolved: sha.to_string(),
subpath: None,
});
let dep_path = local.dep_path("git-only");
graph.packages.insert(
dep_path.clone(),
LockedPackage {
name: "git-only".to_string(),
version: "1.2.3".to_string(),
dep_path: dep_path.clone(),
local_source: Some(local),
..Default::default()
},
);
graph.importers.insert(
".".to_string(),
vec![DirectDep {
name: "git-only".to_string(),
dep_path,
dep_type: DepType::Production,
specifier: Some(format!("github:owner/repo#{sha}")),
}],
);
let manifest = aube_manifest::PackageJson {
name: Some("test".to_string()),
version: Some("1.0.0".to_string()),
dependencies: [("git-only".to_string(), format!("github:owner/repo#{sha}"))]
.into_iter()
.collect(),
..Default::default()
};
let out = tempfile::NamedTempFile::new().unwrap();
write(out.path(), &graph, &manifest).unwrap();
let body = std::fs::read_to_string(out.path()).unwrap();
assert!(
body.contains(&format!(
"\"resolved\": \"git+ssh://git@github.com/owner/repo.git#{sha}\""
)),
"expected git resolved URL emitted; got:\n{body}"
);
let reparsed = parse(out.path()).unwrap();
let pkg = &reparsed.packages[&graph.importers["."][0].dep_path];
assert!(matches!(pkg.local_source, Some(LocalSource::Git(_))));
}
#[test]
fn test_write_skips_non_git_local_sources() {
let local = LocalSource::Directory(PathBuf::from("vendor/local-dir"));
let dep_path = local.dep_path("local-dir");
let mut graph = LockfileGraph::default();
graph.packages.insert(
dep_path.clone(),
LockedPackage {
name: "local-dir".to_string(),
version: "1.0.0".to_string(),
dep_path: dep_path.clone(),
local_source: Some(local),
..Default::default()
},
);
graph.importers.insert(
".".to_string(),
vec![DirectDep {
name: "local-dir".to_string(),
dep_path,
dep_type: DepType::Production,
specifier: Some("file:vendor/local-dir".to_string()),
}],
);
let manifest = aube_manifest::PackageJson {
name: Some("test".to_string()),
version: Some("1.0.0".to_string()),
dependencies: [("local-dir".to_string(), "file:vendor/local-dir".to_string())]
.into_iter()
.collect(),
..Default::default()
};
let out = tempfile::NamedTempFile::new().unwrap();
write(out.path(), &graph, &manifest).unwrap();
let body = std::fs::read_to_string(out.path()).unwrap();
assert!(!body.contains("\"node_modules/local-dir\""));
}
#[test]
fn test_parse_file_resolved_without_link() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let content = r#"{
"lockfileVersion": 3,
"packages": {
"": {
"dependencies": {
"tar-dep": "file:../utils/tar-dep-1.0.0.tgz",
"dir-dep": "file:../utils"
}
},
"node_modules/tar-dep": {
"version": "1.0.0",
"resolved": "file:../utils/tar-dep-1.0.0.tgz",
"integrity": "sha512-aaa"
},
"node_modules/dir-dep": {
"version": "1.0.0",
"resolved": "file:../utils"
}
}
}"#;
std::fs::write(tmp.path(), content).unwrap();
let graph = parse(tmp.path()).unwrap();
let tar_pkg = graph
.packages
.values()
.find(|p| p.name == "tar-dep")
.expect("tar-dep entry");
assert!(
matches!(&tar_pkg.local_source, Some(LocalSource::Tarball(p)) if p == Path::new("../utils/tar-dep-1.0.0.tgz")),
"expected Tarball source, got {:?}",
tar_pkg.local_source,
);
assert!(
tar_pkg.dep_path.starts_with("tar-dep@file+"),
"tarball dep_path should be local-source-keyed, got {}",
tar_pkg.dep_path,
);
let dir_pkg = graph
.packages
.values()
.find(|p| p.name == "dir-dep")
.expect("dir-dep entry");
assert!(
matches!(&dir_pkg.local_source, Some(LocalSource::Directory(p)) if p == Path::new("../utils")),
"expected Directory source, got {:?}",
dir_pkg.local_source,
);
assert!(
dir_pkg.dep_path.starts_with("dir-dep@file+"),
"directory dep_path should be local-source-keyed, got {}",
dir_pkg.dep_path,
);
let root = graph.importers.get(".").unwrap();
let tar_direct = root.iter().find(|d| d.name == "tar-dep").unwrap();
assert_eq!(tar_direct.dep_path, tar_pkg.dep_path);
let dir_direct = root.iter().find(|d| d.name == "dir-dep").unwrap();
assert_eq!(dir_direct.dep_path, dir_pkg.dep_path);
}
#[test]
fn test_parse_scoped_package() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let content = r#"{
"lockfileVersion": 3,
"packages": {
"": {
"dependencies": { "@scope/pkg": "^1.0.0" }
},
"node_modules/@scope/pkg": {
"version": "1.0.0",
"integrity": "sha512-zzz"
}
}
}"#;
std::fs::write(tmp.path(), content).unwrap();
let graph = parse(tmp.path()).unwrap();
assert!(graph.packages.contains_key("@scope/pkg@1.0.0"));
let root = graph.importers.get(".").unwrap();
assert_eq!(root[0].name, "@scope/pkg");
assert_eq!(root[0].dep_path, "@scope/pkg@1.0.0");
}
#[test]
fn test_parse_multi_version_nested() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let content = r#"{
"lockfileVersion": 3,
"packages": {
"": {
"dependencies": { "foo": "^1.0.0", "bar": "^2.0.0" }
},
"node_modules/bar": {
"version": "2.0.0",
"integrity": "sha512-top-bar"
},
"node_modules/foo": {
"version": "1.0.0",
"integrity": "sha512-foo",
"dependencies": { "bar": "^1.0.0" }
},
"node_modules/foo/node_modules/bar": {
"version": "1.0.0",
"integrity": "sha512-nested-bar"
}
}
}"#;
std::fs::write(tmp.path(), content).unwrap();
let graph = parse(tmp.path()).unwrap();
assert!(graph.packages.contains_key("bar@2.0.0"));
assert!(graph.packages.contains_key("bar@1.0.0"));
assert!(graph.packages.contains_key("foo@1.0.0"));
let foo = &graph.packages["foo@1.0.0"];
assert_eq!(
foo.dependencies.get("bar").map(String::as_str),
Some("1.0.0")
);
let root = graph.importers.get(".").unwrap();
let root_bar = root.iter().find(|d| d.name == "bar").unwrap();
assert_eq!(root_bar.dep_path, "bar@2.0.0");
}
#[test]
fn test_write_dev_and_optional_reachable_uses_dev_optional() {
let mut graph = LockfileGraph::default();
let mk = |name: &str| LockedPackage {
name: name.to_string(),
version: "1.0.0".to_string(),
integrity: Some(format!("sha512-{name}")),
dep_path: format!("{name}@1.0.0"),
dependencies: [("shared".to_string(), "1.0.0".to_string())]
.into_iter()
.collect(),
..Default::default()
};
graph
.packages
.insert("dev-root@1.0.0".to_string(), mk("dev-root"));
graph
.packages
.insert("opt-root@1.0.0".to_string(), mk("opt-root"));
graph.packages.insert(
"shared@1.0.0".to_string(),
LockedPackage {
name: "shared".to_string(),
version: "1.0.0".to_string(),
integrity: Some("sha512-shared".to_string()),
dep_path: "shared@1.0.0".to_string(),
..Default::default()
},
);
graph.importers.insert(
".".to_string(),
vec![
DirectDep {
name: "dev-root".to_string(),
dep_path: "dev-root@1.0.0".to_string(),
dep_type: DepType::Dev,
specifier: None,
},
DirectDep {
name: "opt-root".to_string(),
dep_path: "opt-root@1.0.0".to_string(),
dep_type: DepType::Optional,
specifier: None,
},
],
);
let manifest = aube_manifest::PackageJson {
name: Some("test".to_string()),
version: Some("1.0.0".to_string()),
dev_dependencies: [("dev-root".to_string(), "^1.0.0".to_string())]
.into_iter()
.collect(),
optional_dependencies: [("opt-root".to_string(), "^1.0.0".to_string())]
.into_iter()
.collect(),
..Default::default()
};
let out = tempfile::NamedTempFile::new().unwrap();
write(out.path(), &graph, &manifest).unwrap();
let json: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(out.path()).unwrap()).unwrap();
let shared = &json["packages"]["node_modules/shared"];
assert_eq!(shared["devOptional"], true, "expected devOptional flag");
assert!(
shared.get("dev").is_none(),
"must not emit dev: true alongside devOptional",
);
assert!(
shared.get("optional").is_none(),
"must not emit optional: true alongside devOptional",
);
assert_eq!(json["packages"]["node_modules/dev-root"]["dev"], true);
assert_eq!(json["packages"]["node_modules/opt-root"]["optional"], true);
}
#[test]
fn test_write_filters_missing_canonical_deps() {
let mut graph = LockfileGraph::default();
graph.packages.insert(
"foo@1.0.0".to_string(),
LockedPackage {
name: "foo".to_string(),
version: "1.0.0".to_string(),
integrity: Some("sha512-foo".to_string()),
dep_path: "foo@1.0.0".to_string(),
dependencies: [("ghost".to_string(), "1.0.0".to_string())]
.into_iter()
.collect(),
..Default::default()
},
);
graph.importers.insert(
".".to_string(),
vec![DirectDep {
name: "foo".to_string(),
dep_path: "foo@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: None,
}],
);
let manifest = test_manifest();
let out = tempfile::NamedTempFile::new().unwrap();
write(out.path(), &graph, &manifest).unwrap();
let json: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(out.path()).unwrap()).unwrap();
let foo_entry = &json["packages"]["node_modules/foo"];
assert!(
foo_entry
.get("dependencies")
.and_then(|d| d.get("ghost"))
.is_none(),
"writer emitted a ghost dep that has no packages entry: {foo_entry}",
);
assert!(
json["packages"].get("node_modules/ghost").is_none(),
"writer hallucinated a ghost entry",
);
}
#[test]
fn test_nested_shadow_forces_nested_placement() {
let mut graph = LockfileGraph::default();
let mk = |name: &str, version: &str, deps: &[(&str, &str)]| LockedPackage {
name: name.to_string(),
version: version.to_string(),
integrity: Some(format!("sha512-{name}-{version}")),
dep_path: format!("{name}@{version}"),
dependencies: deps
.iter()
.map(|(n, v)| (n.to_string(), (*v).to_string()))
.collect(),
..Default::default()
};
graph.packages.insert(
"foo@1.0.0".to_string(),
mk(
"foo",
"1.0.0",
&[
("bar", "1.0.0"),
("baz", "1.0.0"),
],
),
);
graph.packages.insert(
"baz@1.0.0".to_string(),
mk("baz", "1.0.0", &[("bar", "2.0.0")]),
);
graph
.packages
.insert("bar@1.0.0".to_string(), mk("bar", "1.0.0", &[]));
graph
.packages
.insert("bar@2.0.0".to_string(), mk("bar", "2.0.0", &[]));
graph.importers.insert(
".".to_string(),
vec![
DirectDep {
name: "foo".to_string(),
dep_path: "foo@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: None,
},
DirectDep {
name: "bar".to_string(),
dep_path: "bar@2.0.0".to_string(),
dep_type: DepType::Production,
specifier: None,
},
],
);
let manifest = test_manifest();
let out = tempfile::NamedTempFile::new().unwrap();
write(out.path(), &graph, &manifest).unwrap();
let reparsed = parse(out.path()).unwrap();
let baz = &reparsed.packages["baz@1.0.0"];
assert_eq!(
baz.dependencies.get("bar").map(String::as_str),
Some("2.0.0"),
"baz's bar dep was shadowed by foo/bar@1.0.0 — shadow-nest fix regressed",
);
}
#[test]
fn test_parse_npm_preserves_platform_optional_metadata() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let content = r#"{
"name": "platform-optional-root",
"version": "1.0.0",
"lockfileVersion": 3,
"packages": {
"": {
"name": "platform-optional-root",
"version": "1.0.0",
"dependencies": { "host": "file:host" }
},
"node_modules/host": {
"resolved": "host",
"link": true
},
"host": {
"name": "host",
"version": "1.0.0",
"optionalDependencies": { "native-win": "1.0.0" }
},
"node_modules/native-win": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/native-win/-/native-win-1.0.0.tgz",
"integrity": "sha512-native",
"optional": true,
"os": ["win32"],
"cpu": ["x64"],
"libc": ["glibc"]
}
}
}"#;
std::fs::write(tmp.path(), content).unwrap();
let graph = parse(tmp.path()).unwrap();
let host_dep_path = &graph.importers["."][0].dep_path;
let host = &graph.packages[host_dep_path];
assert_eq!(
host.dependencies.get("native-win").map(String::as_str),
Some("1.0.0")
);
assert_eq!(
host.optional_dependencies
.get("native-win")
.map(String::as_str),
Some("1.0.0")
);
let native = &graph.packages["native-win@1.0.0"];
assert_eq!(
native.os.iter().map(String::as_str).collect::<Vec<_>>(),
vec!["win32"]
);
assert_eq!(
native.cpu.iter().map(String::as_str).collect::<Vec<_>>(),
vec!["x64"]
);
assert_eq!(
native.libc.iter().map(String::as_str).collect::<Vec<_>>(),
vec!["glibc"]
);
}
#[test]
fn parse_npm_package_platform_fields_accept_scalar_strings() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let content = r#"{
"name": "scalar-platform-root",
"version": "1.0.0",
"lockfileVersion": 3,
"packages": {
"": {
"name": "scalar-platform-root",
"version": "1.0.0",
"dependencies": { "sass-embedded-linux-arm": "1.99.0" }
},
"node_modules/sass-embedded-linux-arm": {
"version": "1.99.0",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.99.0.tgz",
"integrity": "sha512-native",
"cpu": "arm",
"os": "linux",
"libc": "glibc"
}
}
}"#;
std::fs::write(tmp.path(), content).unwrap();
let graph = parse(tmp.path()).unwrap();
let pkg = &graph.packages["sass-embedded-linux-arm@1.99.0"];
assert_eq!(
pkg.os.iter().map(String::as_str).collect::<Vec<_>>(),
vec!["linux"]
);
assert_eq!(
pkg.cpu.iter().map(String::as_str).collect::<Vec<_>>(),
vec!["arm"]
);
assert_eq!(
pkg.libc.iter().map(String::as_str).collect::<Vec<_>>(),
vec!["glibc"]
);
}
#[test]
fn test_write_npm_preserves_platform_optional_metadata() {
let mut graph = LockfileGraph::default();
graph.packages.insert(
"host@1.0.0".to_string(),
LockedPackage {
name: "host".to_string(),
version: "1.0.0".to_string(),
integrity: Some("sha512-host".to_string()),
dep_path: "host@1.0.0".to_string(),
dependencies: [("native-win".to_string(), "1.0.0".to_string())]
.into_iter()
.collect(),
optional_dependencies: [("native-win".to_string(), "1.0.0".to_string())]
.into_iter()
.collect(),
declared_dependencies: [("native-win".to_string(), "1.0.0".to_string())]
.into_iter()
.collect(),
..Default::default()
},
);
graph.packages.insert(
"native-win@1.0.0".to_string(),
LockedPackage {
name: "native-win".to_string(),
version: "1.0.0".to_string(),
integrity: Some("sha512-native".to_string()),
dep_path: "native-win@1.0.0".to_string(),
os: vec!["win32".to_string()].into(),
cpu: vec!["x64".to_string()].into(),
libc: vec!["glibc".to_string()].into(),
..Default::default()
},
);
graph.importers.insert(
".".to_string(),
vec![DirectDep {
name: "host".to_string(),
dep_path: "host@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: Some("1.0.0".to_string()),
}],
);
let manifest = aube_manifest::PackageJson {
name: Some("platform-optional-root".to_string()),
version: Some("1.0.0".to_string()),
dependencies: [("host".to_string(), "1.0.0".to_string())]
.into_iter()
.collect(),
..Default::default()
};
let out = tempfile::NamedTempFile::new().unwrap();
write(out.path(), &graph, &manifest).unwrap();
let json: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(out.path()).unwrap()).unwrap();
let host = &json["packages"]["node_modules/host"];
assert_eq!(host["optionalDependencies"]["native-win"], "1.0.0");
assert!(
host.get("dependencies")
.and_then(|deps| deps.get("native-win"))
.is_none(),
"optional child must not be duplicated as a required dependency: {host}",
);
let native = &json["packages"]["node_modules/native-win"];
assert_eq!(native["os"], serde_json::json!(["win32"]));
assert_eq!(native["cpu"], serde_json::json!(["x64"]));
assert_eq!(native["libc"], serde_json::json!(["glibc"]));
let reparsed = parse(out.path()).unwrap();
let host = &reparsed.packages["host@1.0.0"];
assert_eq!(
host.optional_dependencies
.get("native-win")
.map(String::as_str),
Some("1.0.0")
);
assert_eq!(
host.dependencies.get("native-win").map(String::as_str),
Some("1.0.0")
);
let native = &reparsed.packages["native-win@1.0.0"];
assert_eq!(
native.os.iter().map(String::as_str).collect::<Vec<_>>(),
vec!["win32"]
);
assert_eq!(
native.cpu.iter().map(String::as_str).collect::<Vec<_>>(),
vec!["x64"]
);
assert_eq!(
native.libc.iter().map(String::as_str).collect::<Vec<_>>(),
vec!["glibc"]
);
}
#[test]
fn test_canonical_key_strips_peer_suffix() {
assert_eq!(canonical_key_from_dep_path("foo@1.0.0"), "foo@1.0.0");
assert_eq!(
canonical_key_from_dep_path("styled-components@6.1.0(react@18.2.0)"),
"styled-components@6.1.0"
);
assert_eq!(
canonical_key_from_dep_path("@scope/pkg@2.0.0(peer@1.0.0)"),
"@scope/pkg@2.0.0"
);
assert_eq!(
canonical_key_from_dep_path("expo-router@4.0.22_94c00fd028"),
"expo-router@4.0.22"
);
assert_eq!(
child_canonical_key("expo-router", "4.0.22_94c00fd028"),
"expo-router@4.0.22"
);
assert_eq!(
dep_value_as_version("expo-router", "expo-router@4.0.22_94c00fd028"),
"4.0.22"
);
assert_eq!(
canonical_key_from_dep_path("expo-router@4.0.22_94C00FD028"),
"expo-router@4.0.22"
);
}
fn test_manifest() -> aube_manifest::PackageJson {
aube_manifest::PackageJson {
name: Some("test".to_string()),
version: Some("1.0.0".to_string()),
dependencies: [
("foo".to_string(), "^1.0.0".to_string()),
("bar".to_string(), "^2.0.0".to_string()),
]
.into_iter()
.collect(),
..Default::default()
}
}
#[test]
fn test_write_roundtrip_multi_version() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let content = r#"{
"name": "test",
"version": "1.0.0",
"lockfileVersion": 3,
"packages": {
"": {
"name": "test",
"version": "1.0.0",
"dependencies": { "foo": "^1.0.0", "bar": "^2.0.0" }
},
"node_modules/bar": {
"version": "2.0.0",
"integrity": "sha512-top-bar"
},
"node_modules/foo": {
"version": "1.0.0",
"integrity": "sha512-foo",
"dependencies": { "bar": "^1.0.0" }
},
"node_modules/foo/node_modules/bar": {
"version": "1.0.0",
"integrity": "sha512-nested-bar"
}
}
}"#;
std::fs::write(tmp.path(), content).unwrap();
let graph = parse(tmp.path()).unwrap();
let manifest = test_manifest();
let out = tempfile::NamedTempFile::new().unwrap();
write(out.path(), &graph, &manifest).unwrap();
let reparsed = parse(out.path()).unwrap();
assert!(reparsed.packages.contains_key("bar@1.0.0"));
assert!(reparsed.packages.contains_key("bar@2.0.0"));
assert!(reparsed.packages.contains_key("foo@1.0.0"));
assert_eq!(
reparsed.packages["bar@2.0.0"].integrity.as_deref(),
Some("sha512-top-bar")
);
assert_eq!(
reparsed.packages["bar@1.0.0"].integrity.as_deref(),
Some("sha512-nested-bar")
);
assert_eq!(
reparsed.packages["foo@1.0.0"]
.dependencies
.get("bar")
.map(String::as_str),
Some("1.0.0")
);
}
#[test]
fn test_write_dev_optional_flags() {
let mut graph = LockfileGraph::default();
graph.packages.insert(
"foo@1.0.0".to_string(),
LockedPackage {
name: "foo".to_string(),
version: "1.0.0".to_string(),
integrity: Some("sha512-foo".to_string()),
dep_path: "foo@1.0.0".to_string(),
..Default::default()
},
);
graph.packages.insert(
"devdep@1.0.0".to_string(),
LockedPackage {
name: "devdep".to_string(),
version: "1.0.0".to_string(),
integrity: Some("sha512-dev".to_string()),
dep_path: "devdep@1.0.0".to_string(),
..Default::default()
},
);
graph.importers.insert(
".".to_string(),
vec![
DirectDep {
name: "foo".to_string(),
dep_path: "foo@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: None,
},
DirectDep {
name: "devdep".to_string(),
dep_path: "devdep@1.0.0".to_string(),
dep_type: DepType::Dev,
specifier: None,
},
],
);
let manifest = aube_manifest::PackageJson {
name: Some("test".to_string()),
version: Some("1.0.0".to_string()),
dependencies: [("foo".to_string(), "^1.0.0".to_string())]
.into_iter()
.collect(),
dev_dependencies: [("devdep".to_string(), "^1.0.0".to_string())]
.into_iter()
.collect(),
..Default::default()
};
let out = tempfile::NamedTempFile::new().unwrap();
write(out.path(), &graph, &manifest).unwrap();
let json: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(out.path()).unwrap()).unwrap();
let packages = &json["packages"];
assert_eq!(packages["node_modules/devdep"]["dev"], true);
assert!(packages["node_modules/foo"].get("dev").is_none());
}
#[test]
fn test_reject_v1() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let content = r#"{
"name": "test",
"lockfileVersion": 1,
"dependencies": {}
}"#;
std::fs::write(tmp.path(), content).unwrap();
let err = parse(tmp.path()).unwrap_err();
assert!(matches!(err, Error::Parse(_, msg) if msg.contains("lockfileVersion 1")));
}
#[test]
fn test_parse_legacy_array_engines() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let content = r#"{
"name": "test",
"version": "1.0.0",
"lockfileVersion": 3,
"packages": {
"": {
"name": "test",
"version": "1.0.0",
"dependencies": { "ansi-html-community": "0.0.8" }
},
"node_modules/ansi-html-community": {
"version": "0.0.8",
"integrity": "sha512-aaa",
"engines": ["node >= 0.8.0"]
}
}
}"#;
std::fs::write(tmp.path(), content).unwrap();
let graph = parse(tmp.path()).unwrap();
let pkg = &graph.packages["ansi-html-community@0.0.8"];
assert!(pkg.engines.is_empty());
}
#[test]
fn test_parse_npm_alias_dependency() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let content = r#"{
"name": "test",
"version": "1.0.0",
"lockfileVersion": 3,
"packages": {
"": {
"name": "test",
"version": "1.0.0",
"dependencies": { "h3-v2": "npm:h3@2.0.1-rc.20" }
},
"node_modules/h3-v2": {
"name": "h3",
"version": "2.0.1-rc.20",
"resolved": "https://registry.npmjs.org/h3/-/h3-2.0.1-rc.20.tgz",
"integrity": "sha512-aliased"
}
}
}"#;
std::fs::write(tmp.path(), content).unwrap();
let graph = parse(tmp.path()).unwrap();
assert_eq!(graph.packages.len(), 1);
let pkg = graph
.packages
.get("h3-v2@2.0.1-rc.20")
.expect("aliased entry should be keyed by the alias dep_path");
assert_eq!(pkg.name, "h3-v2");
assert_eq!(pkg.version, "2.0.1-rc.20");
assert_eq!(pkg.alias_of.as_deref(), Some("h3"));
assert_eq!(pkg.registry_name(), "h3");
assert_eq!(
pkg.tarball_url.as_deref(),
Some("https://registry.npmjs.org/h3/-/h3-2.0.1-rc.20.tgz")
);
let root = graph.importers.get(".").unwrap();
assert_eq!(root.len(), 1);
assert_eq!(root[0].name, "h3-v2");
assert_eq!(root[0].dep_path, "h3-v2@2.0.1-rc.20");
}
#[test]
fn test_parse_non_alias_preserves_empty_alias_of() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let content = r#"{
"name": "test",
"version": "1.0.0",
"lockfileVersion": 3,
"packages": {
"": {
"name": "test",
"version": "1.0.0",
"dependencies": { "foo": "^1.0.0" }
},
"node_modules/foo": {
"name": "foo",
"version": "1.2.3",
"integrity": "sha512-foo"
}
}
}"#;
std::fs::write(tmp.path(), content).unwrap();
let graph = parse(tmp.path()).unwrap();
let pkg = &graph.packages["foo@1.2.3"];
assert_eq!(pkg.name, "foo");
assert!(pkg.alias_of.is_none());
assert_eq!(pkg.registry_name(), "foo");
assert!(pkg.tarball_url.is_none());
}
#[test]
fn test_write_roundtrip_npm_alias() {
let mut graph = LockfileGraph::default();
graph.packages.insert(
"h3-v2@2.0.1-rc.20".to_string(),
LockedPackage {
name: "h3-v2".to_string(),
version: "2.0.1-rc.20".to_string(),
integrity: Some("sha512-aliased".to_string()),
dep_path: "h3-v2@2.0.1-rc.20".to_string(),
alias_of: Some("h3".to_string()),
tarball_url: Some("https://registry.npmjs.org/h3/-/h3-2.0.1-rc.20.tgz".to_string()),
..Default::default()
},
);
graph.importers.insert(
".".to_string(),
vec![DirectDep {
name: "h3-v2".to_string(),
dep_path: "h3-v2@2.0.1-rc.20".to_string(),
dep_type: DepType::Production,
specifier: Some("npm:h3@2.0.1-rc.20".to_string()),
}],
);
let manifest = test_manifest();
let out = tempfile::NamedTempFile::new().unwrap();
write(out.path(), &graph, &manifest).unwrap();
let body = std::fs::read_to_string(out.path()).unwrap();
assert!(
body.contains("\"name\": \"h3\""),
"expected `name: h3` emitted for aliased entry; got:\n{body}"
);
assert!(
body.contains("\"resolved\": \"https://registry.npmjs.org/h3/-/h3-2.0.1-rc.20.tgz\""),
"expected `resolved:` URL emitted for aliased entry; got:\n{body}"
);
let reparsed = parse(out.path()).unwrap();
let pkg = &reparsed.packages["h3-v2@2.0.1-rc.20"];
assert_eq!(pkg.alias_of.as_deref(), Some("h3"));
assert_eq!(pkg.registry_name(), "h3");
}
#[test]
fn test_parse_peer_dependencies() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let content = r#"{
"name": "peer-test",
"version": "1.0.0",
"lockfileVersion": 3,
"packages": {
"": {
"name": "peer-test",
"version": "1.0.0",
"dependencies": { "devtools-vite": "0.6.0", "vite": "8.0.0" }
},
"node_modules/devtools-vite": {
"version": "0.6.0",
"integrity": "sha512-a",
"peerDependencies": {
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
},
"peerDependenciesMeta": {
"vite": { "optional": false }
}
},
"node_modules/vite": {
"version": "8.0.0",
"integrity": "sha512-b"
}
}
}"#;
std::fs::write(tmp.path(), content).unwrap();
let graph = parse(tmp.path()).unwrap();
let devtools = &graph.packages["devtools-vite@0.6.0"];
assert_eq!(
devtools.peer_dependencies.get("vite").map(String::as_str),
Some("^6.0.0 || ^7.0.0 || ^8.0.0")
);
assert_eq!(
devtools
.peer_dependencies_meta
.get("vite")
.map(|m| m.optional),
Some(false)
);
}
#[test]
fn test_parse_no_peer_fields_stays_empty() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let content = r#"{
"name": "no-peers",
"version": "1.0.0",
"lockfileVersion": 3,
"packages": {
"": { "name": "no-peers", "version": "1.0.0", "dependencies": { "foo": "1.0.0" } },
"node_modules/foo": { "version": "1.0.0", "integrity": "sha512-x" }
}
}"#;
std::fs::write(tmp.path(), content).unwrap();
let graph = parse(tmp.path()).unwrap();
let foo = &graph.packages["foo@1.0.0"];
assert!(foo.peer_dependencies.is_empty());
assert!(foo.peer_dependencies_meta.is_empty());
}
#[test]
fn test_write_roundtrip_peer_dependencies() {
let mut graph = LockfileGraph::default();
let mut peer_deps = BTreeMap::new();
peer_deps.insert("vite".to_string(), "^6.0.0 || ^7.0.0 || ^8.0.0".to_string());
let mut peer_deps_meta = BTreeMap::new();
peer_deps_meta.insert("vite".to_string(), crate::PeerDepMeta { optional: true });
graph.packages.insert(
"devtools-vite@0.6.0".to_string(),
LockedPackage {
name: "devtools-vite".to_string(),
version: "0.6.0".to_string(),
integrity: Some("sha512-a".to_string()),
dep_path: "devtools-vite@0.6.0".to_string(),
peer_dependencies: peer_deps,
peer_dependencies_meta: peer_deps_meta,
..Default::default()
},
);
graph.packages.insert(
"vite@8.0.0".to_string(),
LockedPackage {
name: "vite".to_string(),
version: "8.0.0".to_string(),
integrity: Some("sha512-b".to_string()),
dep_path: "vite@8.0.0".to_string(),
..Default::default()
},
);
graph.importers.insert(
".".to_string(),
vec![
DirectDep {
name: "devtools-vite".to_string(),
dep_path: "devtools-vite@0.6.0".to_string(),
dep_type: DepType::Production,
specifier: None,
},
DirectDep {
name: "vite".to_string(),
dep_path: "vite@8.0.0".to_string(),
dep_type: DepType::Production,
specifier: None,
},
],
);
let manifest = test_manifest();
let out = tempfile::NamedTempFile::new().unwrap();
write(out.path(), &graph, &manifest).unwrap();
let body = std::fs::read_to_string(out.path()).unwrap();
assert!(
body.contains("\"peerDependencies\""),
"expected peerDependencies block to round-trip; got:\n{body}"
);
assert!(
body.contains("\"peerDependenciesMeta\""),
"expected peerDependenciesMeta block to round-trip; got:\n{body}"
);
let reparsed = parse(out.path()).unwrap();
let devtools = &reparsed.packages["devtools-vite@0.6.0"];
assert_eq!(
devtools.peer_dependencies.get("vite").map(String::as_str),
Some("^6.0.0 || ^7.0.0 || ^8.0.0")
);
assert_eq!(
devtools
.peer_dependencies_meta
.get("vite")
.map(|m| m.optional),
Some(true),
"peerDependenciesMeta.optional must survive write → parse round-trip"
);
}
#[test]
fn test_parse_npm_workspace_importers() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let content = r#"{
"name": "workspace-root",
"version": "1.0.0",
"lockfileVersion": 3,
"packages": {
"": {
"name": "workspace-root",
"version": "1.0.0",
"workspaces": ["web"]
},
"node_modules/mise-versions-web": {
"resolved": "web",
"link": true
},
"web": {
"name": "mise-versions-web",
"version": "0.0.1",
"dependencies": { "astro": "^6.0.0" },
"devDependencies": { "vite": "^7.3.2" }
},
"web/node_modules/astro": {
"version": "6.2.1",
"integrity": "sha512-astro"
},
"web/node_modules/vite": {
"version": "7.3.2",
"integrity": "sha512-vite"
}
}
}"#;
std::fs::write(tmp.path(), content).unwrap();
let graph = parse(tmp.path()).unwrap();
let root = graph.importers.get(".").expect("root importer");
assert_eq!(root.len(), 1);
assert_eq!(root[0].name, "mise-versions-web");
assert!(matches!(
graph.packages[&root[0].dep_path].local_source,
Some(LocalSource::Link(_))
));
let web = graph.importers.get("web").expect("web importer");
assert_eq!(web.len(), 2);
assert!(web.iter().any(|dep| {
dep.name == "astro"
&& dep.dep_type == DepType::Production
&& dep.specifier.as_deref() == Some("^6.0.0")
}));
assert!(web.iter().any(|dep| {
dep.name == "vite"
&& dep.dep_type == DepType::Dev
&& dep.specifier.as_deref() == Some("^7.3.2")
}));
}
#[test]
fn test_write_npm_workspace_importers() {
let mut graph = LockfileGraph::default();
let web_link = LocalSource::Link(PathBuf::from("web"));
let web_dep_path = web_link.dep_path("mise-versions-web");
graph.packages.insert(
web_dep_path.clone(),
LockedPackage {
name: "mise-versions-web".to_string(),
version: "0.0.1".to_string(),
dep_path: web_dep_path.clone(),
local_source: Some(web_link),
..Default::default()
},
);
graph.packages.insert(
"astro@6.2.1".to_string(),
LockedPackage {
name: "astro".to_string(),
version: "6.2.1".to_string(),
integrity: Some("sha512-astro".to_string()),
dep_path: "astro@6.2.1".to_string(),
..Default::default()
},
);
graph.packages.insert(
"vite@7.3.2".to_string(),
LockedPackage {
name: "vite".to_string(),
version: "7.3.2".to_string(),
integrity: Some("sha512-vite".to_string()),
dep_path: "vite@7.3.2".to_string(),
..Default::default()
},
);
graph.importers.insert(
".".to_string(),
vec![DirectDep {
name: "mise-versions-web".to_string(),
dep_path: web_dep_path.clone(),
dep_type: DepType::Production,
specifier: None,
}],
);
graph.importers.insert(
"web".to_string(),
vec![
DirectDep {
name: "astro".to_string(),
dep_path: "astro@6.2.1".to_string(),
dep_type: DepType::Production,
specifier: Some("^6.0.0".to_string()),
},
DirectDep {
name: "vite".to_string(),
dep_path: "vite@7.3.2".to_string(),
dep_type: DepType::Dev,
specifier: Some("^7.3.2".to_string()),
},
],
);
let manifest = aube_manifest::PackageJson {
name: Some("workspace-root".to_string()),
version: Some("1.0.0".to_string()),
..Default::default()
};
let out = tempfile::NamedTempFile::new().unwrap();
write(out.path(), &graph, &manifest).unwrap();
let json: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(out.path()).unwrap()).unwrap();
assert_eq!(
json["packages"]["node_modules/mise-versions-web"]["link"],
true
);
assert_eq!(
json["packages"]["node_modules/mise-versions-web"]["resolved"],
"web"
);
assert_eq!(json["packages"]["web"]["dependencies"]["astro"], "^6.0.0");
assert_eq!(json["packages"]["web"]["devDependencies"]["vite"], "^7.3.2");
assert_eq!(
json["packages"]["web/node_modules/astro"]["version"],
"6.2.1"
);
assert_eq!(
json["packages"]["web/node_modules/vite"]["version"],
"7.3.2"
);
let reparsed = parse(out.path()).unwrap();
assert!(reparsed.importers.contains_key("web"));
}
#[test]
fn test_write_npm_workspace_skips_root_hoisted_dups() {
let mut graph = LockfileGraph::default();
let web_link = LocalSource::Link(PathBuf::from("web"));
let web_dep_path = web_link.dep_path("workspace-web");
graph.packages.insert(
web_dep_path.clone(),
LockedPackage {
name: "workspace-web".to_string(),
version: "0.0.1".to_string(),
dep_path: web_dep_path.clone(),
local_source: Some(web_link),
..Default::default()
},
);
graph.packages.insert(
"astro@6.2.1".to_string(),
LockedPackage {
name: "astro".to_string(),
version: "6.2.1".to_string(),
integrity: Some("sha512-astro".to_string()),
dep_path: "astro@6.2.1".to_string(),
..Default::default()
},
);
graph.importers.insert(
".".to_string(),
vec![
DirectDep {
name: "astro".to_string(),
dep_path: "astro@6.2.1".to_string(),
dep_type: DepType::Production,
specifier: Some("^6.0.0".to_string()),
},
DirectDep {
name: "workspace-web".to_string(),
dep_path: web_dep_path.clone(),
dep_type: DepType::Production,
specifier: None,
},
],
);
graph.importers.insert(
"web".to_string(),
vec![DirectDep {
name: "astro".to_string(),
dep_path: "astro@6.2.1".to_string(),
dep_type: DepType::Production,
specifier: Some("^6.0.0".to_string()),
}],
);
let manifest = aube_manifest::PackageJson {
name: Some("workspace-root".to_string()),
version: Some("1.0.0".to_string()),
dependencies: [("astro".to_string(), "^6.0.0".to_string())]
.into_iter()
.collect(),
..Default::default()
};
let out = tempfile::NamedTempFile::new().unwrap();
write(out.path(), &graph, &manifest).unwrap();
let json: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(out.path()).unwrap()).unwrap();
assert_eq!(json["packages"]["node_modules/astro"]["version"], "6.2.1");
assert!(
json["packages"].get("web/node_modules/astro").is_none(),
"redundant workspace-nested astro should not be emitted"
);
}
#[test]
fn test_write_byte_identical_to_native_npm() {
let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/npm-native.json");
let original = std::fs::read_to_string(&fixture)
.unwrap()
.replace("\r\n", "\n");
let graph = parse(&fixture).unwrap();
let manifest = aube_manifest::PackageJson {
name: Some("aube-lockfile-stability".to_string()),
version: Some("1.0.0".to_string()),
dependencies: [
("chalk".to_string(), "^4.1.2".to_string()),
("picocolors".to_string(), "^1.1.1".to_string()),
("semver".to_string(), "^7.6.3".to_string()),
]
.into_iter()
.collect(),
..Default::default()
};
let tmp = tempfile::NamedTempFile::new().unwrap();
write(tmp.path(), &graph, &manifest).unwrap();
let written = std::fs::read_to_string(tmp.path()).unwrap();
if written != original {
panic!(
"npm writer drifted from native npm output.\n\n--- expected ---\n{original}\n--- got ---\n{written}"
);
}
}
#[test]
fn test_parse_workspace_links() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let content = r#"{
"name": "workspace-root",
"version": "1.0.0",
"lockfileVersion": 3,
"packages": {
"": {
"name": "workspace-root",
"version": "1.0.0",
"dependencies": { "@scope/app": "file:packages/app" }
},
"node_modules/@scope/app": {
"resolved": "packages/app",
"link": true
},
"node_modules/chalk": {
"version": "5.4.1",
"integrity": "sha512-chalk"
},
"packages/app": {
"name": "@scope/app",
"version": "0.68.1",
"dependencies": {
"chalk": "^5.4.1"
}
}
}
}"#;
std::fs::write(tmp.path(), content).unwrap();
let graph = parse(tmp.path()).unwrap();
let dep_path = LocalSource::Link(PathBuf::from("packages/app")).dep_path("@scope/app");
let importer = &graph.importers["."];
assert_eq!(importer.len(), 1);
assert_eq!(importer[0].name, "@scope/app");
assert_eq!(importer[0].dep_path, dep_path);
assert!(matches!(importer[0].dep_type, DepType::Production));
assert!(importer[0].specifier.is_none());
let app = &graph.packages[&importer[0].dep_path];
assert_eq!(app.version, "0.68.1");
assert_eq!(
app.local_source,
Some(LocalSource::Link(PathBuf::from("packages/app")))
);
assert_eq!(
app.dependencies.get("chalk").map(String::as_str),
Some("5.4.1")
);
assert!(!graph.packages.contains_key("@scope/app@0.68.1"));
}
#[test]
fn test_parse_workspace_links_undeclared_in_root_deps() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let content = r#"{
"name": "workspace-root",
"version": "1.0.0",
"lockfileVersion": 3,
"packages": {
"": {
"name": "workspace-root",
"version": "1.0.0",
"workspaces": ["projects/element-ng", "projects/charts-ng"],
"dependencies": { "chalk": "^5.4.1" }
},
"node_modules/@siemens/element-ng": {
"resolved": "projects/element-ng",
"link": true
},
"node_modules/@siemens/charts-ng": {
"resolved": "projects/charts-ng",
"link": true
},
"node_modules/chalk": {
"version": "5.4.1",
"integrity": "sha512-chalk"
},
"projects/element-ng": {
"name": "@siemens/element-ng",
"version": "21.0.0"
},
"projects/charts-ng": {
"name": "@siemens/charts-ng",
"version": "21.0.0"
}
}
}"#;
std::fs::write(tmp.path(), content).unwrap();
let graph = parse(tmp.path()).unwrap();
let importer = &graph.importers["."];
let names: Vec<&str> = importer.iter().map(|d| d.name.as_str()).collect();
assert!(names.contains(&"chalk"));
assert!(
names.contains(&"@siemens/element-ng"),
"workspace package `@siemens/element-ng` should be a direct dep of root \
so the linker creates `node_modules/@siemens/element-ng`, even though \
the root manifest doesn't list it; got importer deps {names:?}"
);
assert!(
names.contains(&"@siemens/charts-ng"),
"workspace package `@siemens/charts-ng` should be a direct dep of root; \
got importer deps {names:?}"
);
let element_ng = importer
.iter()
.find(|d| d.name == "@siemens/element-ng")
.unwrap();
assert_eq!(
graph.packages[&element_ng.dep_path].local_source,
Some(LocalSource::Link(PathBuf::from("projects/element-ng")))
);
}
#[test]
fn test_parse_funding_all_shapes() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let content = r#"{
"name": "test",
"version": "1.0.0",
"lockfileVersion": 3,
"packages": {
"": {
"name": "test",
"version": "1.0.0",
"dependencies": {
"string-funding": "1.0.0",
"object-funding": "1.0.0",
"array-funding": "1.0.0",
"mixed-array-funding": "1.0.0",
"no-funding": "1.0.0"
}
},
"node_modules/string-funding": {
"version": "1.0.0",
"integrity": "sha512-aaa",
"funding": "https://example.com/sponsor"
},
"node_modules/object-funding": {
"version": "1.0.0",
"integrity": "sha512-bbb",
"funding": { "type": "github", "url": "https://github.com/sponsors/foo" }
},
"node_modules/array-funding": {
"version": "1.0.0",
"integrity": "sha512-ccc",
"funding": [
{ "type": "github", "url": "https://github.com/sponsors/csstools" },
{ "type": "opencollective", "url": "https://opencollective.com/csstools" }
]
},
"node_modules/mixed-array-funding": {
"version": "1.0.0",
"integrity": "sha512-ddd",
"funding": [
"https://github.com/fb55/htmlparser2?sponsor=1",
{ "type": "github", "url": "https://github.com/sponsors/fb55" }
]
},
"node_modules/no-funding": {
"version": "1.0.0",
"integrity": "sha512-eee"
}
}
}"#;
std::fs::write(tmp.path(), content).unwrap();
let graph = parse(tmp.path()).unwrap();
assert_eq!(
graph.packages["string-funding@1.0.0"]
.funding_url
.as_deref(),
Some("https://example.com/sponsor"),
);
assert_eq!(
graph.packages["object-funding@1.0.0"]
.funding_url
.as_deref(),
Some("https://github.com/sponsors/foo"),
);
assert_eq!(
graph.packages["array-funding@1.0.0"].funding_url.as_deref(),
Some("https://github.com/sponsors/csstools"),
);
assert_eq!(
graph.packages["mixed-array-funding@1.0.0"]
.funding_url
.as_deref(),
Some("https://github.com/fb55/htmlparser2?sponsor=1"),
);
assert!(graph.packages["no-funding@1.0.0"].funding_url.is_none());
}
#[test]
fn test_parse_license_all_shapes() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let content = r#"{
"name": "test",
"version": "1.0.0",
"lockfileVersion": 3,
"packages": {
"": {
"name": "test",
"version": "1.0.0",
"dependencies": {
"string-license": "1.0.0",
"object-license": "1.0.0",
"array-license": "1.0.0",
"mixed-array-license": "1.0.0",
"no-license": "1.0.0"
}
},
"node_modules/string-license": {
"version": "1.0.0",
"integrity": "sha512-aaa",
"license": "MIT"
},
"node_modules/object-license": {
"version": "1.0.0",
"integrity": "sha512-bbb",
"license": { "type": "ISC", "url": "https://example.com/ISC" }
},
"node_modules/array-license": {
"version": "1.0.0",
"integrity": "sha512-ccc",
"license": [
{ "type": "Public Domain", "url": "http://geraintluff.github.io/tv4/LICENSE.txt" },
{ "type": "MIT", "url": "http://jsonary.com/LICENSE.txt" }
]
},
"node_modules/mixed-array-license": {
"version": "1.0.0",
"integrity": "sha512-ddd",
"license": [
"MIT",
{ "type": "Apache-2.0", "url": "https://example.com/apache" }
]
},
"node_modules/no-license": {
"version": "1.0.0",
"integrity": "sha512-eee"
}
}
}"#;
std::fs::write(tmp.path(), content).unwrap();
let graph = parse(tmp.path()).unwrap();
assert_eq!(
graph.packages["string-license@1.0.0"].license.as_deref(),
Some("MIT"),
);
assert_eq!(
graph.packages["object-license@1.0.0"].license.as_deref(),
Some("ISC"),
);
assert_eq!(
graph.packages["array-license@1.0.0"].license.as_deref(),
Some("Public Domain"),
);
assert_eq!(
graph.packages["mixed-array-license@1.0.0"]
.license
.as_deref(),
Some("MIT"),
);
assert!(graph.packages["no-license@1.0.0"].license.is_none());
}