o- 0.5.0

Multi-Engine JavaScript Runtime
Documentation
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap};
use std::fs;
use std::io;
use std::path::{Path, PathBuf};

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct LockFile {
    pub name: Option<String>,
    pub version: Option<String>,
    #[serde(rename = "lockfileVersion")]
    pub lockfile_version: u8,
    pub packages: BTreeMap<String, LockedPackage>,
}

#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct LockedPackage {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub version: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub resolved: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub integrity: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub dependencies: Option<BTreeMap<String, String>>,
    #[serde(skip_serializing_if = "Option::is_none", rename = "devDependencies")]
    pub dev_dependencies: Option<BTreeMap<String, String>>,
    #[serde(
        skip_serializing_if = "Option::is_none",
        rename = "optionalDependencies"
    )]
    pub optional_dependencies: Option<BTreeMap<String, String>>,
    #[serde(skip_serializing_if = "Option::is_none", rename = "peerDependencies")]
    pub peer_dependencies: Option<BTreeMap<String, String>>,
}

#[derive(Debug, Default)]
pub struct LockCollector {
    packages: BTreeMap<String, LockedPackage>,
}

impl LockCollector {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn insert_root_fields(
        &mut self,
        name: &str,
        version: &str,
        dependencies: &HashMap<String, String>,
        dev_dependencies: &HashMap<String, String>,
        optional_dependencies: &HashMap<String, String>,
        peer_dependencies: &HashMap<String, String>,
    ) {
        self.packages.insert(
            String::new(),
            LockedPackage {
                name: Some(name.to_string()),
                version: Some(version.to_string()),
                resolved: None,
                integrity: None,
                dependencies: Some(to_btree(dependencies.clone())),
                dev_dependencies: Some(to_btree(dev_dependencies.clone())),
                optional_dependencies: Some(to_btree(optional_dependencies.clone())),
                peer_dependencies: Some(to_btree(peer_dependencies.clone())),
            },
        );
    }

    pub fn insert_package(
        &mut self,
        project_root: &Path,
        package_dir: &Path,
        name: &str,
        version: &str,
        resolved: &str,
        integrity: Option<&str>,
        dependencies: &HashMap<String, String>,
        optional_dependencies: &HashMap<String, String>,
        peer_dependencies: &HashMap<String, String>,
    ) -> io::Result<()> {
        let key = lockfile_key(project_root, package_dir)?;
        self.packages.insert(
            key,
            LockedPackage {
                name: Some(name.to_string()),
                version: Some(version.to_string()),
                resolved: Some(resolved.to_string()),
                integrity: integrity.map(str::to_string),
                dependencies: Some(to_btree(dependencies.clone())),
                dev_dependencies: None,
                optional_dependencies: Some(to_btree(optional_dependencies.clone())),
                peer_dependencies: Some(to_btree(peer_dependencies.clone())),
            },
        );
        Ok(())
    }

    pub fn into_lockfile_fields(self, name: &str, version: &str) -> LockFile {
        LockFile {
            name: Some(name.to_string()),
            version: Some(version.to_string()),
            lockfile_version: 3,
            packages: self.packages,
        }
    }
}

pub fn write_lockfile(project_root: &Path, lockfile: &LockFile) -> io::Result<PathBuf> {
    let path = project_root.join("package-lock.json");
    let temp_path = project_root.join(".package-lock.json.tmp");
    let mut json = serde_json::to_vec_pretty(lockfile).map_err(io::Error::other)?;
    json.push(b'\n');
    fs::write(&temp_path, json)?;
    fs::rename(&temp_path, &path)?;
    Ok(path)
}

fn lockfile_key(project_root: &Path, package_dir: &Path) -> io::Result<String> {
    let relative = package_dir.strip_prefix(project_root).map_err(|_| {
        io::Error::new(
            io::ErrorKind::InvalidInput,
            format!(
                "package path `{}` is outside project root `{}`",
                package_dir.display(),
                project_root.display()
            ),
        )
    })?;

    Ok(relative.to_string_lossy().replace('\\', "/"))
}

fn to_btree(map: HashMap<String, String>) -> BTreeMap<String, String> {
    map.into_iter().collect()
}