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}