use std::hash::Hash;
use std::hash::Hasher;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Mutex;
use std::sync::OnceLock;
use deno_npm::resolution::ValidSerializedNpmResolutionSnapshot;
use deno_npmrc::NpmRegistryUrl;
use sys_traits::impls::RealSys;
use sys_traits::EnvHomeDir;
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct NpmCacheKey {
pub cwd: PathBuf,
pub node_modules_exists: bool,
pub package_json: Option<u64>,
pub project_npmrc: Option<(u64, u64)>,
pub global_npmrc: Option<(u64, u64)>,
pub registry: String,
}
pub fn compute_key(cwd: &Path) -> NpmCacheKey {
let canonical = std::fs::canonicalize(cwd).unwrap_or_else(|_| cwd.to_path_buf());
NpmCacheKey {
cwd: canonical.clone(),
node_modules_exists: canonical.join("node_modules").exists(),
package_json: content_hash(&canonical.join("package.json")),
project_npmrc: stat_fingerprint(&canonical.join(".npmrc")),
global_npmrc: global_npmrc_path().and_then(|p| stat_fingerprint(&p)),
registry: NpmRegistryUrl::for_npm(&RealSys).url.to_string(),
}
}
fn global_npmrc_path() -> Option<PathBuf> {
std::env::var_os("NPM_CONFIG_USERCONFIG")
.map(PathBuf::from)
.or_else(|| RealSys.env_home_dir().map(|h| h.join(".npmrc")))
}
static CACHE: OnceLock<Mutex<Vec<(NpmCacheKey, ValidSerializedNpmResolutionSnapshot)>>> =
OnceLock::new();
const MAX_ENTRIES: usize = 8;
fn cache() -> &'static Mutex<Vec<(NpmCacheKey, ValidSerializedNpmResolutionSnapshot)>> {
CACHE.get_or_init(|| Mutex::new(Vec::new()))
}
pub fn get(key: &NpmCacheKey) -> Option<ValidSerializedNpmResolutionSnapshot> {
let entries = cache().lock().unwrap_or_else(|e| e.into_inner());
entries
.iter()
.find(|(k, _)| k == key)
.map(|(_, snapshot)| snapshot.clone())
}
pub fn insert(key: NpmCacheKey, snapshot: ValidSerializedNpmResolutionSnapshot) {
let mut entries = cache().lock().unwrap_or_else(|e| e.into_inner());
if let Some(entry) = entries.iter_mut().find(|(k, _)| *k == key) {
entry.1 = snapshot;
return;
}
entries.push((key, snapshot));
if entries.len() > MAX_ENTRIES {
entries.remove(0);
}
}
fn stat_fingerprint(path: &Path) -> Option<(u64, u64)> {
let meta = std::fs::metadata(path).ok()?;
let mtime = meta
.modified()
.ok()?
.duration_since(std::time::UNIX_EPOCH)
.ok()?
.as_nanos() as u64;
Some((mtime, meta.len()))
}
pub(crate) fn content_hash(path: &Path) -> Option<u64> {
let bytes = std::fs::read(path).ok()?;
let mut hasher = std::collections::hash_map::DefaultHasher::new();
bytes.hash(&mut hasher);
Some(hasher.finish())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fifo_eviction_and_replace() {
let key = |n: u64| NpmCacheKey {
cwd: PathBuf::from(format!("/proj/{n}")),
node_modules_exists: false,
package_json: Some(n),
project_npmrc: None,
global_npmrc: None,
registry: String::new(),
};
for n in 0..9 {
insert(key(n), ValidSerializedNpmResolutionSnapshot::default());
}
assert!(get(&key(0)).is_none(), "oldest entry should be evicted");
assert!(get(&key(8)).is_some(), "newest entry should be present");
insert(key(8), ValidSerializedNpmResolutionSnapshot::default());
assert!(get(&key(8)).is_some());
insert(key(9), ValidSerializedNpmResolutionSnapshot::default());
assert!(get(&key(1)).is_none(), "next oldest should be evicted");
assert!(get(&key(9)).is_some());
}
}