Skip to main content

jhol_core/
prefetch.rs

1//! Prefetch: fill the store from lockfile without writing node_modules.
2
3use std::path::Path;
4
5use crate::lockfile;
6use crate::registry;
7use crate::utils;
8
9/// Package name without version (for URL construction when missing from map).
10fn base_name(package: &str) -> &str {
11    if let Some(idx) = package.rfind('@') {
12        if idx > 0 && !package[idx + 1..].contains('/') {
13            return &package[..idx];
14        }
15    }
16    package
17}
18
19/// Prefetch all lockfile dependencies into the store. Requires package.json and lockfile.
20/// Does not create node_modules or run backend. Use before `jhol install --offline`.
21pub fn prefetch_from_lockfile(quiet: bool) -> Result<(), String> {
22    let specs = crate::install::resolve_install_from_package_json(true)?;
23    if specs.is_empty() {
24        if !quiet {
25            println!("No dependencies to prefetch.");
26        }
27        return Ok(());
28    }
29    let (resolved_urls, resolved_integrity) = lockfile::read_resolved_urls_and_integrity_from_dir(Path::new("."))
30        .ok_or("No package-lock.json or bun.lock found.")?;
31    let cache_dir = std::path::PathBuf::from(utils::get_cache_dir());
32    let store_dir = cache_dir.join("store");
33    std::fs::create_dir_all(&store_dir).map_err(|e| e.to_string())?;
34
35    let mut work: Vec<(String, String, Option<String>)> = Vec::new();
36    for spec in &specs {
37        if utils::get_cached_tarball(spec).is_some() {
38            continue;
39        }
40        let url = resolved_urls.get(spec).cloned().or_else(|| {
41            let base = base_name(spec);
42            let version = spec.rfind('@').map(|i| &spec[i + 1..]).unwrap_or("latest");
43            Some(lockfile::tarball_url_from_registry(base, version))
44        });
45        if let Some(url) = url {
46            let integrity = resolved_integrity.get(spec).cloned();
47            work.push((spec.clone(), url, integrity));
48        }
49    }
50
51    const PREFETCH_CONCURRENCY: usize = 8;
52    let mut fetched = 0usize;
53    let mut index_batch: std::collections::HashMap<String, String> = std::collections::HashMap::new();
54    for chunk in work.chunks(PREFETCH_CONCURRENCY) {
55        use std::sync::mpsc;
56        use std::thread;
57        let (tx, rx) = mpsc::channel();
58        for (spec, url, integrity) in chunk {
59            let spec = spec.clone();
60            let url = url.clone();
61            let integrity = integrity.clone();
62            let cache_dir = cache_dir.clone();
63            let tx = tx.clone();
64            if !quiet {
65                println!("Prefetching {}...", spec);
66            }
67            thread::spawn(move || {
68                let res = registry::download_tarball_to_store_hash_only(
69                    &url,
70                    &cache_dir,
71                    &spec,
72                    integrity.as_deref(),
73                );
74                let _ = tx.send((spec, res));
75            });
76        }
77        drop(tx);
78        for (spec, res) in rx {
79            let hash = res?;
80            index_batch.insert(spec, hash);
81            fetched += 1;
82        }
83    }
84
85    if !index_batch.is_empty() {
86        let mut index = utils::read_store_index();
87        index.extend(index_batch);
88        utils::write_store_index(&index).map_err(|e| e.to_string())?;
89    }
90
91    if !quiet && fetched > 0 {
92        println!("Prefetched {} package(s).", fetched);
93    }
94    Ok(())
95}