Skip to content
Merged
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ tracing = "0.1.41"

simd-json = { version = "0.17.0", features = ["serde_impl", "runtime-detection"], default-features = false }

camino = { version = "1.2.2", features = ["serde1"] } # UTF-8 path types used for all internal path representations
cfg-if = "1.0"
dunce = "1.0.5" # Normalize Windows paths to the most compatible format, avoiding UNC where possible
indexmap = { version = "2.12.0", features = ["serde"] }
Expand Down
94 changes: 56 additions & 38 deletions src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ use std::{
hash::{BuildHasherDefault, Hash, Hasher},
io,
ops::Deref,
path::{Path, PathBuf},
sync::Arc,
};

use camino::{Utf8Path, Utf8PathBuf};
use dashmap::{DashMap, DashSet};
use futures::future::BoxFuture;
use rustc_hash::FxHasher;
Expand All @@ -26,7 +26,7 @@ use crate::{
pub struct Cache<Fs> {
pub(crate) fs: Fs,
paths: DashSet<CachedPath, BuildHasherDefault<IdentityHasher>>,
tsconfigs: DashMap<PathBuf, Arc<TsConfig>, BuildHasherDefault<FxHasher>>,
tsconfigs: DashMap<Utf8PathBuf, Arc<TsConfig>, BuildHasherDefault<FxHasher>>,
}

impl<Fs: Send + Sync + FileSystem> Cache<Fs> {
Expand All @@ -43,8 +43,8 @@ impl<Fs: Send + Sync + FileSystem> Cache<Fs> {
self.tsconfigs.clear();
}

pub fn value(&self, path: &Path) -> CachedPath {
let hash = hash_path(path);
pub fn value(&self, path: &Utf8Path) -> CachedPath {
let hash = hash_path(path.as_std_path());
if let Some(cache_entry) = self.paths.get((hash, path).borrow() as &dyn CacheKey) {
return cache_entry.clone();
}
Expand All @@ -61,7 +61,7 @@ impl<Fs: Send + Sync + FileSystem> Cache<Fs> {
pub async fn tsconfig<F, Fut>(
&self,
root: bool,
path: &Path,
path: &Utf8Path,
callback: F, // callback for modifying tsconfig with `extends`
) -> Result<Arc<TsConfig>, ResolveError>
where
Expand All @@ -71,25 +71,25 @@ impl<Fs: Send + Sync + FileSystem> Cache<Fs> {
if let Some(tsconfig_ref) = self.tsconfigs.get(path) {
return Ok(Arc::clone(tsconfig_ref.value()));
}
let meta = self.fs.metadata(path).await.ok();
let meta = self.fs.metadata(path.as_std_path()).await.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 mut string = path.as_str().to_string();
string.push_str(".json");
Cow::Owned(Utf8PathBuf::from(string))
};
let mut tsconfig_string = self
.fs
.read_to_string(&tsconfig_path)
.read_to_string(tsconfig_path.as_std_path())
.await
.map_err(|_| ResolveError::TsconfigNotFound(path.to_path_buf()))?;
.map_err(|_| ResolveError::TsconfigNotFound(path.as_std_path().to_path_buf()))?;
let mut tsconfig =
TsConfig::parse(root, &tsconfig_path, &mut tsconfig_string).map_err(|error| {
ResolveError::from_serde_json_error(
tsconfig_path.to_path_buf(),
tsconfig_path.as_std_path().to_path_buf(),
&error,
Some(tsconfig_string),
)
Expand All @@ -114,7 +114,15 @@ impl Hash for CachedPath {

impl PartialEq for CachedPath {
fn eq(&self, other: &Self) -> bool {
self.0.path == other.0.path
// Compare through std `Path`, not camino's `Utf8Path`. std `Path`/`Components`
// equality has a raw-byte `memcmp` fast path (its own comment: "for hashmap
// lookups"), whereas camino's `Utf8Path` always walks components with no fast
// path — that regressed this hottest cache-lookup equality. `as_std_path()`
// also preserves the per-platform semantics that match `hash_path`
// (byte-wise on Unix; component-wise on Windows, so `pack1/foo` and
// `pack1\foo` stay the same entry). A plain `as_str()` byte compare would be
// inconsistent with the component-wise hash on Windows and break dedup there.
self.0.path.as_std_path() == other.0.path.as_std_path()
}
}
impl Eq for CachedPath {}
Expand All @@ -140,17 +148,17 @@ impl AsRef<CachedPathImpl> for CachedPath {
}

impl CacheKey for CachedPath {
fn tuple(&self) -> (u64, &Path) {
fn tuple(&self) -> (u64, &Utf8Path) {
(self.hash, &self.path)
}
}

pub struct CachedPathImpl {
hash: u64,
path: Box<Path>,
path: Box<Utf8Path>,
parent: Option<CachedPath>,
meta: OnceLock<Option<FileMetadata>>,
canonicalized: OnceLock<Option<PathBuf>>,
canonicalized: OnceLock<Option<Utf8PathBuf>>,
node_modules: OnceLock<Option<CachedPath>>,
package_json: OnceLock<Option<Arc<PackageJson>>>,
/// Memoized `<self.path>/package.json` `ResolverPath` for the
Expand All @@ -164,12 +172,12 @@ impl From<&CachedPathImpl> for ResolverPath {
/// only remaining work is one `Arc::from(&Path)` to materialize the shared
/// path buffer for the `ResolveContext` sink.
fn from(cached: &CachedPathImpl) -> Self {
Self::from_parts(cached.hash, Arc::from(&*cached.path))
Self::from_parts(cached.hash, Arc::from(cached.path.as_std_path()))
}
}

impl CachedPathImpl {
fn new(hash: u64, path: Box<Path>, parent: Option<CachedPath>) -> Self {
fn new(hash: u64, path: Box<Utf8Path>, parent: Option<CachedPath>) -> Self {
Self {
hash,
path,
Expand All @@ -191,11 +199,11 @@ impl CachedPathImpl {
.clone()
}

pub fn path(&self) -> &Path {
pub fn path(&self) -> &Utf8Path {
&self.path
}

pub fn to_path_buf(&self) -> PathBuf {
pub fn to_path_buf(&self) -> Utf8PathBuf {
self.path.to_path_buf()
}

Expand All @@ -211,7 +219,7 @@ impl CachedPathImpl {
}
*self
.meta
.get_or_init(|| async { fs.metadata(&self.path).await.ok() })
.get_or_init(|| async { fs.metadata(self.path.as_std_path()).await.ok() })
.await
}

Expand All @@ -235,7 +243,7 @@ impl CachedPathImpl {
)
}

pub async fn realpath<Fs: FileSystem + Send + Sync>(&self, fs: &Fs) -> io::Result<PathBuf> {
pub async fn realpath<Fs: FileSystem + Send + Sync>(&self, fs: &Fs) -> io::Result<Utf8PathBuf> {
// Cache hit: avoid the heap-allocated `Box::pin` for the cache-miss state machine
// by returning before delegating to the boxed recursive helper.
if let Some(cached) = self.canonicalized.get() {
Expand All @@ -247,22 +255,25 @@ impl CachedPathImpl {
fn realpath_uncached<'a, Fs: FileSystem + Send + Sync>(
&'a self,
fs: &'a Fs,
) -> BoxFuture<'a, io::Result<PathBuf>> {
) -> BoxFuture<'a, io::Result<Utf8PathBuf>> {
Box::pin(async move {
self
.canonicalized
.get_or_try_init(|| async move {
if fs
.symlink_metadata(&self.path)
.symlink_metadata(self.path.as_std_path())
.await
.is_ok_and(|m| m.is_symlink)
{
return fs.canonicalize(&self.path).await.map(Some);
return fs
.canonicalize(self.path.as_std_path())
.await
.map(|path| Some(Utf8PathBuf::from_path_buf(path).expect("path should be UTF-8")));
}
if let Some(parent) = self.parent() {
let parent_path = parent.realpath(fs).await?;
return Ok(Some(
parent_path.normalize_with(self.path.strip_prefix(&parent.path).unwrap()),
parent_path.normalize_with(self.path.strip_prefix(parent.path()).unwrap()),
));
}
Ok(None)
Expand Down Expand Up @@ -311,7 +322,7 @@ impl CachedPathImpl {
/// # Errors
///
/// * [ResolveError::JSON]
#[cfg_attr(feature="enable_instrument", tracing::instrument(level=tracing::Level::DEBUG, skip_all, fields(path = %self.path.display())))]
#[cfg_attr(feature="enable_instrument", tracing::instrument(level=tracing::Level::DEBUG, skip_all, fields(path = %self.path)))]
pub async fn find_package_json<Fs: FileSystem + Send + Sync>(
&self,
fs: &Fs,
Expand Down Expand Up @@ -342,7 +353,7 @@ impl CachedPathImpl {
/// # Errors
///
/// * [ResolveError::JSON]
#[cfg_attr(feature="enable_instrument", tracing::instrument(level=tracing::Level::DEBUG, skip_all, fields(path = %self.path.display())))]
#[cfg_attr(feature="enable_instrument", tracing::instrument(level=tracing::Level::DEBUG, skip_all, fields(path = %self.path)))]
pub async fn package_json<Fs: FileSystem + Send + Sync>(
&self,
fs: &Fs,
Expand All @@ -366,19 +377,24 @@ impl CachedPathImpl {
.package_json
.get_or_try_init(|| async {
let package_json_path = self.path.join("package.json");
let Ok(package_json_string) = fs.read(&package_json_path).await else {
let Ok(package_json_string) = fs.read(package_json_path.as_std_path()).await else {
return Ok(None);
};
let real_path = if options.symlinks {
self.realpath(fs).await?.join("package.json")
} else {
package_json_path.clone()
};
match PackageJson::parse(package_json_path.clone(), real_path, package_json_string) {
match PackageJson::parse(
package_json_path.clone().into(),
real_path.into(),
package_json_string,
) {
Ok(v) => Ok(Some(Arc::new(v))),
Err(parse_err) => {
let package_json_path = self.path.join("package.json");
let package_json_string = match fs.read_to_string(&package_json_path).await {
let package_json_string = match fs.read_to_string(package_json_path.as_std_path()).await
{
Ok(c) => c,
Err(io_err) => {
return Err(ResolveError::from(io_err));
Expand All @@ -388,15 +404,15 @@ impl CachedPathImpl {

if let Some(err) = serde_err {
Err(ResolveError::from_serde_json_error(
package_json_path,
package_json_path.into(),
&err,
Some(package_json_string),
))
} else {
let (line, column) = off_to_location(&package_json_string, parse_err.index());

Err(ResolveError::JSON(JSONError {
path: package_json_path,
path: package_json_path.into(),
message: parse_err.error().to_string(),
line,
column,
Expand Down Expand Up @@ -432,7 +448,7 @@ impl CachedPathImpl {

/// Memoized cache key, code adapted from <https://stackoverflow.com/a/50478038>.
trait CacheKey {
fn tuple(&self) -> (u64, &Path);
fn tuple(&self) -> (u64, &Utf8Path);
}

impl Hash for dyn CacheKey + '_ {
Expand All @@ -443,19 +459,21 @@ impl Hash for dyn CacheKey + '_ {

impl PartialEq for dyn CacheKey + '_ {
fn eq(&self, other: &Self) -> bool {
self.tuple().1 == other.tuple().1
// std `Path` equality (memcmp fast path + per-platform semantics matching
// `hash_path`); see `CachedPath`'s `PartialEq` for the full rationale.
self.tuple().1.as_std_path() == other.tuple().1.as_std_path()
}
}

impl Eq for dyn CacheKey + '_ {}

impl CacheKey for (u64, &Path) {
fn tuple(&self) -> (u64, &Path) {
impl CacheKey for (u64, &Utf8Path) {
fn tuple(&self) -> (u64, &Utf8Path) {
(self.0, self.1)
}
}

impl<'a> Borrow<dyn CacheKey + 'a> for (u64, &'a Path) {
impl<'a> Borrow<dyn CacheKey + 'a> for (u64, &'a Utf8Path) {
fn borrow(&self) -> &(dyn CacheKey + 'a) {
self
}
Expand Down
Loading
Loading