use crate::{DirectDep, Error, LocalSource, LockedPackage, LockfileGraph};
use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
pub fn write(
path: &Path,
graph: &LockfileGraph,
manifest: &aube_manifest::PackageJson,
) -> Result<(), Error> {
use serde_json::{Value, json};
let mut canonical: BTreeMap<String, &LockedPackage> = BTreeMap::new();
for pkg in graph.packages.values() {
if matches!(pkg.local_source, Some(LocalSource::Link(_))) {
continue;
}
canonical.entry(pkg.spec_key()).or_insert(pkg);
}
let mut all_roots: Vec<DirectDep> = Vec::new();
let mut seen_names: BTreeSet<String> = BTreeSet::new();
for deps in graph.importers.values() {
for d in deps {
if matches!(
graph
.packages
.get(&d.dep_path)
.and_then(|p| p.local_source.as_ref()),
Some(LocalSource::Link(_))
) {
continue;
}
if !seen_names.insert(d.name.clone()) {
continue;
}
all_roots.push(d.clone());
}
}
let tree = crate::npm::build_hoist_tree(&canonical, &all_roots);
let project_dir = path.parent().unwrap_or_else(|| Path::new("."));
let mut workspace_manifests: BTreeMap<String, aube_manifest::PackageJson> = BTreeMap::new();
for importer_path in graph.importers.keys() {
if importer_path == "." {
continue;
}
let pj_path = project_dir.join(importer_path).join("package.json");
let pj = aube_manifest::PackageJson::from_path(&pj_path).unwrap_or_default();
workspace_manifests.insert(importer_path.clone(), pj);
}
fn build_workspace_pairs(
pj: &aube_manifest::PackageJson,
is_root: bool,
ws_extras: Option<&BTreeMap<String, Value>>,
) -> Vec<(String, Value)> {
let mut pairs: Vec<(String, Value)> = Vec::new();
if let Some(name) = &pj.name {
pairs.push(("name".to_string(), json!(name)));
}
if !is_root {
if let Some(version) = &pj.version {
pairs.push(("version".to_string(), json!(version)));
}
if let Some(bin) = pj.extra.get("bin") {
pairs.push(("bin".to_string(), bin.clone()));
}
}
if !pj.dependencies.is_empty() {
pairs.push(("dependencies".to_string(), json!(pj.dependencies)));
}
if !pj.dev_dependencies.is_empty() {
pairs.push(("devDependencies".to_string(), json!(pj.dev_dependencies)));
}
if !pj.optional_dependencies.is_empty() {
pairs.push((
"optionalDependencies".to_string(),
json!(pj.optional_dependencies),
));
}
if !pj.peer_dependencies.is_empty() {
pairs.push(("peerDependencies".to_string(), json!(pj.peer_dependencies)));
}
if !is_root
&& let Some(meta) = pj
.extra
.get("peerDependenciesMeta")
.and_then(Value::as_object)
{
let mut optional_peer_names: Vec<&String> = meta
.iter()
.filter(|(_, v)| v.get("optional").and_then(Value::as_bool).unwrap_or(false))
.map(|(k, _)| k)
.collect();
optional_peer_names.sort();
if !optional_peer_names.is_empty() {
let optional_peers: Vec<Value> = optional_peer_names
.into_iter()
.map(|k| Value::String(k.clone()))
.collect();
pairs.push(("optionalPeers".to_string(), Value::Array(optional_peers)));
}
}
if let Some(extras) = ws_extras {
let already: BTreeSet<String> = pairs.iter().map(|(k, _)| k.clone()).collect();
for (k, v) in extras {
if already.contains(k) {
continue;
}
pairs.push((k.clone(), v.clone()));
}
}
pairs
}
let mut workspace_pairs: Vec<(String, Vec<(String, Value)>)> = Vec::new();
workspace_pairs.push((
"".to_string(),
build_workspace_pairs(manifest, true, graph.workspace_extra_fields.get(".")),
));
for (importer_path, pj) in &workspace_manifests {
let extras = graph.workspace_extra_fields.get(importer_path);
workspace_pairs.push((
importer_path.clone(),
build_workspace_pairs(pj, false, extras),
));
}
let mut package_entries: Vec<(String, Value)> = Vec::new();
for (segs, canonical_key) in &tree {
let Some(pkg) = canonical.get(canonical_key).copied() else {
continue;
};
let bun_key = segs.join("/");
let mut deps_obj = serde_json::Map::new();
let mut opt_deps_obj = serde_json::Map::new();
for (dep_name, dep_value) in &pkg.dependencies {
let key = crate::npm::child_canonical_key(dep_name, dep_value);
if !canonical.contains_key(&key) {
continue;
}
let rendered = pkg
.declared_dependencies
.get(dep_name)
.cloned()
.unwrap_or_else(|| {
crate::npm::dep_value_as_version(dep_name, dep_value).to_string()
});
if pkg.optional_dependencies.contains_key(dep_name) {
opt_deps_obj.insert(dep_name.clone(), Value::String(rendered));
} else {
deps_obj.insert(dep_name.clone(), Value::String(rendered));
}
}
let mut meta = serde_json::Map::new();
if !deps_obj.is_empty() {
meta.insert("dependencies".to_string(), Value::Object(deps_obj));
}
if !opt_deps_obj.is_empty() {
meta.insert(
"optionalDependencies".to_string(),
Value::Object(opt_deps_obj),
);
}
if !pkg.peer_dependencies.is_empty() {
let map: serde_json::Map<String, Value> = pkg
.peer_dependencies
.iter()
.map(|(k, v)| (k.clone(), Value::String(v.clone())))
.collect();
meta.insert("peerDependencies".to_string(), Value::Object(map));
}
let optional_peer_names: Vec<String> = pkg
.peer_dependencies_meta
.iter()
.filter(|(_, v)| v.optional)
.map(|(k, _)| k.clone())
.collect();
if !optional_peer_names.is_empty() {
let mut sorted = optional_peer_names.clone();
sorted.sort();
let arr: Vec<Value> = sorted.into_iter().map(Value::String).collect();
meta.insert("optionalPeers".to_string(), Value::Array(arr));
}
if let Some(raw_bin) = pkg.extra_meta.get("bin")
&& !matches!(raw_bin, Value::Null)
{
meta.insert("bin".to_string(), raw_bin.clone());
} else {
let real_bins: serde_json::Map<String, Value> = pkg
.bin
.iter()
.filter(|(k, _)| !k.is_empty())
.map(|(k, v)| (k.clone(), Value::String(v.clone())))
.collect();
if !real_bins.is_empty() {
meta.insert("bin".to_string(), Value::Object(real_bins));
}
}
if !pkg.os.is_empty() {
let arr: Vec<Value> = pkg.os.iter().map(|s| Value::String(s.clone())).collect();
meta.insert("os".to_string(), Value::Array(arr));
}
if !pkg.cpu.is_empty() {
let arr: Vec<Value> = pkg.cpu.iter().map(|s| Value::String(s.clone())).collect();
meta.insert("cpu".to_string(), Value::Array(arr));
}
if !pkg.libc.is_empty() {
let arr: Vec<Value> = pkg.libc.iter().map(|s| Value::String(s.clone())).collect();
meta.insert("libc".to_string(), Value::Array(arr));
}
const MODELED_META_KEYS: &[&str] = &[
"dependencies",
"optionalDependencies",
"peerDependencies",
"optionalPeers",
"bin",
"os",
"cpu",
"libc",
];
for (k, v) in &pkg.extra_meta {
if MODELED_META_KEYS.contains(&k.as_str()) {
continue;
}
meta.insert(k.clone(), v.clone());
}
let ident_name = pkg.alias_of.as_deref().unwrap_or(&pkg.name);
let ident = format!("{}@{}", ident_name, pkg.version);
let integrity = pkg.integrity.clone().unwrap_or_default();
let entry = Value::Array(vec![
Value::String(ident),
Value::String(String::new()),
Value::Object(meta),
Value::String(integrity),
]);
package_entries.push((bun_key, entry));
}
let workspace_dep_paths: BTreeSet<String> = graph
.packages
.values()
.filter(|p| matches!(p.local_source, Some(LocalSource::Link(_))))
.map(|p| p.dep_path.clone())
.collect();
let mut emitted_workspace_keys: BTreeSet<String> = BTreeSet::new();
for pkg in graph.packages.values() {
let Some(LocalSource::Link(rel_path)) = pkg.local_source.as_ref() else {
continue;
};
let key = pkg.alias_of.as_deref().unwrap_or(&pkg.name).to_string();
if !emitted_workspace_keys.insert(key.clone()) {
continue;
}
let ident_name = pkg.alias_of.as_deref().unwrap_or(&pkg.name);
let workspace_spec = if pkg.version.starts_with("workspace:") {
pkg.version
.strip_prefix("workspace:")
.unwrap_or("*")
.to_string()
} else {
let path_str = rel_path.to_string_lossy();
if path_str.is_empty() || path_str == "." {
"*".to_string()
} else {
path_str.into_owned()
}
};
let ident = format!("{ident_name}@workspace:{workspace_spec}");
let mut deps_obj = serde_json::Map::new();
let mut opt_deps_obj = serde_json::Map::new();
for (dep_name, dep_value) in &pkg.dependencies {
let canonical_key = crate::npm::child_canonical_key(dep_name, dep_value);
if !canonical.contains_key(&canonical_key)
&& !workspace_dep_paths.contains(&canonical_key)
{
continue;
}
let rendered = pkg
.declared_dependencies
.get(dep_name)
.cloned()
.unwrap_or_else(|| {
crate::npm::dep_value_as_version(dep_name, dep_value).to_string()
});
if pkg.optional_dependencies.contains_key(dep_name) {
opt_deps_obj.insert(dep_name.clone(), Value::String(rendered));
} else {
deps_obj.insert(dep_name.clone(), Value::String(rendered));
}
}
let entry = if deps_obj.is_empty() && opt_deps_obj.is_empty() {
Value::Array(vec![Value::String(ident)])
} else {
let mut meta = serde_json::Map::new();
if !deps_obj.is_empty() {
meta.insert("dependencies".to_string(), Value::Object(deps_obj));
}
if !opt_deps_obj.is_empty() {
meta.insert(
"optionalDependencies".to_string(),
Value::Object(opt_deps_obj),
);
}
Value::Array(vec![Value::String(ident), Value::Object(meta)])
};
package_entries.push((key, entry));
}
package_entries.sort_by(|a, b| a.0.cmp(&b.0));
let config_version = graph.bun_config_version.unwrap_or(1);
let mut top_level_extras: Vec<(String, Value)> = Vec::new();
if !graph.overrides.is_empty() {
let mut obj = serde_json::Map::new();
for (k, v) in &graph.overrides {
obj.insert(k.clone(), Value::String(v.clone()));
}
top_level_extras.push(("overrides".to_string(), Value::Object(obj)));
}
if !graph.patched_dependencies.is_empty() {
let mut obj = serde_json::Map::new();
for (k, v) in &graph.patched_dependencies {
obj.insert(k.clone(), Value::String(v.clone()));
}
top_level_extras.push(("patchedDependencies".to_string(), Value::Object(obj)));
}
if !graph.trusted_dependencies.is_empty() {
let arr: Vec<Value> = graph
.trusted_dependencies
.iter()
.map(|s| Value::String(s.clone()))
.collect();
top_level_extras.push(("trustedDependencies".to_string(), Value::Array(arr)));
}
if let Some(default_catalog) = graph.catalogs.get("default") {
let mut obj = serde_json::Map::new();
for (k, v) in default_catalog {
obj.insert(k.clone(), Value::String(v.specifier.clone()));
}
if !obj.is_empty() {
top_level_extras.push(("catalog".to_string(), Value::Object(obj)));
}
}
let named_catalogs: BTreeMap<&String, &BTreeMap<String, crate::CatalogEntry>> = graph
.catalogs
.iter()
.filter(|(k, _)| k.as_str() != "default")
.collect();
if !named_catalogs.is_empty() {
let mut outer = serde_json::Map::new();
for (name, entries) in named_catalogs {
let mut inner = serde_json::Map::new();
for (k, v) in entries {
inner.insert(k.clone(), Value::String(v.specifier.clone()));
}
outer.insert(name.clone(), Value::Object(inner));
}
top_level_extras.push(("catalogs".to_string(), Value::Object(outer)));
}
const MODELED_TOP_KEYS: &[&str] = &[
"lockfileVersion",
"configVersion",
"workspaces",
"packages",
"overrides",
"patchedDependencies",
"trustedDependencies",
"catalog",
"catalogs",
];
for (k, v) in &graph.extra_fields {
if MODELED_TOP_KEYS.contains(&k.as_str()) {
continue;
}
top_level_extras.push((k.clone(), v.clone()));
}
let body = format_bun_lockfile(
&workspace_pairs,
&package_entries,
config_version,
&top_level_extras,
);
crate::atomic_write_lockfile(path, body.as_bytes())?;
Ok(())
}
fn format_bun_lockfile(
workspaces: &[(String, Vec<(String, serde_json::Value)>)],
package_entries: &[(String, serde_json::Value)],
config_version: u32,
top_level_extras: &[(String, serde_json::Value)],
) -> String {
let mut out = String::with_capacity(8192);
out.push_str("{\n");
out.push_str(" \"lockfileVersion\": 1,\n");
out.push_str(&format!(" \"configVersion\": {config_version},\n"));
out.push_str(" \"workspaces\": {\n");
for (path, pairs) in workspaces.iter() {
out.push_str(&format!(
" {}: {{\n",
serde_json::to_string(path).unwrap()
));
const MULTILINE_KEYS: &[&str] = &[
"dependencies",
"devDependencies",
"optionalDependencies",
"peerDependencies",
];
for (k, v) in pairs.iter() {
let key_str = serde_json::to_string(k).unwrap();
match v {
serde_json::Value::Object(map)
if !map.is_empty() && MULTILINE_KEYS.contains(&k.as_str()) =>
{
out.push_str(&format!(" {key_str}: {{\n"));
for (dk, dv) in map {
out.push_str(&format!(
" {}: {},\n",
serde_json::to_string(dk).unwrap(),
inline_json(dv, 0)
));
}
out.push_str(" },\n");
}
_ => {
out.push_str(&format!(" {key_str}: {},\n", inline_json(v, 0)));
}
}
}
out.push_str(" },\n");
}
out.push_str(" },\n");
for (k, v) in top_level_extras {
let key_str = serde_json::to_string(k).unwrap();
match v {
serde_json::Value::Object(map) if !map.is_empty() => {
out.push_str(&format!(" {key_str}: {{\n"));
for (dk, dv) in map {
out.push_str(&format!(
" {}: {},\n",
serde_json::to_string(dk).unwrap(),
inline_json(dv, 0)
));
}
out.push_str(" },\n");
}
_ => {
out.push_str(&format!(" {key_str}: {},\n", inline_json(v, 0)));
}
}
}
out.push_str(" \"packages\": {\n");
for (i, (key, entry)) in package_entries.iter().enumerate() {
if i > 0 {
out.push('\n');
}
out.push_str(&format!(
" {}: {},\n",
serde_json::to_string(key).unwrap(),
inline_json(entry, 0)
));
}
out.push_str(" }\n");
out.push_str("}\n");
out
}
fn inline_json(value: &serde_json::Value, _base_indent: usize) -> String {
use serde_json::Value;
match value {
Value::Null => "null".to_string(),
Value::Bool(b) => b.to_string(),
Value::Number(n) => n.to_string(),
Value::String(_) => serde_json::to_string(value).unwrap(),
Value::Array(arr) => {
let parts: Vec<String> = arr.iter().map(|v| inline_json(v, 0)).collect();
format!("[{}]", parts.join(", "))
}
Value::Object(map) => {
if map.is_empty() {
return "{}".to_string();
}
let parts: Vec<String> = map
.iter()
.map(|(k, v)| {
format!(
"{}: {}",
serde_json::to_string(k).unwrap(),
inline_json(v, 0)
)
})
.collect();
format!("{{ {} }}", parts.join(", "))
}
}
}