1use std::collections::HashMap;
4use std::path::Path;
5
6#[derive(Clone, Copy, Debug, Eq, PartialEq)]
8pub enum LockfileKind {
9 None,
10 Npm,
11 Bun,
12}
13
14pub fn detect_lockfile(dir: &Path) -> LockfileKind {
16 if dir.join("bun.lock").exists() {
17 LockfileKind::Bun
18 } else if dir.join("package-lock.json").exists() {
19 LockfileKind::Npm
20 } else {
21 LockfileKind::None
22 }
23}
24
25pub fn read_package_json_deps(path: &Path) -> Option<HashMap<String, String>> {
27 let s = std::fs::read_to_string(path).ok()?;
28 let v: serde_json::Value = serde_json::from_str(&s).ok()?;
29 let mut deps = HashMap::new();
30 if let Some(d) = v.get("dependencies").and_then(|d| d.as_object()) {
31 for (k, v) in d {
32 if let Some(s) = v.as_str() {
33 deps.insert(k.clone(), s.to_string());
34 }
35 }
36 }
37 if let Some(d) = v.get("devDependencies").and_then(|d| d.as_object()) {
38 for (k, v) in d {
39 if let Some(s) = v.as_str() {
40 deps.insert(k.clone(), s.to_string());
41 }
42 }
43 }
44 Some(deps)
45}
46
47pub fn read_lockfile_resolved(path: &Path) -> Option<HashMap<String, String>> {
50 let s = std::fs::read_to_string(path).ok()?;
51 let v: serde_json::Value = serde_json::from_str(&s).ok()?;
52 let packages = v.get("packages")?.as_object()?;
53 let mut resolved = HashMap::new();
54 for (key, val) in packages {
55 let version = val.get("version")?.as_str()?;
56 let name = key.trim_start_matches("node_modules/");
58 if name.is_empty() {
59 continue;
60 }
61 resolved.insert(name.to_string(), version.to_string());
62 }
63 Some(resolved)
64}
65
66pub fn read_bun_lock_resolved(path: &Path) -> Option<HashMap<String, String>> {
69 let s = std::fs::read_to_string(path).ok()?;
70 let v: serde_json::Value = serde_json::from_str(&s).ok()?;
71 let packages = v.get("packages")?.as_object()?;
72 let mut resolved = HashMap::new();
73 for (key, _val) in packages {
74 let rest = key.strip_prefix("npm:").unwrap_or(key);
75 let at_pos = rest.rfind('@')?;
77 if at_pos == 0 {
78 continue; }
80 let name = rest[..at_pos].to_string();
81 let version = rest[at_pos + 1..].to_string();
82 if !version.is_empty() && !name.is_empty() {
83 resolved.insert(name, version);
84 }
85 }
86 Some(resolved)
87}
88
89pub fn resolve_deps_for_install(
92 package_json_deps: &HashMap<String, String>,
93 lockfile_resolved: Option<&HashMap<String, String>>,
94) -> Vec<String> {
95 let mut out = Vec::with_capacity(package_json_deps.len());
96 for (name, spec) in package_json_deps {
97 let version = lockfile_resolved
98 .and_then(|r| r.get(name).cloned())
99 .unwrap_or_else(|| spec.clone());
100 out.push(format!("{}@{}", name, version));
101 }
102 out
103}
104
105pub fn read_resolved_from_dir(dir: &Path) -> Option<HashMap<String, String>> {
107 let bun_lock = dir.join("bun.lock");
108 let npm_lock = dir.join("package-lock.json");
109 if bun_lock.exists() {
110 read_bun_lock_resolved(&bun_lock)
111 } else if npm_lock.exists() {
112 read_lockfile_resolved(&npm_lock)
113 } else {
114 None
115 }
116}
117
118pub fn read_lockfile_resolved_urls(path: &Path) -> Option<HashMap<String, String>> {
120 read_lockfile_resolved_urls_with_integrity(path).map(|(urls, _)| urls)
121}
122
123pub fn read_lockfile_resolved_urls_with_integrity(
125 path: &Path,
126) -> Option<(HashMap<String, String>, HashMap<String, String>)> {
127 let s = std::fs::read_to_string(path).ok()?;
128 let v: serde_json::Value = serde_json::from_str(&s).ok()?;
129 let packages = v.get("packages")?.as_object()?;
130 let mut urls = HashMap::new();
131 let mut integrity = HashMap::new();
132 for (key, val) in packages {
133 let name = key.trim_start_matches("node_modules/");
134 if name.is_empty() {
135 continue;
136 }
137 let version = val.get("version")?.as_str()?;
138 let resolved = val.get("resolved")?.as_str()?;
139 if resolved.ends_with(".tgz") {
140 let pkg_key = format!("{}@{}", name, version);
141 urls.insert(pkg_key.clone(), resolved.to_string());
142 if let Some(sri) = val.get("integrity").and_then(|i| i.as_str()) {
143 integrity.insert(pkg_key, sri.to_string());
144 }
145 }
146 }
147 Some((urls, integrity))
148}
149
150pub fn tarball_url_from_registry(name: &str, version: &str) -> String {
153 const REGISTRY: &str = "https://registry.npmjs.org";
154 let encoded = if name.starts_with('@') {
155 name.replace('/', "%2F")
156 } else {
157 name.to_string()
158 };
159 let tarball_name = if name.starts_with('@') {
160 name.split('/').last().unwrap_or(name).to_string()
161 } else {
162 name.to_string()
163 };
164 format!(
165 "{}/{}/-/{}-{}.tgz",
166 REGISTRY.trim_end_matches('/'),
167 encoded,
168 tarball_name,
169 version
170 )
171}
172
173pub fn read_resolved_urls_from_dir(dir: &Path) -> Option<HashMap<String, String>> {
175 read_resolved_urls_and_integrity_from_dir(dir).map(|(urls, _)| urls)
176}
177
178pub fn read_resolved_urls_and_integrity_from_dir(
180 dir: &Path,
181) -> Option<(HashMap<String, String>, HashMap<String, String>)> {
182 let npm_lock = dir.join("package-lock.json");
183 let bun_lock = dir.join("bun.lock");
184 if npm_lock.exists() {
185 return read_lockfile_resolved_urls_with_integrity(&npm_lock);
186 }
187 if bun_lock.exists() {
188 let resolved = read_bun_lock_resolved(&bun_lock)?;
189 let mut urls = HashMap::new();
190 for (name, version) in resolved {
191 let spec = format!("{}@{}", name, version);
192 urls.insert(spec, tarball_url_from_registry(&name, &version));
193 }
194 return Some((urls, HashMap::new()));
195 }
196 None
197}
198
199pub fn lockfile_integrity_complete(dir: &Path) -> bool {
201 let npm_lock = dir.join("package-lock.json");
202 if !npm_lock.exists() {
203 return true;
204 }
205 let Ok(s) = std::fs::read_to_string(&npm_lock) else {
206 return false;
207 };
208 let Ok(v) = serde_json::from_str::<serde_json::Value>(&s) else {
209 return false;
210 };
211 let Some(packages) = v.get("packages").and_then(|p| p.as_object()) else {
212 return false;
213 };
214 for (key, val) in packages {
215 let name = key.trim_start_matches("node_modules/");
216 if name.is_empty() {
217 continue;
218 }
219 if val.get("integrity").and_then(|i| i.as_str()).is_none() {
220 return false;
221 }
222 }
223 true
224}
225
226pub fn read_all_resolved_specs_from_dir(dir: &Path) -> Option<Vec<String>> {
230 let npm_lock = dir.join("package-lock.json");
231 let bun_lock = dir.join("bun.lock");
232 if npm_lock.exists() {
233 let resolved = read_lockfile_resolved(&npm_lock)?;
234 let mut specs: Vec<String> = resolved
235 .into_iter()
236 .map(|(name, version)| format!("{}@{}", name, version))
237 .collect();
238 specs.sort();
239 specs.dedup();
240 return Some(specs);
241 }
242 if bun_lock.exists() {
243 let resolved = read_bun_lock_resolved(&bun_lock)?;
244 let mut specs: Vec<String> = resolved
245 .into_iter()
246 .map(|(name, version)| format!("{}@{}", name, version))
247 .collect();
248 specs.sort();
249 specs.dedup();
250 return Some(specs);
251 }
252 None
253}