use std::collections::HashMap;
use std::path::Path;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum LockfileKind {
None,
Npm,
Bun,
}
pub fn detect_lockfile(dir: &Path) -> LockfileKind {
if dir.join("bun.lock").exists() {
LockfileKind::Bun
} else if dir.join("package-lock.json").exists() {
LockfileKind::Npm
} else {
LockfileKind::None
}
}
pub fn read_package_json_deps(path: &Path) -> Option<HashMap<String, String>> {
let s = std::fs::read_to_string(path).ok()?;
let v: serde_json::Value = serde_json::from_str(&s).ok()?;
let mut deps = HashMap::new();
if let Some(d) = v.get("dependencies").and_then(|d| d.as_object()) {
for (k, v) in d {
if let Some(s) = v.as_str() {
deps.insert(k.clone(), s.to_string());
}
}
}
if let Some(d) = v.get("devDependencies").and_then(|d| d.as_object()) {
for (k, v) in d {
if let Some(s) = v.as_str() {
deps.insert(k.clone(), s.to_string());
}
}
}
Some(deps)
}
pub fn read_lockfile_resolved(path: &Path) -> Option<HashMap<String, String>> {
let s = std::fs::read_to_string(path).ok()?;
let v: serde_json::Value = serde_json::from_str(&s).ok()?;
let packages = v.get("packages")?.as_object()?;
let mut resolved = HashMap::new();
for (key, val) in packages {
let version = val.get("version")?.as_str()?;
let name = key.trim_start_matches("node_modules/");
if name.is_empty() {
continue;
}
resolved.insert(name.to_string(), version.to_string());
}
Some(resolved)
}
pub fn read_bun_lock_resolved(path: &Path) -> Option<HashMap<String, String>> {
let s = std::fs::read_to_string(path).ok()?;
let v: serde_json::Value = serde_json::from_str(&s).ok()?;
let packages = v.get("packages")?.as_object()?;
let mut resolved = HashMap::new();
for (key, _val) in packages {
let rest = key.strip_prefix("npm:").unwrap_or(key);
let at_pos = rest.rfind('@')?;
if at_pos == 0 {
continue; }
let name = rest[..at_pos].to_string();
let version = rest[at_pos + 1..].to_string();
if !version.is_empty() && !name.is_empty() {
resolved.insert(name, version);
}
}
Some(resolved)
}
pub fn resolve_deps_for_install(
package_json_deps: &HashMap<String, String>,
lockfile_resolved: Option<&HashMap<String, String>>,
) -> Vec<String> {
let mut out = Vec::with_capacity(package_json_deps.len());
for (name, spec) in package_json_deps {
let version = lockfile_resolved
.and_then(|r| r.get(name).cloned())
.unwrap_or_else(|| spec.clone());
out.push(format!("{}@{}", name, version));
}
out
}
pub fn read_resolved_from_dir(dir: &Path) -> Option<HashMap<String, String>> {
let bun_lock = dir.join("bun.lock");
let npm_lock = dir.join("package-lock.json");
if bun_lock.exists() {
read_bun_lock_resolved(&bun_lock)
} else if npm_lock.exists() {
read_lockfile_resolved(&npm_lock)
} else {
None
}
}
pub fn read_lockfile_resolved_urls(path: &Path) -> Option<HashMap<String, String>> {
read_lockfile_resolved_urls_with_integrity(path).map(|(urls, _)| urls)
}
pub fn read_lockfile_resolved_urls_with_integrity(
path: &Path,
) -> Option<(HashMap<String, String>, HashMap<String, String>)> {
let s = std::fs::read_to_string(path).ok()?;
let v: serde_json::Value = serde_json::from_str(&s).ok()?;
let packages = v.get("packages")?.as_object()?;
let mut urls = HashMap::new();
let mut integrity = HashMap::new();
for (key, val) in packages {
let name = key.trim_start_matches("node_modules/");
if name.is_empty() {
continue;
}
let version = val.get("version")?.as_str()?;
let resolved = val.get("resolved")?.as_str()?;
if resolved.ends_with(".tgz") {
let pkg_key = format!("{}@{}", name, version);
urls.insert(pkg_key.clone(), resolved.to_string());
if let Some(sri) = val.get("integrity").and_then(|i| i.as_str()) {
integrity.insert(pkg_key, sri.to_string());
}
}
}
Some((urls, integrity))
}
pub fn tarball_url_from_registry(name: &str, version: &str) -> String {
const REGISTRY: &str = "https://registry.npmjs.org";
let encoded = if name.starts_with('@') {
name.replace('/', "%2F")
} else {
name.to_string()
};
let tarball_name = if name.starts_with('@') {
name.split('/').last().unwrap_or(name).to_string()
} else {
name.to_string()
};
format!(
"{}/{}/-/{}-{}.tgz",
REGISTRY.trim_end_matches('/'),
encoded,
tarball_name,
version
)
}
pub fn read_resolved_urls_from_dir(dir: &Path) -> Option<HashMap<String, String>> {
read_resolved_urls_and_integrity_from_dir(dir).map(|(urls, _)| urls)
}
pub fn read_resolved_urls_and_integrity_from_dir(
dir: &Path,
) -> Option<(HashMap<String, String>, HashMap<String, String>)> {
let npm_lock = dir.join("package-lock.json");
let bun_lock = dir.join("bun.lock");
if npm_lock.exists() {
return read_lockfile_resolved_urls_with_integrity(&npm_lock);
}
if bun_lock.exists() {
let resolved = read_bun_lock_resolved(&bun_lock)?;
let mut urls = HashMap::new();
for (name, version) in resolved {
let spec = format!("{}@{}", name, version);
urls.insert(spec, tarball_url_from_registry(&name, &version));
}
return Some((urls, HashMap::new()));
}
None
}
pub fn lockfile_integrity_complete(dir: &Path) -> bool {
let npm_lock = dir.join("package-lock.json");
if !npm_lock.exists() {
return true;
}
let Ok(s) = std::fs::read_to_string(&npm_lock) else {
return false;
};
let Ok(v) = serde_json::from_str::<serde_json::Value>(&s) else {
return false;
};
let Some(packages) = v.get("packages").and_then(|p| p.as_object()) else {
return false;
};
for (key, val) in packages {
let name = key.trim_start_matches("node_modules/");
if name.is_empty() {
continue;
}
if val.get("integrity").and_then(|i| i.as_str()).is_none() {
return false;
}
}
true
}
pub fn read_all_resolved_specs_from_dir(dir: &Path) -> Option<Vec<String>> {
let npm_lock = dir.join("package-lock.json");
let bun_lock = dir.join("bun.lock");
if npm_lock.exists() {
let resolved = read_lockfile_resolved(&npm_lock)?;
let mut specs: Vec<String> = resolved
.into_iter()
.map(|(name, version)| format!("{}@{}", name, version))
.collect();
specs.sort();
specs.dedup();
return Some(specs);
}
if bun_lock.exists() {
let resolved = read_bun_lock_resolved(&bun_lock)?;
let mut specs: Vec<String> = resolved
.into_iter()
.map(|(name, version)| format!("{}@{}", name, version))
.collect();
specs.sort();
specs.dedup();
return Some(specs);
}
None
}