Skip to main content

o_/
lock.rs

1use serde::{Deserialize, Serialize};
2use std::collections::{BTreeMap, HashMap};
3use std::fs;
4use std::io;
5use std::path::{Path, PathBuf};
6
7#[derive(Debug, Serialize, Deserialize, Clone)]
8pub struct LockFile {
9    pub name: Option<String>,
10    pub version: Option<String>,
11    #[serde(rename = "lockfileVersion")]
12    pub lockfile_version: u8,
13    pub packages: BTreeMap<String, LockedPackage>,
14}
15
16#[derive(Debug, Serialize, Deserialize, Clone, Default)]
17pub struct LockedPackage {
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub name: Option<String>,
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub version: Option<String>,
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub resolved: Option<String>,
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub integrity: Option<String>,
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub dependencies: Option<BTreeMap<String, String>>,
28    #[serde(skip_serializing_if = "Option::is_none", rename = "devDependencies")]
29    pub dev_dependencies: Option<BTreeMap<String, String>>,
30    #[serde(
31        skip_serializing_if = "Option::is_none",
32        rename = "optionalDependencies"
33    )]
34    pub optional_dependencies: Option<BTreeMap<String, String>>,
35    #[serde(skip_serializing_if = "Option::is_none", rename = "peerDependencies")]
36    pub peer_dependencies: Option<BTreeMap<String, String>>,
37}
38
39#[derive(Debug, Default)]
40pub struct LockCollector {
41    packages: BTreeMap<String, LockedPackage>,
42}
43
44impl LockCollector {
45    pub fn new() -> Self {
46        Self::default()
47    }
48
49    pub fn insert_root_fields(
50        &mut self,
51        name: &str,
52        version: &str,
53        dependencies: &HashMap<String, String>,
54        dev_dependencies: &HashMap<String, String>,
55        optional_dependencies: &HashMap<String, String>,
56        peer_dependencies: &HashMap<String, String>,
57    ) {
58        self.packages.insert(
59            String::new(),
60            LockedPackage {
61                name: Some(name.to_string()),
62                version: Some(version.to_string()),
63                resolved: None,
64                integrity: None,
65                dependencies: Some(to_btree(dependencies.clone())),
66                dev_dependencies: Some(to_btree(dev_dependencies.clone())),
67                optional_dependencies: Some(to_btree(optional_dependencies.clone())),
68                peer_dependencies: Some(to_btree(peer_dependencies.clone())),
69            },
70        );
71    }
72
73    pub fn insert_package(
74        &mut self,
75        project_root: &Path,
76        package_dir: &Path,
77        name: &str,
78        version: &str,
79        resolved: &str,
80        integrity: Option<&str>,
81        dependencies: &HashMap<String, String>,
82        optional_dependencies: &HashMap<String, String>,
83        peer_dependencies: &HashMap<String, String>,
84    ) -> io::Result<()> {
85        let key = lockfile_key(project_root, package_dir)?;
86        self.packages.insert(
87            key,
88            LockedPackage {
89                name: Some(name.to_string()),
90                version: Some(version.to_string()),
91                resolved: Some(resolved.to_string()),
92                integrity: integrity.map(str::to_string),
93                dependencies: Some(to_btree(dependencies.clone())),
94                dev_dependencies: None,
95                optional_dependencies: Some(to_btree(optional_dependencies.clone())),
96                peer_dependencies: Some(to_btree(peer_dependencies.clone())),
97            },
98        );
99        Ok(())
100    }
101
102    pub fn into_lockfile_fields(self, name: &str, version: &str) -> LockFile {
103        LockFile {
104            name: Some(name.to_string()),
105            version: Some(version.to_string()),
106            lockfile_version: 3,
107            packages: self.packages,
108        }
109    }
110}
111
112pub fn write_lockfile(project_root: &Path, lockfile: &LockFile) -> io::Result<PathBuf> {
113    let path = project_root.join("package-lock.json");
114    let temp_path = project_root.join(".package-lock.json.tmp");
115    let mut json = serde_json::to_vec_pretty(lockfile).map_err(io::Error::other)?;
116    json.push(b'\n');
117    fs::write(&temp_path, json)?;
118    fs::rename(&temp_path, &path)?;
119    Ok(path)
120}
121
122fn lockfile_key(project_root: &Path, package_dir: &Path) -> io::Result<String> {
123    let relative = package_dir.strip_prefix(project_root).map_err(|_| {
124        io::Error::new(
125            io::ErrorKind::InvalidInput,
126            format!(
127                "package path `{}` is outside project root `{}`",
128                package_dir.display(),
129                project_root.display()
130            ),
131        )
132    })?;
133
134    Ok(relative.to_string_lossy().replace('\\', "/"))
135}
136
137fn to_btree(map: HashMap<String, String>) -> BTreeMap<String, String> {
138    map.into_iter().collect()
139}