use super::*;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use tokio;
use std::collections::{BTreeMap};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LockfileType {
PackageLock,
YarnLock,
PnpmLock,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LockfileEntry {
pub version: String,
pub resolved: String,
pub integrity: String,
pub requires: HashMap<String, String>,
pub dependencies: HashMap<String, String>,
pub dev: bool,
pub optional: bool,
pub bundled: bool,
pub license: Option<String>,
pub bin: Option<HashMap<String, String>>,
pub engines: Option<HashMap<String, String>>,
pub os: Option<Vec<String>>,
pub cpu: Option<Vec<String>>,
}
#[derive(Debug)]
pub struct LockfileManager {
lockfile_type: LockfileType,
entries: HashMap<String, LockfileEntry>,
}
impl LockfileManager {
pub fn new() -> Self {
let lockfile_type: _ = if PathBuf::from("yarn.lock").exists() {
LockfileType::YarnLock
} else if PathBuf::from("pnpm-lock.yaml").exists() {
LockfileType::PnpmLock
} else {
LockfileType::PackageLock
};
Self {
lockfile_type,
entries: HashMap::new(),
}
}
pub async fn load_from_file(&mut self, path: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
let content: _ = tokio::fs::read_to_string(path).await?;
match self.lockfile_type {
LockfileType::PackageLock => {
self.load_package_lock(&content)?;
}
LockfileType::YarnLock => {
self.load_yarn_lock(&content)?;
}
LockfileType::PnpmLock => {
self.load_pnpm_lock(&content)?;
}
}
Ok(())
}
fn load_package_lock(&mut self, content: &str) -> Result<(), Box<dyn std::error::Error>> {
let package_lock: serde_json::Value = serde_json::from_str(content)?;
if let Some(packages) = package_lock.get("packages") {
for (key, value) in packages.as_object().unwrap() {
if key.starts_with("node_modules/") {
let package_name: _ = key.strip_prefix("node_modules/").unwrap();
let entry: _ = LockfileEntry {
version: value.get("version").unwrap().as_str().unwrap_or("").to_string(),
resolved: value.get("resolved").unwrap().as_str().unwrap_or("").to_string(),
integrity: value.get("integrity").unwrap().as_str().unwrap_or("").to_string(),
requires: value.get("requires")
.and_then(|r| r.as_object())
.map(|m| m.iter()
.map(|(k, v)| (k.clone(), v.as_str().unwrap_or("").to_string())
.collect())
.unwrap_or_default(),
dependencies: value.get("dependencies")
.and_then(|d| d.as_object())
.map(|m| m.iter()
.map(|(k, v)| (k.clone(), v.as_str().unwrap_or("").to_string())
.collect())
.unwrap_or_default(),
dev: value.get("dev").unwrap().as_bool().unwrap_or(false),
optional: value.get("optional").unwrap().as_bool().unwrap_or(false),
bundled: value.get("bundled").unwrap().as_bool().unwrap_or(false),
license: value.get("license").and_then(|l| l.as_str()).map(|s| s.to_string()),
bin: value.get("bin").and_then(|b| b.as_object())
.map(|m| m.iter()
.map(|(k, v)| (k.clone(), v.as_str().unwrap_or("").to_string())
.collect()),
engines: value.get("engines").and_then(|e| e.as_object())
.map(|m| m.iter()
.map(|(k, v)| (k.clone(), v.as_str().unwrap_or("").to_string())
.collect()),
os: value.get("os").and_then(|o| o.as_array())
.map(|arr| arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string())
.collect()),
cpu: value.get("cpu").and_then(|c| c.as_array())
.map(|arr| arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string())
.collect()),
};
self.entries.insert(package_name.to_string(), entry);
}
}
}
Ok(())
}
fn load_yarn_lock(&mut self, content: &str) -> Result<(), Box<dyn std::error::Error>> {
let lines: Vec<&str> = content.lines().collect();
let mut current_package = None;
let mut current_entry = None;
for line in lines {
if line.starts_with('"') {
if let Some((name, version)) = self.parse_yarn_lock_key(line) {
if let Some(entry) = current_entry.take() {
if let Some(pkg_name) = current_package.take() {
self.entries.insert(pkg_name, entry);
}
}
current_package = Some(format!("{}@{}", name, version));
current_entry = Some(LockfileEntry {
version,
resolved: "".to_string(),
integrity: "".to_string(),
requires: HashMap::new(),
dependencies: HashMap::new(),
dev: false,
optional: false,
bundled: false,
license: None,
bin: None,
engines: None,
os: None,
cpu: None,
});
}
} else if line.trim().starts_with("version") {
if let Some(ref mut entry) = current_entry {
entry.version = line.split('"').nth(1).unwrap_or("").to_string();
}
} else if line.trim().starts_with("resolved") {
if let Some(ref mut entry) = current_entry {
entry.resolved = line.split('"').nth(1).unwrap_or("").to_string();
}
} else if line.trim().starts_with("integrity") {
if let Some(ref mut entry) = current_entry {
entry.integrity = line.split('"').nth(1).unwrap_or("").to_string();
}
}
}
if let Some(entry) = current_entry.take() {
if let Some(pkg_name) = current_package.take() {
self.entries.insert(pkg_name, entry);
}
}
Ok(())
}
fn parse_yarn_lock_key(&self, key: &str) -> Option<(String, String)> {
let key: _ = key.trim().trim_matches('"');
if let Some(at_pos) = key.rfind('@') {
let (name_part, version_part) = key.split_at(at_pos);
let name: _ = if name_part.starts_with("@scope/") {
name_part.to_string()
} else {
name_part.trim_start_matches('@').to_string()
};
let version: _ = version_part.trim_start_matches('@').to_string();
Some((name, version))
} else {
None
}
}
fn load_pnpm_lock(&mut self, content: &str) -> Result<(), Box<dyn std::error::Error>> {
let lines: Vec<&str> = content.lines().collect();
let mut current_package = None;
let mut current_version = None;
for line in lines {
if line.starts_with(" ") && line.contains(":") {
let parts: Vec<&str> = line.split(':').collect();
if parts.len() >= 2 {
let key: _ = parts[0].trim();
let value: _ = parts[1].trim();
match key {
"version" => {
current_version = Some(value.to_string());
}
_ => {}
}
}
} else if !line.starts_with(' ') && line.contains('/') {
let parts: Vec<&str> = line.split('/').collect();
if parts.len() >= 2 {
let name: _ = if parts[0].starts_with("@") {
format!("{}/{}", parts[0], parts[1])
} else {
parts[0].to_string()
};
if let Some(version) = current_version.take() {
self.entries.insert(
name,
LockfileEntry {
version,
resolved: line.trim().to_string(),
integrity: "".to_string(),
requires: HashMap::new(),
dependencies: HashMap::new(),
dev: false,
optional: false,
bundled: false,
license: None,
bin: None,
engines: None,
os: None,
cpu: None,
},
);
}
}
}
}
Ok(())
}
pub async fn save_to_file(&self, path: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
match self.lockfile_type {
LockfileType::PackageLock => {
self.save_package_lock(path).await?;
}
LockfileType::YarnLock => {
self.save_yarn_lock(path).await?;
}
LockfileType::PnpmLock => {
self.save_pnpm_lock(path).await?;
}
}
Ok(())
}
async fn save_package_lock(&self, path: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
let mut package_lock = serde_json::Map::new();
package_lock.insert("name".to_string(), serde_json::Value::String("amberjs-project".to_string());
package_lock.insert("version".to_string(), serde_json::Value::String("1.0.0".to_string());
package_lock.insert("lockfileVersion".to_string(), serde_json::Value::Number(serde_json::Number::from(3));
let mut packages = serde_json::Map::new();
packages.insert("".to_string(), serde_json::Value::Object(serde_json::Map::new());
for (name, entry) in &self.entries {
let mut package_obj = serde_json::Map::new();
package_obj.insert("version".to_string(), serde_json::Value::String(entry.version.clone());
package_obj.insert("resolved".to_string(), serde_json::Value::String(entry.resolved.clone());
package_obj.insert("integrity".to_string(), serde_json::Value::String(entry.integrity.clone());
package_obj.insert("dev".to_string(), serde_json::Value::Bool(entry.dev));
package_obj.insert("optional".to_string(), serde_json::Value::Bool(entry.optional));
if !entry.requires.is_empty() {
let requires: _ = serde_json::Value::Object(
entry.requires.iter()
.map(|(k, v)| (k.clone(), serde_json::Value::String(v.clone())
.collect(),
);
package_obj.insert("requires".to_string(), requires);
}
if !entry.dependencies.is_empty() {
let dependencies: _ = serde_json::Value::Object(
entry.dependencies.iter()
.map(|(k, v)| (k.clone(), serde_json::Value::String(v.clone())
.collect(),
);
package_obj.insert("dependencies".to_string(), dependencies);
}
let package_key: _ = format!("node_modules/{}", name));
packages.insert(package_key, serde_json::Value::Object(package_obj));
}
package_lock.insert("packages".to_string(), serde_json::Value::Object(packages));
let content: _ = serde_json::to_string_pretty(&serde_json::Value::Object(package_lock))?;
tokio::fs::write(path, content).await?;
Ok(())
}
async fn save_yarn_lock(&self, path: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
let mut content = String::new();
content.push_str("# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.\n");
content.push_str(&format!("# yarn lockfile v1\n\n"));
for (name, entry) in &self.entries {
let (name_part, version_part) = if name.starts_with("@") {
let parts: Vec<&str> = name.split('/').collect();
if parts.len() >= 2 {
(format!("@{}", parts[1]), parts[0].to_string())
} else {
(name.clone(), "".to_string())
}
} else {
(name.clone(), "".to_string())
};
content.push_str(&format!("\"{}@{}\":\n", name_part, version_part));
content.push_str(&format!(" version \"{}\"\n", entry.version));
content.push_str(&format!(" resolved \"{}\"\n", entry.resolved));
content.push_str(&format!(" integrity {}\n", entry.integrity));
content.push('\n');
}
tokio::fs::write(path, content).await?;
Ok(())
}
async fn save_pnpm_lock(&self, path: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
let mut content = String::new();
content.push_str("lockfileVersion: 6.0\n");
content.push_str("settings:\n autoInstallPeers: true\n excludeLinksFromLockfile: false\n\n");
content.push_str("importers:\n .:\n devDependencies:\n\n");
content.push_str("packages:\n");
for (name, entry) in &self.entries {
let package_path: _ = if name.starts_with("@") {
format!("node_modules/{}", name)
} else {
format!("node_modules/{}", name)
};
content.push_str(&format!(" {}:\n", package_path));
content.push_str(&format!(" version: {}\n", entry.version));
if !entry.requires.is_empty() {
content.push_str(&format!(" requires:\n"));
for (dep, version) in &entry.requires {
content.push_str(&format!(" {}: {}\n", dep, version));
}
}
content.push('\n');
}
tokio::fs::write(path, content).await?;
Ok(())
}
pub async fn update_lockfile(
&mut self,
resolutions: &HashMap<String, PackageResolution>,
) -> Result<(), Box<dyn std::error::Error>> {
for (name, resolution) in resolutions {
let entry: _ = LockfileEntry {
version: resolution.version.clone(),
resolved: resolution.resolved_url.clone(),
integrity: resolution.integrity.clone(),
requires: resolution.dependencies.clone(),
dependencies: HashMap::new(),
dev: false,
optional: false,
bundled: false,
license: None,
bin: Some(resolution.bins.clone()),
engines: None,
os: None,
cpu: None,
};
self.entries.insert(name.clone(), entry);
}
Ok(())
}
pub fn get_entries(&self) -> &HashMap<String, LockfileEntry> {
&self.entries
}
pub fn find_package(&self, name: &str) -> Option<&LockfileEntry> {
self.entries.get(name)
}
pub fn validate(&self) -> Result<(), Box<dyn std::error::Error>> {
for (name, entry) in &self.entries {
if entry.version.is_empty() {
return Err(format!("Package {} missing version", name).into());
}
if entry.resolved.is_empty() {
return Err(format!("Package {} missing resolved URL", name).into());
}
}
Ok(())
}
}