use super::{jsonc::strip_jsonc, parse, raw::is_integrity_hash, source::split_ident, write};
use crate::{DepType, DirectDep, LocalSource, LockedPackage, LockfileGraph};
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
#[test]
fn test_split_ident() {
assert_eq!(
split_ident("foo@1.2.3"),
Some(("foo".to_string(), "1.2.3".to_string()))
);
assert_eq!(
split_ident("@scope/pkg@1.0.0"),
Some(("@scope/pkg".to_string(), "1.0.0".to_string()))
);
}
#[test]
fn test_is_integrity_hash() {
assert!(is_integrity_hash(&format!("sha512-{}", "A".repeat(88))));
assert!(is_integrity_hash(&format!("sha256-{}", "A".repeat(44))));
assert!(is_integrity_hash(&format!("sha1-{}", "A".repeat(28))));
let mixed = format!("{}+/==", "A".repeat(84));
assert_eq!(mixed.len(), 88);
assert!(is_integrity_hash(&format!("sha512-{mixed}")));
assert!(!is_integrity_hash("sha1-myrepo-abc123"));
assert!(!is_integrity_hash("sha256-owner-repo-deadbee"));
assert!(!is_integrity_hash("foo-bar"));
assert!(!is_integrity_hash("sha512-tooshort"));
let with_dash = format!("sha512-{}-{}", "A".repeat(43), "A".repeat(44));
assert_eq!(with_dash.len(), "sha512-".len() + 88);
assert!(!is_integrity_hash(&with_dash));
assert!(!is_integrity_hash("opaquestring"));
}
#[test]
fn test_strip_jsonc_trailing_comma() {
let input = r#"{ "a": 1, "b": 2, }"#;
let out = strip_jsonc(input);
let v: serde_json::Value = serde_json::from_str(&out).unwrap();
assert_eq!(v["a"], 1);
assert_eq!(v["b"], 2);
}
#[test]
fn test_strip_jsonc_line_comment() {
let input = "{ // comment\n \"a\": 1 }";
let out = strip_jsonc(input);
let v: serde_json::Value = serde_json::from_str(&out).unwrap();
assert_eq!(v["a"], 1);
}
#[test]
fn test_strip_jsonc_respects_strings() {
let input = r#"{ "url": "http://example.com/path" }"#;
let out = strip_jsonc(input);
let v: serde_json::Value = serde_json::from_str(&out).unwrap();
assert_eq!(v["url"], "http://example.com/path");
}
#[test]
fn strip_jsonc_preserves_utf8_string_value() {
let input = "{ \"name\": \"café\" }";
let out = strip_jsonc(input);
assert_eq!(out.len(), input.len());
let v: serde_json::Value = serde_json::from_str(&out).unwrap();
assert_eq!(v["name"], "café");
}
#[test]
fn strip_jsonc_preserves_offsets_for_nonascii_in_comments() {
let input = "{ // café\n \"a\": 1 }";
let out = strip_jsonc(input);
assert_eq!(out.len(), input.len());
}
#[test]
fn test_strip_jsonc_preserves_byte_offsets() {
let cases = [
"{ \"a\": 1 }", "{ // line\n \"a\": 1 }", "{ /* block */ \"a\": 1 }", "{ /* multi\nline */ \"a\": 1 }", "{ \"a\": 1, \"b\": 2, }", "{ \"a\": \"// not a comment\" }", "{ \"a\": 1 /* trailing", ];
for input in cases {
let out = strip_jsonc(input);
assert_eq!(
out.len(),
input.len(),
"length mismatch stripping {input:?} -> {out:?}"
);
let raw_nls: Vec<usize> = input.match_indices('\n').map(|(i, _)| i).collect();
let out_nls: Vec<usize> = out.match_indices('\n').map(|(i, _)| i).collect();
assert_eq!(raw_nls, out_nls, "newline drift stripping {input:?}");
}
}
fn fake_sri(tag: char) -> String {
format!("sha512-{}", tag.to_string().repeat(88))
}
#[test]
fn test_parse_simple() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let sri_foo = fake_sri('a');
let sri_nested = fake_sri('b');
let sri_bar = fake_sri('c');
let content = r#"{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "test",
"dependencies": {
"foo": "^1.0.0",
},
"devDependencies": {
"bar": "^2.0.0",
},
},
},
"packages": {
"foo": ["foo@1.2.3", "", { "dependencies": { "nested": "^3.0.0" } }, "SRI_FOO"],
"nested": ["nested@3.1.0", "", {}, "SRI_NESTED"],
"bar": ["bar@2.5.0", "", {}, "SRI_BAR"],
}
}"#
.replace("SRI_FOO", &sri_foo)
.replace("SRI_NESTED", &sri_nested)
.replace("SRI_BAR", &sri_bar);
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(sri_foo.as_str()));
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_bun_lifecycle_deps_as_dep_path_tails() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let sri_bufferutil = fake_sri('a');
let sri_node_gyp_build = fake_sri('b');
let sri_electron = fake_sri('c');
let sri_electron_get = fake_sri('d');
let content = r#"{
"lockfileVersion": 1,
"workspaces": {
"": {
"dependencies": {
"bufferutil": "4.0.9",
"electron": "39.2.7"
}
}
},
"packages": {
"bufferutil": ["bufferutil@4.0.9", "", { "dependencies": { "node-gyp-build": "^4.3.0" } }, "SRI_BUFFERUTIL"],
"node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js" } }, "SRI_NODE_GYP_BUILD"],
"electron": ["electron@39.2.7", "", { "dependencies": { "@electron/get": "^2.0.0" } }, "SRI_ELECTRON"],
"@electron/get": ["@electron/get@2.0.3", "", {}, "SRI_ELECTRON_GET"]
}
}"#
.replace("SRI_BUFFERUTIL", &sri_bufferutil)
.replace("SRI_NODE_GYP_BUILD", &sri_node_gyp_build)
.replace("SRI_ELECTRON", &sri_electron)
.replace("SRI_ELECTRON_GET", &sri_electron_get);
std::fs::write(tmp.path(), &content).unwrap();
let graph = parse(tmp.path()).unwrap();
let bufferutil = &graph.packages["bufferutil@4.0.9"];
assert_eq!(
bufferutil
.dependencies
.get("node-gyp-build")
.map(String::as_str),
Some("4.8.4")
);
let electron = &graph.packages["electron@39.2.7"];
assert_eq!(
electron
.dependencies
.get("@electron/get")
.map(String::as_str),
Some("2.0.3")
);
let root = graph.importers.get(".").unwrap();
assert!(
root.iter()
.any(|d| d.name == "bufferutil" && d.dep_path == "bufferutil@4.0.9")
);
assert!(
root.iter()
.any(|d| d.name == "electron" && d.dep_path == "electron@39.2.7")
);
}
#[test]
fn test_parse_multi_version_nested() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let sri_top_bar = fake_sri('a');
let sri_foo = fake_sri('b');
let sri_nested_bar = fake_sri('c');
let content = r#"{
"lockfileVersion": 1,
"workspaces": {
"": {
"dependencies": { "foo": "^1.0.0", "bar": "^2.0.0" }
}
},
"packages": {
"bar": ["bar@2.0.0", "", {}, "SRI_TOP_BAR"],
"foo": ["foo@1.0.0", "", { "dependencies": { "bar": "^1.0.0" } }, "SRI_FOO"],
"foo/bar": ["bar@1.0.0", "", {}, "SRI_NESTED_BAR"]
}
}"#
.replace("SRI_TOP_BAR", &sri_top_bar)
.replace("SRI_FOO", &sri_foo)
.replace("SRI_NESTED_BAR", &sri_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 bar = root.iter().find(|d| d.name == "bar").unwrap();
assert_eq!(bar.dep_path, "bar@2.0.0");
}
#[test]
fn test_parse_scoped() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let sri = fake_sri('s');
let content = r#"{
"lockfileVersion": 1,
"workspaces": {
"": {
"dependencies": { "@scope/pkg": "^1.0.0" }
}
},
"packages": {
"@scope/pkg": ["@scope/pkg@1.0.0", "", {}, "SRI"]
}
}"#
.replace("SRI", &sri);
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");
}
#[test]
fn test_parse_github_dep() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let sri_dep = fake_sri('d');
let content = r#"{
"lockfileVersion": 1,
"workspaces": {
"": {
"dependencies": { "vfs": "github:collinstevens/vfs#0b6ea53" }
}
},
"packages": {
"vfs": ["vfs@github:collinstevens/vfs#0b6ea53abcdef", { "dependencies": { "dep": "^1.0.0" } }, "collinstevens-vfs-0b6ea53"],
"dep": ["dep@1.0.0", "", {}, "SRI_DEP"]
}
}"#
.replace("SRI_DEP", &sri_dep);
std::fs::write(tmp.path(), &content).unwrap();
let graph = parse(tmp.path()).unwrap();
let vfs_key = "vfs@github:collinstevens/vfs#0b6ea53abcdef";
assert!(graph.packages.contains_key(vfs_key));
let vfs = &graph.packages[vfs_key];
assert_eq!(
vfs.dependencies.get("dep").map(String::as_str),
Some("1.0.0")
);
assert!(vfs.integrity.is_none());
let dep = &graph.packages["dep@1.0.0"];
assert_eq!(dep.integrity.as_deref(), Some(sri_dep.as_str()));
let root = graph.importers.get(".").unwrap();
assert!(root.iter().any(|d| d.name == "vfs"));
}
#[test]
fn test_parse_prefixless_local_tarball() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let sri = fake_sri('t');
let content = r#"{
"lockfileVersion": 1,
"workspaces": {
"": {
"dependencies": { "local-helper": "file:tarballs/local-helper-1.0.0.tgz" }
}
},
"packages": {
"local-helper": ["local-helper@tarballs/local-helper-1.0.0.tgz", {}, "SRI"]
}
}"#
.replace("SRI", &sri);
std::fs::write(tmp.path(), &content).unwrap();
let graph = parse(tmp.path()).unwrap();
let pkg = &graph.packages["local-helper@tarballs/local-helper-1.0.0.tgz"];
assert!(
matches!(pkg.local_source, Some(LocalSource::Tarball(_))),
"prefixless bun tarball ident must be LocalSource::Tarball, got {:?}",
pkg.local_source
);
}
#[test]
fn test_write_roundtrip_multi_version() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let sri_top = fake_sri('t');
let sri_foo = fake_sri('f');
let sri_nested = fake_sri('n');
let content = r#"{
"lockfileVersion": 1,
"workspaces": {
"": {
"dependencies": { "foo": "^1.0.0", "bar": "^2.0.0" }
}
},
"packages": {
"bar": ["bar@2.0.0", "", {}, "SRI_TOP"],
"foo": ["foo@1.0.0", "", { "dependencies": { "bar": "^1.0.0" } }, "SRI_FOO"],
"foo/bar": ["bar@1.0.0", "", {}, "SRI_NESTED"]
}
}"#
.replace("SRI_TOP", &sri_top)
.replace("SRI_FOO", &sri_foo)
.replace("SRI_NESTED", &sri_nested);
std::fs::write(tmp.path(), &content).unwrap();
let graph = parse(tmp.path()).unwrap();
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()),
("bar".to_string(), "^2.0.0".to_string()),
]
.into_iter()
.collect(),
..Default::default()
};
let out = tempfile::NamedTempFile::new().unwrap();
write(out.path(), &graph, &manifest).unwrap();
let reparsed = parse(out.path()).unwrap();
assert!(reparsed.packages.contains_key("bar@2.0.0"));
assert!(reparsed.packages.contains_key("bar@1.0.0"));
assert!(reparsed.packages.contains_key("foo@1.0.0"));
assert_eq!(
reparsed.packages["bar@2.0.0"].integrity.as_deref(),
Some(sri_top.as_str())
);
assert_eq!(
reparsed.packages["bar@1.0.0"].integrity.as_deref(),
Some(sri_nested.as_str())
);
assert_eq!(
reparsed.packages["foo@1.0.0"]
.dependencies
.get("bar")
.map(String::as_str),
Some("1.0.0")
);
}
#[test]
fn test_write_byte_identical_to_native_bun() {
let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/bun-native.lock");
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!(
"bun writer drifted from native bun output.\n\n--- expected ---\n{original}\n--- got ---\n{written}"
);
}
}
#[test]
fn test_write_roundtrips_config_version() {
let project = tempfile::TempDir::new().unwrap();
let pj = project.path().join("package.json");
std::fs::write(&pj, r#"{"name":"root","dependencies":{}}"#).unwrap();
let lock_path = project.path().join("bun.lock");
std::fs::write(
&lock_path,
r#"{
"lockfileVersion": 1,
"configVersion": 42,
"workspaces": {
"": { "name": "root" }
},
"packages": {}
}"#,
)
.unwrap();
let graph = parse(&lock_path).unwrap();
assert_eq!(graph.bun_config_version, Some(42));
let manifest = aube_manifest::PackageJson::from_path(&pj).unwrap();
write(&lock_path, &graph, &manifest).unwrap();
let written = std::fs::read_to_string(&lock_path).unwrap();
assert!(
written.contains("\"configVersion\": 42,"),
"configVersion must round-trip verbatim, got:\n{written}"
);
}
#[test]
fn test_parse_and_write_multi_workspace() {
use tempfile::TempDir;
let sri_foo = fake_sri('a');
let sri_bar = fake_sri('b');
let project = TempDir::new().unwrap();
let project_dir = project.path();
std::fs::write(
project_dir.join("package.json"),
r#"{"name":"root","version":"1.0.0","dependencies":{"foo":"^1.0.0"}}"#,
)
.unwrap();
std::fs::create_dir_all(project_dir.join("packages/app")).unwrap();
std::fs::write(
project_dir.join("packages/app/package.json"),
r#"{"name":"app","version":"2.0.0","dependencies":{"bar":"^3.0.0"}}"#,
)
.unwrap();
let lock_path = project_dir.join("bun.lock");
let content = r#"{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "root",
"version": "1.0.0",
"dependencies": { "foo": "^1.0.0" }
},
"packages/app": {
"name": "app",
"version": "2.0.0",
"dependencies": { "bar": "^3.0.0" }
}
},
"packages": {
"foo": ["foo@1.2.3", "", {}, "SRI_FOO"],
"bar": ["bar@3.1.0", "", {}, "SRI_BAR"]
}
}"#
.replace("SRI_FOO", &sri_foo)
.replace("SRI_BAR", &sri_bar);
std::fs::write(&lock_path, content).unwrap();
let graph = parse(&lock_path).unwrap();
let root = graph.importers.get(".").expect("root importer");
assert_eq!(root.len(), 1);
assert_eq!(root[0].name, "foo");
assert_eq!(root[0].dep_path, "foo@1.2.3");
let app = graph
.importers
.get("packages/app")
.expect("packages/app importer");
assert_eq!(app.len(), 1);
assert_eq!(app[0].name, "bar");
assert_eq!(app[0].dep_path, "bar@3.1.0");
let manifest =
aube_manifest::PackageJson::from_path(&project_dir.join("package.json")).unwrap();
std::fs::remove_file(&lock_path).unwrap();
write(&lock_path, &graph, &manifest).unwrap();
let reparsed = parse(&lock_path).unwrap();
assert!(reparsed.importers.contains_key("."));
assert!(reparsed.importers.contains_key("packages/app"));
let app = &reparsed.importers["packages/app"];
assert_eq!(app.len(), 1);
assert_eq!(app[0].name, "bar");
assert_eq!(app[0].dep_path, "bar@3.1.0");
let raw = std::fs::read_to_string(&lock_path).unwrap();
assert!(raw.contains("\"packages/app\""));
assert!(raw.contains("\"name\": \"app\""));
}
#[test]
fn test_write_workspace_entry_carries_version_bin_and_optional_peers() {
use tempfile::TempDir;
let project = TempDir::new().unwrap();
let project_dir = project.path();
std::fs::write(
project_dir.join("package.json"),
r#"{"name":"root","version":"1.0.0"}"#,
)
.unwrap();
std::fs::create_dir_all(project_dir.join("packages/drifti")).unwrap();
std::fs::write(
project_dir.join("packages/drifti/package.json"),
r#"{
"name": "@redact/drifti",
"version": "0.0.1",
"bin": { "drifti": "./dist/cli/bin.mjs" },
"peerDependencies": {
"@electric-sql/pglite": "*",
"kysely": "*"
},
"peerDependenciesMeta": {
"kysely": { "optional": true },
"@electric-sql/pglite": { "optional": true },
"not-optional": { "optional": false }
}
}"#,
)
.unwrap();
let mut importers = BTreeMap::new();
importers.insert(".".to_string(), vec![]);
importers.insert("packages/drifti".to_string(), vec![]);
let graph = LockfileGraph {
importers,
..Default::default()
};
let manifest =
aube_manifest::PackageJson::from_path(&project_dir.join("package.json")).unwrap();
let lock_path = project_dir.join("bun.lock");
write(&lock_path, &graph, &manifest).unwrap();
let raw = std::fs::read_to_string(&lock_path).unwrap();
let v: serde_json::Value = serde_json::from_str(&strip_jsonc(&raw)).unwrap();
let drifti = &v["workspaces"]["packages/drifti"];
assert_eq!(drifti["name"], "@redact/drifti");
assert_eq!(drifti["version"], "0.0.1");
assert_eq!(drifti["bin"]["drifti"], "./dist/cli/bin.mjs");
let optional_peers: Vec<&str> = drifti["optionalPeers"]
.as_array()
.unwrap()
.iter()
.map(|x| x.as_str().unwrap())
.collect();
assert_eq!(optional_peers, vec!["@electric-sql/pglite", "kysely"]);
assert!(
raw.contains(r#""bin": { "drifti": "./dist/cli/bin.mjs" },"#),
"bin rendered multi-line or unexpected shape:\n{raw}"
);
let root = &v["workspaces"][""];
assert!(
root.get("version").is_none(),
"root carried version: {root}"
);
assert!(root.get("bin").is_none(), "root carried bin: {root}");
assert!(
root.get("optionalPeers").is_none(),
"root carried optionalPeers: {root}"
);
}
#[test]
fn test_write_emits_workspace_link_packages() {
use crate::LocalSource;
use std::path::PathBuf;
let tmp_dir = tempfile::TempDir::new().unwrap();
let project_dir = tmp_dir.path();
std::fs::write(
project_dir.join("package.json"),
r#"{"name":"root","version":"1.0.0"}"#,
)
.unwrap();
std::fs::create_dir_all(project_dir.join("packages/app")).unwrap();
std::fs::write(
project_dir.join("packages/app/package.json"),
r#"{"name":"my-app","version":"0.1.0"}"#,
)
.unwrap();
let mut packages = BTreeMap::new();
packages.insert(
"my-app@0.1.0".to_string(),
LockedPackage {
name: "my-app".to_string(),
version: "0.1.0".to_string(),
dep_path: "my-app@0.1.0".to_string(),
local_source: Some(LocalSource::Link(PathBuf::from("packages/app"))),
..Default::default()
},
);
let mut importers = BTreeMap::new();
importers.insert(".".to_string(), vec![]);
importers.insert("packages/app".to_string(), vec![]);
let graph = LockfileGraph {
importers,
packages,
..Default::default()
};
let manifest =
aube_manifest::PackageJson::from_path(&project_dir.join("package.json")).unwrap();
let lock_path = project_dir.join("bun.lock");
write(&lock_path, &graph, &manifest).unwrap();
let raw = std::fs::read_to_string(&lock_path).unwrap();
let v: serde_json::Value = serde_json::from_str(&strip_jsonc(&raw)).unwrap();
let pkgs = v["packages"].as_object().unwrap();
let entry = pkgs
.get("my-app")
.expect("workspace-link package missing from `packages`");
let arr = entry.as_array().expect("entry must be a JSON array");
assert_eq!(arr.len(), 1, "no-deps workspace entry must be `[ident]`");
assert_eq!(arr[0].as_str(), Some("my-app@workspace:packages/app"));
let ws = v["workspaces"].as_object().unwrap();
assert!(ws.contains_key("packages/app"));
}
#[test]
fn test_write_preserves_workspace_to_workspace_dep_edge() {
use crate::LocalSource;
use std::path::PathBuf;
use tempfile::TempDir;
let project = TempDir::new().unwrap();
let project_dir = project.path();
std::fs::write(
project_dir.join("package.json"),
r#"{"name":"root","version":"1.0.0"}"#,
)
.unwrap();
std::fs::create_dir_all(project_dir.join("packages/app")).unwrap();
std::fs::create_dir_all(project_dir.join("packages/lib")).unwrap();
std::fs::write(
project_dir.join("packages/app/package.json"),
r#"{"name":"app","version":"0.1.0","dependencies":{"lib":"workspace:*"}}"#,
)
.unwrap();
std::fs::write(
project_dir.join("packages/lib/package.json"),
r#"{"name":"lib","version":"0.1.0"}"#,
)
.unwrap();
let mut packages = BTreeMap::new();
packages.insert(
"app@workspace:packages/app".to_string(),
LockedPackage {
name: "app".to_string(),
version: "workspace:packages/app".to_string(),
dep_path: "app@workspace:packages/app".to_string(),
local_source: Some(LocalSource::Link(PathBuf::from("packages/app"))),
dependencies: [("lib".to_string(), "workspace:packages/lib".to_string())].into(),
declared_dependencies: [("lib".to_string(), "workspace:*".to_string())].into(),
..Default::default()
},
);
packages.insert(
"lib@workspace:packages/lib".to_string(),
LockedPackage {
name: "lib".to_string(),
version: "workspace:packages/lib".to_string(),
dep_path: "lib@workspace:packages/lib".to_string(),
local_source: Some(LocalSource::Link(PathBuf::from("packages/lib"))),
..Default::default()
},
);
let mut importers = BTreeMap::new();
importers.insert(".".to_string(), vec![]);
importers.insert("packages/app".to_string(), vec![]);
importers.insert("packages/lib".to_string(), vec![]);
let graph = LockfileGraph {
importers,
packages,
..Default::default()
};
let manifest =
aube_manifest::PackageJson::from_path(&project_dir.join("package.json")).unwrap();
let lock_path = project_dir.join("bun.lock");
write(&lock_path, &graph, &manifest).unwrap();
let raw = std::fs::read_to_string(&lock_path).unwrap();
let v: serde_json::Value = serde_json::from_str(&strip_jsonc(&raw)).unwrap();
let app_entry = v["packages"]["app"].as_array().unwrap();
assert_eq!(
app_entry.len(),
2,
"workspace entry with deps must be `[ident, {{ meta }}]`"
);
assert_eq!(app_entry[0].as_str(), Some("app@workspace:packages/app"));
assert_eq!(
app_entry[1]["dependencies"]["lib"].as_str(),
Some("workspace:*"),
"workspace-to-workspace dep edge dropped"
);
}
#[test]
fn test_roundtrip_workspace_entry_in_packages_section() {
use tempfile::TempDir;
let project = TempDir::new().unwrap();
let project_dir = project.path();
std::fs::write(
project_dir.join("package.json"),
r#"{"name":"root","version":"1.0.0"}"#,
)
.unwrap();
std::fs::create_dir_all(project_dir.join("packages/app")).unwrap();
std::fs::write(
project_dir.join("packages/app/package.json"),
r#"{"name":"app","version":"0.1.0"}"#,
)
.unwrap();
let lock_path = project_dir.join("bun.lock");
let content = r#"{
"lockfileVersion": 1,
"workspaces": {
"": { "name": "root", "version": "1.0.0" },
"packages/app": { "name": "app", "version": "0.1.0" }
},
"packages": {
"app": ["app@workspace:packages/app"]
}
}"#;
std::fs::write(&lock_path, content).unwrap();
let graph = parse(&lock_path).unwrap();
let manifest =
aube_manifest::PackageJson::from_path(&project_dir.join("package.json")).unwrap();
std::fs::remove_file(&lock_path).unwrap();
write(&lock_path, &graph, &manifest).unwrap();
let raw = std::fs::read_to_string(&lock_path).unwrap();
let v: serde_json::Value = serde_json::from_str(&strip_jsonc(&raw)).unwrap();
let arr = v["packages"]["app"]
.as_array()
.expect("workspace entry survived as array");
assert_eq!(arr.len(), 1);
assert_eq!(arr[0].as_str(), Some("app@workspace:packages/app"));
}
#[test]
fn test_write_dedupes_duplicate_direct_deps_across_workspaces() {
use tempfile::TempDir;
let project = TempDir::new().unwrap();
let project_dir = project.path();
std::fs::write(
project_dir.join("package.json"),
r#"{"name":"root","dependencies":{"foo":"^1.0.0"}}"#,
)
.unwrap();
std::fs::create_dir_all(project_dir.join("packages/app")).unwrap();
std::fs::write(
project_dir.join("packages/app/package.json"),
r#"{"name":"app","dependencies":{"foo":"^2.0.0"}}"#,
)
.unwrap();
let mut packages = BTreeMap::new();
packages.insert(
"foo@1.0.0".to_string(),
LockedPackage {
name: "foo".to_string(),
version: "1.0.0".to_string(),
dep_path: "foo@1.0.0".to_string(),
dependencies: [("bar".to_string(), "2.0.0".to_string())]
.into_iter()
.collect(),
..Default::default()
},
);
packages.insert(
"foo@2.0.0".to_string(),
LockedPackage {
name: "foo".to_string(),
version: "2.0.0".to_string(),
dep_path: "foo@2.0.0".to_string(),
..Default::default()
},
);
packages.insert(
"bar@2.0.0".to_string(),
LockedPackage {
name: "bar".to_string(),
version: "2.0.0".to_string(),
dep_path: "bar@2.0.0".to_string(),
..Default::default()
},
);
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
vec![DirectDep {
name: "foo".to_string(),
dep_path: "foo@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: None,
}],
);
importers.insert(
"packages/app".to_string(),
vec![DirectDep {
name: "foo".to_string(),
dep_path: "foo@2.0.0".to_string(),
dep_type: DepType::Production,
specifier: None,
}],
);
let graph = LockfileGraph {
importers,
packages,
..Default::default()
};
let manifest =
aube_manifest::PackageJson::from_path(&project_dir.join("package.json")).unwrap();
let lock_path = project_dir.join("bun.lock");
write(&lock_path, &graph, &manifest).unwrap();
let reparsed = parse(&lock_path).unwrap();
let foo = reparsed.packages.get("foo@1.0.0").expect("foo@1.0.0");
assert_eq!(foo.version, "1.0.0");
assert!(
reparsed.packages.contains_key("bar@2.0.0"),
"root foo's transitive `bar` was dropped: {:?}",
reparsed.packages.keys().collect::<Vec<_>>()
);
}
#[test]
fn test_parse_workspace_path_does_not_alias_npm_package() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let sri = fake_sri('a');
let content = r#"{
"lockfileVersion": 1,
"workspaces": {
"": { "dependencies": { "packages": "^1.0.0" } },
"packages/app": {
"name": "app",
"dependencies": { "bar": "^1.0.0" }
}
},
"packages": {
"bar": ["bar@1.0.0", "", {}, "SRI"],
"packages": ["packages@1.0.0", "", { "dependencies": { "bar": "^9.0.0" } }, "SRI"],
"packages/bar": ["bar@9.9.9", "", {}, "SRI"]
}
}"#
.replace("SRI", &sri);
std::fs::write(tmp.path(), &content).unwrap();
let graph = parse(tmp.path()).unwrap();
let app = graph
.importers
.get("packages/app")
.expect("packages/app importer");
let bar = app.iter().find(|d| d.name == "bar").expect("bar dep");
assert_eq!(
bar.dep_path, "bar@1.0.0",
"workspace `bar` must resolve to hoisted 1.0.0, not packages/bar@9.9.9"
);
}
#[test]
fn test_parse_workspace_dep_prefers_workspace_name_scope() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let sri = fake_sri('a');
let content = r#"{
"lockfileVersion": 1,
"workspaces": {
"": { "name": "root" },
"packages/a-other": {
"name": "a-other",
"dependencies": { "tslib": "2.8.1" }
},
"packages/z-app": {
"name": "z-app",
"dependencies": { "tslib": "2.4.0" }
}
},
"packages": {
"a-other": ["a-other@workspace:packages/a-other"],
"tslib": ["tslib@2.8.1", "", {}, "SRI"],
"z-app": ["z-app@workspace:packages/z-app"],
"z-app/tslib": ["tslib@2.4.0", "", {}, "SRI"]
}
}"#
.replace("SRI", &sri);
std::fs::write(tmp.path(), &content).unwrap();
let graph = parse(tmp.path()).unwrap();
let other = graph
.importers
.get("packages/a-other")
.expect("packages/a-other importer");
let hoisted_tslib = other.iter().find(|d| d.name == "tslib").expect("tslib dep");
assert_eq!(
hoisted_tslib.dep_path, "tslib@2.8.1",
"sibling workspace must still resolve to the hoisted tslib"
);
let app = graph
.importers
.get("packages/z-app")
.expect("packages/z-app importer");
let tslib = app.iter().find(|d| d.name == "tslib").expect("tslib dep");
assert_eq!(
tslib.dep_path, "tslib@2.4.0",
"workspace dep must resolve to z-app/tslib, not hoisted tslib"
);
}
#[test]
fn test_parse_rebases_workspace_scoped_local_tarball() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let sri = fake_sri('a');
let content = r#"{
"lockfileVersion": 1,
"workspaces": {
"": { "name": "root" },
"packages/app": {
"name": "app",
"dependencies": { "local-tar": "file:../../vendor/local-tar-1.0.0.tgz" }
}
},
"packages": {
"app": ["app@workspace:packages/app"],
"app/local-tar": ["local-tar@../../vendor/local-tar-1.0.0.tgz", {}, "SRI"]
}
}"#
.replace("SRI", &sri);
std::fs::write(tmp.path(), &content).unwrap();
let graph = parse(tmp.path()).unwrap();
let local_tar = graph
.packages
.values()
.find(|p| p.name == "local-tar")
.expect("local-tar package");
assert_eq!(local_tar.version, "../../vendor/local-tar-1.0.0.tgz");
assert_eq!(
local_tar.local_source,
Some(LocalSource::Tarball(PathBuf::from(
"vendor/local-tar-1.0.0.tgz"
)))
);
}
#[test]
fn test_roundtrip_top_level_metadata() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let content = r#"{
"lockfileVersion": 1,
"workspaces": {
"": { "name": "root" }
},
"overrides": {
"lodash": "^4.17.21",
"lodash>debug": "^4.0.0"
},
"patchedDependencies": {
"lodash@4.17.21": "patches/lodash@4.17.21.patch"
},
"trustedDependencies": ["sharp", "esbuild"],
"catalog": {
"react": "^18.2.0"
},
"catalogs": {
"evens": { "date-fns": "^2.30.0" }
},
"packages": {}
}"#;
std::fs::write(tmp.path(), content).unwrap();
let graph = parse(tmp.path()).unwrap();
assert_eq!(
graph.overrides.get("lodash").map(String::as_str),
Some("^4.17.21")
);
assert_eq!(
graph.overrides.get("lodash>debug").map(String::as_str),
Some("^4.0.0")
);
assert_eq!(
graph
.patched_dependencies
.get("lodash@4.17.21")
.map(String::as_str),
Some("patches/lodash@4.17.21.patch")
);
assert_eq!(
graph.trusted_dependencies,
vec!["sharp".to_string(), "esbuild".to_string()],
"trustedDependencies must preserve bun's original order on parse"
);
assert_eq!(graph.catalogs["default"]["react"].specifier, "^18.2.0");
assert_eq!(graph.catalogs["evens"]["date-fns"].specifier, "^2.30.0");
let manifest = aube_manifest::PackageJson {
name: Some("root".to_string()),
..Default::default()
};
let out = tempfile::NamedTempFile::new().unwrap();
write(out.path(), &graph, &manifest).unwrap();
let written = std::fs::read_to_string(out.path()).unwrap();
assert!(
written.contains("\"overrides\""),
"overrides dropped:\n{written}"
);
assert!(
written.contains("\"patchedDependencies\""),
"patchedDependencies dropped:\n{written}"
);
assert!(
written.contains("\"trustedDependencies\""),
"trustedDependencies dropped:\n{written}"
);
let sharp_at = written
.find("\"sharp\"")
.expect("sharp in trustedDependencies");
let esbuild_at = written
.find("\"esbuild\"")
.expect("esbuild in trustedDependencies");
assert!(
sharp_at < esbuild_at,
"trustedDependencies reordered on write — expected sharp before esbuild:\n{written}"
);
assert!(
written.contains("\"catalog\""),
"catalog dropped:\n{written}"
);
assert!(
written.contains("\"catalogs\""),
"catalogs dropped:\n{written}"
);
let reparsed = parse(out.path()).unwrap();
assert_eq!(reparsed.overrides, graph.overrides);
assert_eq!(reparsed.patched_dependencies, graph.patched_dependencies);
assert_eq!(reparsed.trusted_dependencies, graph.trusted_dependencies);
assert_eq!(reparsed.catalogs["default"]["react"].specifier, "^18.2.0");
}
#[test]
fn test_parse_routes_non_registry_specs_to_localsource() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let content = r#"{
"lockfileVersion": 1,
"workspaces": {
"": {
"dependencies": {
"vfs": "github:collinstevens/vfs#0b6ea53",
"localdir": "file:./vendor/localdir",
"localtgz": "file:./vendor/thing.tgz",
"sibling": "link:../sibling",
"remote": "https://example.com/thing.tgz"
}
}
},
"packages": {
"vfs": ["vfs@github:collinstevens/vfs#0b6ea53abcdef", {}, "collinstevens-vfs-0b6ea53abcdef"],
"localdir": ["localdir@file:./vendor/localdir", {}],
"localtgz": ["localtgz@file:./vendor/thing.tgz", {}],
"sibling": ["sibling@link:../sibling", {}],
"remote": ["remote@https://example.com/thing.tgz", {}]
}
}"#;
std::fs::write(tmp.path(), content).unwrap();
let graph = parse(tmp.path()).unwrap();
let vfs = graph
.packages
.values()
.find(|p| p.name == "vfs")
.expect("vfs package");
assert!(
matches!(vfs.local_source, Some(LocalSource::Git(_))),
"github dep must be LocalSource::Git, got {:?}",
vfs.local_source
);
let localdir = graph
.packages
.values()
.find(|p| p.name == "localdir")
.expect("localdir package");
assert!(
matches!(localdir.local_source, Some(LocalSource::Directory(_))),
"file:./dir must be LocalSource::Directory, got {:?}",
localdir.local_source
);
let localtgz = graph
.packages
.values()
.find(|p| p.name == "localtgz")
.expect("localtgz package");
assert!(
matches!(localtgz.local_source, Some(LocalSource::Tarball(_))),
"file:./*.tgz must be LocalSource::Tarball, got {:?}",
localtgz.local_source
);
let sibling = graph
.packages
.values()
.find(|p| p.name == "sibling")
.expect("sibling package");
assert!(
matches!(sibling.local_source, Some(LocalSource::Link(_))),
"link: must be LocalSource::Link, got {:?}",
sibling.local_source
);
let remote = graph
.packages
.values()
.find(|p| p.name == "remote")
.expect("remote package");
assert!(
matches!(remote.local_source, Some(LocalSource::RemoteTarball(_))),
"https://*.tgz must be LocalSource::RemoteTarball, got {:?}",
remote.local_source
);
}
#[test]
fn test_parse_bun_workspace_package_path_as_link_target() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let content = r#"{
"lockfileVersion": 1,
"workspaces": {
"": { "name": "root" },
"packages/app": {
"name": "app",
"dependencies": { "lib": "workspace:*" }
},
"packages/lib": { "name": "lib" }
},
"packages": {
"app": ["app@workspace:packages/app"],
"lib": ["lib@workspace:packages/lib"]
}
}"#;
std::fs::write(tmp.path(), content).unwrap();
let graph = parse(tmp.path()).unwrap();
let lib = graph.packages.get("lib@workspace:packages/lib").unwrap();
assert_eq!(
lib.local_source.as_ref().and_then(LocalSource::path),
Some(Path::new("packages/lib"))
);
let app_deps = graph.importers.get("packages/app").unwrap();
assert_eq!(app_deps[0].dep_path, "lib@workspace:packages/lib");
}
#[test]
fn test_parse_and_write_npm_alias() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let sri = fake_sri('a');
let content = r#"{
"lockfileVersion": 1,
"workspaces": {
"": { "dependencies": { "h3-v2": "npm:h3@2.0.1" } }
},
"packages": {
"h3-v2": ["h3@2.0.1", "", {}, "SRI"]
}
}"#
.replace("SRI", &sri);
std::fs::write(tmp.path(), &content).unwrap();
let graph = parse(tmp.path()).unwrap();
let h3 = graph
.packages
.values()
.find(|p| p.name == "h3-v2")
.expect("h3-v2 package");
assert_eq!(h3.alias_of.as_deref(), Some("h3"));
assert_eq!(h3.version, "2.0.1");
let manifest = aube_manifest::PackageJson {
name: Some("root".to_string()),
dependencies: [("h3-v2".to_string(), "npm:h3@2.0.1".to_string())]
.into_iter()
.collect(),
..Default::default()
};
let out = tempfile::NamedTempFile::new().unwrap();
write(out.path(), &graph, &manifest).unwrap();
let written = std::fs::read_to_string(out.path()).unwrap();
assert!(
written.contains("\"h3@2.0.1\""),
"expected ident `h3@2.0.1`, got:\n{written}"
);
assert!(
!written.contains("\"h3-v2@2.0.1\""),
"alias-name ident leaked into packages entry:\n{written}"
);
}
#[test]
fn test_roundtrip_peer_and_platform_metadata() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let sri = fake_sri('a');
let content = r#"{
"lockfileVersion": 1,
"workspaces": { "": { "dependencies": { "foo": "^1.0.0" } } },
"packages": {
"foo": ["foo@1.0.0", "", {
"peerDependencies": { "react": "^18.0.0" },
"optionalPeers": ["react"],
"os": ["darwin", "linux"],
"cpu": ["arm64", "x64"],
"libc": ["glibc"]
}, "SRI"]
}
}"#
.replace("SRI", &sri);
std::fs::write(tmp.path(), &content).unwrap();
let graph = parse(tmp.path()).unwrap();
let foo = &graph.packages["foo@1.0.0"];
assert_eq!(
foo.peer_dependencies.get("react").map(String::as_str),
Some("^18.0.0")
);
assert!(
foo.peer_dependencies_meta
.get("react")
.is_some_and(|m| m.optional)
);
assert_eq!(
foo.os.as_slice(),
&["darwin".to_string(), "linux".to_string()]
);
assert_eq!(
foo.cpu.as_slice(),
&["arm64".to_string(), "x64".to_string()]
);
assert_eq!(foo.libc.as_slice(), &["glibc".to_string()]);
let manifest = aube_manifest::PackageJson {
name: Some("root".to_string()),
dependencies: [("foo".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 reparsed = parse(out.path()).unwrap();
let foo2 = &reparsed.packages["foo@1.0.0"];
assert_eq!(foo2.peer_dependencies, foo.peer_dependencies);
assert_eq!(foo2.os, foo.os);
assert_eq!(foo2.cpu, foo.cpu);
assert_eq!(foo2.libc, foo.libc);
}
#[test]
fn test_parse_scalar_platform_metadata() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let sri = fake_sri('a');
let content = r#"{
"lockfileVersion": 1,
"workspaces": { "": { "dependencies": { "@esbuild/darwin-arm64": "0.27.2" } } },
"packages": {
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", {
"os": "darwin",
"cpu": "arm64",
"libc": "glibc"
}, "SRI"]
}
}"#
.replace("SRI", &sri);
std::fs::write(tmp.path(), &content).unwrap();
let graph = parse(tmp.path()).unwrap();
let pkg = &graph.packages["@esbuild/darwin-arm64@0.27.2"];
assert_eq!(pkg.os.as_slice(), &["darwin".to_string()]);
assert_eq!(pkg.cpu.as_slice(), &["arm64".to_string()]);
assert_eq!(pkg.libc.as_slice(), &["glibc".to_string()]);
}
#[test]
fn test_roundtrip_workspace_peer_dependencies() {
use tempfile::TempDir;
let project = TempDir::new().unwrap();
let project_dir = project.path();
std::fs::write(
project_dir.join("package.json"),
r#"{"name":"root","version":"1.0.0"}"#,
)
.unwrap();
std::fs::create_dir_all(project_dir.join("packages/app")).unwrap();
std::fs::write(
project_dir.join("packages/app/package.json"),
r#"{"name":"app","version":"2.0.0"}"#,
)
.unwrap();
let lock_path = project_dir.join("bun.lock");
std::fs::write(
&lock_path,
r#"{
"lockfileVersion": 1,
"workspaces": {
"": { "name": "root" },
"packages/app": {
"name": "app",
"version": "2.0.0",
"peerDependencies": { "react": "^18.0.0" }
}
},
"packages": {}
}"#,
)
.unwrap();
let graph = parse(&lock_path).unwrap();
let app_extras = graph
.workspace_extra_fields
.get("packages/app")
.expect("packages/app workspace_extra_fields entry");
let peers = app_extras
.get("peerDependencies")
.and_then(serde_json::Value::as_object)
.expect("peerDependencies captured in extras");
assert_eq!(peers.get("react").and_then(|v| v.as_str()), Some("^18.0.0"));
let manifest =
aube_manifest::PackageJson::from_path(&project_dir.join("package.json")).unwrap();
write(&lock_path, &graph, &manifest).unwrap();
let written = std::fs::read_to_string(&lock_path).unwrap();
assert!(
written.contains("\"peerDependencies\""),
"workspace peerDependencies dropped on re-emit:\n{written}"
);
assert!(
written.contains("\"react\""),
"workspace peerDependencies.react dropped on re-emit:\n{written}"
);
}