use std::{
borrow::Cow,
collections::HashSet as StdHashSet,
hash::{BuildHasherDefault, Hash, Hasher},
io,
path::{Path, PathBuf},
sync::Arc,
};
use cfg_if::cfg_if;
#[cfg(feature = "yarn_pnp")]
use once_cell::sync::OnceCell;
use papaya::{HashMap, HashSet};
use rustc_hash::FxHasher;
use super::borrowed_path::BorrowedCachedPath;
use super::cached_path::{CachedPath, CachedPathImpl};
use super::hasher::IdentityHasher;
use crate::{
FileSystem, PackageJson, ResolveError, ResolveOptions, TsConfig,
context::ResolveContext as Ctx, path::PathUtil,
};
#[derive(Default)]
pub struct Cache<Fs> {
pub(crate) fs: Fs,
pub(crate) paths: HashSet<CachedPath, BuildHasherDefault<IdentityHasher>>,
pub(crate) tsconfigs_raw: HashMap<PathBuf, Arc<TsConfig>, BuildHasherDefault<FxHasher>>,
pub(crate) tsconfigs_built: HashMap<PathBuf, Arc<TsConfig>, BuildHasherDefault<FxHasher>>,
#[cfg(feature = "yarn_pnp")]
pub(crate) yarn_pnp_manifest: OnceCell<pnp::Manifest>,
}
impl<Fs: FileSystem> Cache<Fs> {
pub fn clear(&self) {
self.paths.pin().clear();
self.tsconfigs_raw.pin().clear();
self.tsconfigs_built.pin().clear();
}
#[allow(clippy::cast_possible_truncation)]
pub(crate) fn value(&self, path: &Path) -> CachedPath {
let hash = {
let mut hasher = FxHasher::default();
path.as_os_str().hash(&mut hasher);
hasher.finish()
};
let paths = self.paths.pin();
if let Some(entry) = paths.get(&BorrowedCachedPath { hash, path }) {
return entry.clone();
}
let parent = path.parent().map(|p| self.value(p));
let is_node_modules = path.file_name().as_ref().is_some_and(|&name| name == "node_modules");
let inside_node_modules =
is_node_modules || parent.as_ref().is_some_and(|parent| parent.inside_node_modules);
let parent_weak = parent.as_ref().map(|p| Arc::downgrade(&p.0));
let cached_path = CachedPath(Arc::new(CachedPathImpl::new(
hash,
path.to_path_buf().into_boxed_path(),
is_node_modules,
inside_node_modules,
parent_weak,
)));
paths.insert(cached_path.clone());
cached_path
}
pub(crate) fn canonicalize(&self, path: &CachedPath) -> Result<PathBuf, ResolveError> {
let cached_path = self.canonicalize_impl(path)?;
let path = cached_path.to_path_buf();
cfg_if! {
if #[cfg(target_os = "windows")] {
crate::windows::strip_windows_prefix(path)
} else {
Ok(path)
}
}
}
pub(crate) fn is_file(&self, path: &CachedPath, ctx: &mut Ctx) -> bool {
if path.is_file(&self.fs).is_some_and(|b| b) {
ctx.add_file_dependency(path.path());
true
} else {
ctx.add_missing_dependency(path.path());
false
}
}
pub(crate) fn is_dir(&self, path: &CachedPath, ctx: &mut Ctx) -> bool {
path.is_dir(&self.fs).unwrap_or_else(|| {
ctx.add_missing_dependency(path.path());
false
})
}
pub(crate) fn get_package_json(
&self,
path: &CachedPath,
options: &ResolveOptions,
ctx: &mut Ctx,
) -> Result<Option<Arc<PackageJson>>, ResolveError> {
self.find_package_json(path, options, ctx).map(|option_package_json| {
option_package_json.filter(|package_json| {
package_json
.path()
.parent()
.is_some_and(|p| p.as_os_str() == path.path().as_os_str())
})
})
}
pub(crate) fn find_package_json(
&self,
path: &CachedPath,
options: &ResolveOptions,
ctx: &mut Ctx,
) -> Result<Option<Arc<PackageJson>>, ResolveError> {
let mut path = path.clone();
while !self.is_dir(&path, ctx) {
if let Some(cv) = path.parent(self) {
path = cv;
} else {
break;
}
}
self.find_package_json_impl(&path, options, ctx)
}
fn find_package_json_impl(
&self,
path: &CachedPath,
options: &ResolveOptions,
ctx: &mut Ctx,
) -> Result<Option<Arc<PackageJson>>, ResolveError> {
path.package_json
.get_or_try_init(|| {
let package_json_path = path.path.join("package.json");
let Ok(package_json_bytes) = self.fs.read(&package_json_path) else {
if let Some(deps) = &mut ctx.missing_dependencies {
deps.push(package_json_path);
}
return path.parent(self).map_or(Ok(None), |parent| {
self.find_package_json_impl(&parent, options, ctx)
});
};
let real_path = if options.symlinks {
self.canonicalize(path)?.join("package.json")
} else {
package_json_path.clone()
};
PackageJson::parse(
&self.fs,
package_json_path.clone(),
real_path,
package_json_bytes,
)
.map(|package_json| Some(Arc::new(package_json)))
.map_err(ResolveError::Json)
.inspect(|_| {
ctx.add_file_dependency(&package_json_path);
})
.inspect_err(|_| {
if let Some(deps) = &mut ctx.file_dependencies {
deps.push(package_json_path.clone());
}
})
})
.cloned()
}
pub(crate) fn get_tsconfig<F: FnOnce(&mut TsConfig) -> Result<(), ResolveError>>(
&self,
root: bool,
path: &Path,
callback: F, ) -> Result<Arc<TsConfig>, ResolveError> {
if root {
let tsconfigs_built = self.tsconfigs_built.pin();
if let Some(tsconfig) = tsconfigs_built.get(path) {
return Ok(Arc::clone(tsconfig));
}
}
if !root {
let tsconfigs_raw = self.tsconfigs_raw.pin();
if let Some(tsconfig) = tsconfigs_raw.get(path) {
return Ok(Arc::clone(tsconfig));
}
}
let meta = self.fs.metadata(path).ok();
let tsconfig_path = if meta.is_some_and(|m| m.is_file) {
Cow::Borrowed(path)
} else if meta.is_some_and(|m| m.is_dir) {
Cow::Owned(path.join("tsconfig.json"))
} else {
let mut os_string = path.to_path_buf().into_os_string();
os_string.push(".json");
Cow::Owned(PathBuf::from(os_string))
};
let tsconfig_string = self.fs.read_to_string(&tsconfig_path).map_err(|err| {
if err.kind() == io::ErrorKind::NotFound {
ResolveError::TsconfigNotFound(path.to_path_buf())
} else {
ResolveError::from(err)
}
})?;
let mut tsconfig =
TsConfig::parse(root, &tsconfig_path, tsconfig_string).map_err(|error| {
ResolveError::from_serde_json_error(tsconfig_path.to_path_buf(), &error)
})?;
callback(&mut tsconfig)?;
tsconfig.set_should_build(false);
let raw_tsconfig = Arc::new(tsconfig.clone());
self.tsconfigs_raw.pin().insert(path.to_path_buf(), Arc::clone(&raw_tsconfig));
if root {
tsconfig.set_should_build(true);
let tsconfig = Arc::new(tsconfig.build());
self.tsconfigs_built.pin().insert(path.to_path_buf(), Arc::clone(&tsconfig));
Ok(tsconfig)
} else {
Ok(raw_tsconfig)
}
}
#[cfg(feature = "yarn_pnp")]
pub(crate) fn get_yarn_pnp_manifest(
&self,
cwd: Option<&Path>,
) -> Result<&pnp::Manifest, ResolveError> {
self.yarn_pnp_manifest.get_or_try_init(|| {
let cwd = match cwd {
Some(path) => Cow::Borrowed(path),
None => match std::env::current_dir() {
Ok(path) => Cow::Owned(path),
Err(err) => return Err(ResolveError::from(err)),
},
};
let manifest = match pnp::find_pnp_manifest(&cwd) {
Ok(manifest) => match manifest {
Some(manifest) => manifest,
None => {
return Err(ResolveError::FailedToFindYarnPnpManifest(cwd.to_path_buf()));
}
},
Err(err) => return Err(ResolveError::YarnPnpError(err)),
};
Ok(manifest)
})
}
}
impl<Fs: FileSystem> Cache<Fs> {
pub fn new(fs: Fs) -> Self {
Self {
fs,
paths: HashSet::builder()
.hasher(BuildHasherDefault::default())
.resize_mode(papaya::ResizeMode::Blocking)
.build(),
tsconfigs_raw: HashMap::builder()
.hasher(BuildHasherDefault::default())
.resize_mode(papaya::ResizeMode::Blocking)
.build(),
tsconfigs_built: HashMap::builder()
.hasher(BuildHasherDefault::default())
.resize_mode(papaya::ResizeMode::Blocking)
.build(),
#[cfg(feature = "yarn_pnp")]
yarn_pnp_manifest: OnceCell::new(),
}
}
pub(crate) fn canonicalize_impl(&self, path: &CachedPath) -> Result<CachedPath, ResolveError> {
let mut visited = StdHashSet::with_hasher(BuildHasherDefault::<IdentityHasher>::default());
self.canonicalize_with_visited(path, &mut visited).or_else(|err| {
self.fs
.canonicalize(path.path())
.map(|canonical| self.value(&canonical))
.map_err(|_| err)
})
}
fn canonicalize_with_visited(
&self,
path: &CachedPath,
visited: &mut StdHashSet<u64, BuildHasherDefault<IdentityHasher>>,
) -> Result<CachedPath, ResolveError> {
if let Some((weak, path_buf)) = path.canonicalized.get() {
return weak
.upgrade()
.map(CachedPath)
.or_else(|| {
Some(self.value(path_buf))
})
.ok_or_else(|| {
io::Error::new(io::ErrorKind::NotFound, "Cached path no longer exists").into()
});
}
if !visited.insert(path.hash) {
return Err(io::Error::new(io::ErrorKind::NotFound, "Circular symlink").into());
}
let res = path.parent(self).map_or_else(
|| Ok(path.normalize_root(self)),
|parent| {
self.canonicalize_with_visited(&parent, visited).and_then(|parent_canonical| {
let normalized = parent_canonical
.normalize_with(path.path().strip_prefix(parent.path()).unwrap(), self);
if self.fs.symlink_metadata(path.path()).is_ok_and(|m| m.is_symlink) {
let link = self.fs.read_link(normalized.path())?;
if link.is_absolute() {
return self.canonicalize_with_visited(
&self.value(&link.normalize()),
visited,
);
} else if let Some(dir) = normalized.parent(self) {
return self.canonicalize_with_visited(
&dir.normalize_with(&link, self),
visited,
);
}
debug_assert!(
false,
"Failed to get path parent for {}.",
normalized.path().display()
);
}
Ok(normalized)
})
},
)?;
let _ = path.canonicalized.set((Arc::downgrade(&res.0), res.to_path_buf()));
visited.remove(&path.hash);
Ok(res)
}
}