Skip to main content

jhol_core/
lockfile_write.rs

1//! Native lockfile writing: resolve dependency tree and emit package-lock.json.
2
3use std::collections::{HashMap, HashSet};
4use std::path::Path;
5use std::sync::{Arc, Mutex};
6
7use crate::lockfile;
8use crate::registry;
9
10/// One resolved package entry for the lockfile.
11#[derive(Clone, Debug)]
12pub struct ResolvedPackage {
13    pub version: String,
14    pub resolved: String,
15    pub integrity: Option<String>,
16    pub dependencies: HashMap<String, String>,
17    pub peer_dependencies: HashMap<String, String>,
18    pub peer_dependencies_meta: HashMap<String, serde_json::Value>,
19}
20
21#[derive(Clone, Debug)]
22struct PeerRequirement {
23    requester: String,
24    spec: String,
25    optional: bool,
26}
27
28#[derive(Clone, Debug)]
29struct Requirement {
30    requester: String,
31    spec: String,
32}
33
34/// Resolve the full dependency tree from package.json with deterministic conflict handling.
35/// Prefetches direct deps in parallel, then uses cached packuments for the rest.
36/// For conflicts, prefers the highest version that satisfies the combined specs; errors if none match.
37pub fn resolve_full_tree(package_json_path: &Path) -> Result<HashMap<String, ResolvedPackage>, String> {
38    let deps = lockfile::read_package_json_deps(package_json_path)
39        .ok_or("Could not read package.json dependencies.")?;
40    if deps.is_empty() {
41        return Ok(HashMap::new());
42    }
43
44    let direct_names: Vec<String> = deps.keys().cloned().collect();
45    let cache_arc = Arc::new(Mutex::new(HashMap::<String, serde_json::Value>::new()));
46    let results = registry::parallel_fetch_metadata(&direct_names, &cache_arc);
47    for (name, res) in results {
48        if let Err(e) = res {
49            return Err(format!("Failed to fetch metadata for {}: {}", name, e));
50        }
51    }
52
53    let mut tree: HashMap<String, ResolvedPackage> = HashMap::new();
54    let mut seen: HashSet<(String, String)> = HashSet::new();
55    let mut requirements: HashMap<String, Vec<Requirement>> = HashMap::new();
56    let mut peer_requirements: HashMap<String, Vec<PeerRequirement>> = HashMap::new();
57
58    let mut queue: Vec<(String, String, String, String)> = deps
59        .iter()
60        .map(|(name, spec)| {
61            requirements
62                .entry(name.clone())
63                .or_default()
64                .push(Requirement {
65                    requester: "root".to_string(),
66                    spec: spec.clone(),
67                });
68            (
69                format!("node_modules/{}", name),
70                name.clone(),
71                spec.clone(),
72                "root".to_string(),
73            )
74        })
75        .collect();
76
77    let mut conflicts: Vec<String> = Vec::new();
78    while let Some((key, name, spec, requester)) = queue.pop() {
79        let meta = {
80            let mut cache = cache_arc.lock().unwrap();
81            registry::fetch_metadata_cached(&name, &mut *cache)?
82        };
83
84        requirements
85            .entry(name.clone())
86            .or_default()
87            .push(Requirement { requester, spec });
88
89        let combined_specs = requirements
90            .get(&name)
91            .map(|reqs| reqs.iter().map(|r| r.spec.clone()).collect::<Vec<_>>())
92            .unwrap_or_default();
93
94        let version = resolve_highest_satisfying(&meta, &combined_specs).ok_or_else(|| {
95            let refs = requirements
96                .get(&name)
97                .map(|reqs| {
98                    reqs.iter()
99                        .map(|r| format!("{} -> {}", r.requester, r.spec))
100                        .collect::<Vec<_>>()
101                        .join(", ")
102                })
103                .unwrap_or_default();
104            format!("Dependency conflict for {} (no version satisfies all): {}", name, refs)
105        })?;
106
107        if let Some(existing) = tree.get(&key) {
108            if existing.version == version {
109                continue;
110            }
111            let combined_specs_str = combined_specs.join(", ");
112            conflicts.push(format!(
113                "{}: existing {} vs {} (specs: {})",
114                name, existing.version, version, combined_specs_str
115            ));
116            continue;
117        }
118        if seen.contains(&(name.clone(), version.clone())) {
119            continue;
120        }
121        seen.insert((name.clone(), version.clone()));
122
123        let resolved_url = registry::get_tarball_url(&meta, &version)
124            .ok_or_else(|| format!("No tarball URL for {}@{}", name, version))?;
125        let integrity = registry::get_integrity_for_version(&meta, &version);
126
127        let version_deps = registry::get_version_dependencies(&meta, &version);
128        let peer_deps = registry::get_version_peer_dependencies(&meta, &version);
129        let peer_deps_meta = registry::get_version_peer_dependencies_meta(&meta, &version);
130        let mut resolved_deps = HashMap::new();
131        for (dep_name, dep_spec) in &version_deps {
132            let dep_meta = {
133                let mut cache = cache_arc.lock().unwrap();
134                match registry::fetch_metadata_cached(dep_name, &mut *cache) {
135                    Ok(m) => m,
136                    Err(_) => continue,
137                }
138            };
139            requirements
140                .entry(dep_name.clone())
141                .or_default()
142                .push(Requirement {
143                    requester: name.clone(),
144                    spec: dep_spec.clone(),
145                });
146            if let Some(dep_version) = resolve_highest_satisfying(&dep_meta, &[dep_spec.clone()]) {
147                resolved_deps.insert(dep_name.clone(), dep_version.clone());
148                let dep_key = format!("node_modules/{}", dep_name);
149                if !seen.contains(&(dep_name.clone(), dep_version)) {
150                    queue.push((dep_key, dep_name.clone(), dep_spec.clone(), name.clone()));
151                }
152            }
153        }
154
155        for (peer_name, peer_spec) in &peer_deps {
156            let optional = peer_deps_meta
157                .get(peer_name)
158                .and_then(|v| v.get("optional"))
159                .and_then(|v| v.as_bool())
160                .unwrap_or(false);
161            peer_requirements
162                .entry(peer_name.clone())
163                .or_default()
164                .push(PeerRequirement {
165                    requester: name.clone(),
166                    spec: peer_spec.clone(),
167                    optional,
168                });
169        }
170
171        tree.insert(
172            key,
173            ResolvedPackage {
174                version,
175                resolved: resolved_url,
176                integrity,
177                dependencies: resolved_deps,
178                peer_dependencies: peer_deps,
179                peer_dependencies_meta: peer_deps_meta,
180            },
181        );
182    }
183
184    let mut peer_conflicts: Vec<String> = Vec::new();
185    for (peer_name, reqs) in &peer_requirements {
186        let resolved_version = tree
187            .get(&format!("node_modules/{}", peer_name))
188            .map(|p| p.version.clone());
189        if let Some(resolved) = resolved_version {
190            for req in reqs {
191                if !registry::version_satisfies(&req.spec, &resolved) {
192                    peer_conflicts.push(format!(
193                        "peer {} required by {} but resolved {} (spec {})",
194                        peer_name, req.requester, resolved, req.spec
195                    ));
196                }
197            }
198        } else {
199            let required_reqs: Vec<&PeerRequirement> = reqs.iter().filter(|r| !r.optional).collect();
200            if required_reqs.is_empty() {
201                continue;
202            }
203            let req_list = required_reqs
204                .iter()
205                .map(|r| format!("{} -> {}", r.requester, r.spec))
206                .collect::<Vec<_>>()
207                .join(", ");
208            peer_conflicts.push(format!("peer {} missing (requirements: {})", peer_name, req_list));
209        }
210    }
211
212    if !conflicts.is_empty() || !peer_conflicts.is_empty() {
213        let mut all = Vec::new();
214        all.extend(conflicts);
215        all.extend(peer_conflicts);
216        return Err(format!(
217            "Dependency conflict: {}. Consider updating dependencies or using a single version.",
218            all.join("; ")
219        ));
220    }
221    Ok(tree)
222}
223
224fn resolve_highest_satisfying(meta: &serde_json::Value, specs: &[String]) -> Option<String> {
225    let versions = meta.get("versions")?.as_object()?;
226    let mut parsed: Vec<semver::Version> = versions
227        .keys()
228        .filter_map(|v| semver::Version::parse(v).ok())
229        .collect();
230    parsed.sort();
231    parsed.reverse();
232    for ver in parsed {
233        let ver_str = ver.to_string();
234        if specs.iter().all(|s| registry::version_satisfies(s, &ver_str)) {
235            return Some(ver_str);
236        }
237    }
238    None
239}
240
241/// Read root package name and version from package.json.
242fn read_root_package_info(path: &Path) -> Result<(String, String), String> {
243    let s = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
244    let v: serde_json::Value = serde_json::from_str(&s).map_err(|e| e.to_string())?;
245    let name = v
246        .get("name")
247        .and_then(|n| n.as_str())
248        .unwrap_or("")
249        .to_string();
250    let version = v
251        .get("version")
252        .and_then(|v| v.as_str())
253        .unwrap_or("0.0.0")
254        .to_string();
255    Ok((name, version))
256}
257
258/// Build lockfile packages object: root "" + all node_modules/* entries.
259fn build_packages_json(
260    root_name: &str,
261    root_version: &str,
262    direct_dep_names: &[String],
263    tree: &HashMap<String, ResolvedPackage>,
264) -> serde_json::Value {
265    let mut packages = serde_json::Map::new();
266
267    let mut root_deps = serde_json::Map::new();
268    for name in direct_dep_names {
269        let key = format!("node_modules/{}", name);
270        if let Some(pkg) = tree.get(&key) {
271            root_deps.insert(name.clone(), serde_json::Value::String(pkg.version.clone()));
272        }
273    }
274    packages.insert(
275        "".to_string(),
276        serde_json::json!({
277            "name": root_name,
278            "version": root_version,
279            "dependencies": root_deps,
280        }),
281    );
282
283    for (key, pkg) in tree {
284        let mut entry = serde_json::Map::new();
285        entry.insert("version".to_string(), serde_json::Value::String(pkg.version.clone()));
286        entry.insert("resolved".to_string(), serde_json::Value::String(pkg.resolved.clone()));
287        if let Some(ref i) = pkg.integrity {
288            entry.insert("integrity".to_string(), serde_json::Value::String(i.clone()));
289        }
290        let deps: serde_json::Map<String, serde_json::Value> = pkg
291            .dependencies
292            .iter()
293            .map(|(k, v)| (k.clone(), serde_json::Value::String(v.clone())))
294            .collect();
295        let peer_deps: serde_json::Map<String, serde_json::Value> = pkg
296            .peer_dependencies
297            .iter()
298            .map(|(k, v)| (k.clone(), serde_json::Value::String(v.clone())))
299            .collect();
300        let peer_deps_meta: serde_json::Map<String, serde_json::Value> = pkg
301            .peer_dependencies_meta
302            .iter()
303            .map(|(k, v)| (k.clone(), v.clone()))
304            .collect();
305        entry.insert("dependencies".to_string(), serde_json::Value::Object(deps));
306        entry.insert("requires".to_string(), serde_json::Value::Bool(!pkg.dependencies.is_empty()));
307        if !peer_deps.is_empty() {
308            entry.insert("peerDependencies".to_string(), serde_json::Value::Object(peer_deps));
309        }
310        if !peer_deps_meta.is_empty() {
311            entry.insert("peerDependenciesMeta".to_string(), serde_json::Value::Object(peer_deps_meta));
312        }
313        packages.insert(key.clone(), serde_json::Value::Object(entry));
314    }
315
316    serde_json::Value::Object(packages)
317}
318
319/// Write package-lock.json to the given path.
320pub fn write_package_lock(
321    lock_path: &Path,
322    package_json_path: &Path,
323    tree: &HashMap<String, ResolvedPackage>,
324) -> Result<(), String> {
325    let (root_name, root_version) = read_root_package_info(package_json_path)?;
326    let deps = lockfile::read_package_json_deps(package_json_path).unwrap_or_default();
327    let direct_dep_names: Vec<String> = deps.keys().cloned().collect();
328
329    let packages = build_packages_json(&root_name, &root_version, &direct_dep_names, tree);
330
331    let lockfile_content = serde_json::json!({
332        "name": root_name,
333        "version": root_version,
334        "lockfileVersion": 3,
335        "packages": packages,
336    });
337
338    let pretty = serde_json::to_string_pretty(&lockfile_content).map_err(|e| e.to_string())?;
339    std::fs::write(lock_path, pretty).map_err(|e| e.to_string())?;
340    Ok(())
341}