diff --git a/src/cache/cached_path.rs b/src/cache/cached_path.rs index 5389b656..4f7c7ea8 100644 --- a/src/cache/cached_path.rs +++ b/src/cache/cached_path.rs @@ -31,6 +31,9 @@ pub struct CachedPathImpl { pub tsconfig: OnceLock>>, /// `tsconfig.json` after resolving `references`, `files`, `include` and `extend`. pub resolved_tsconfig: OnceLock>>, + /// Cached result of `find_tsconfig_auto` walk from this directory upward. + /// Only accessed via `get()`/`set()` to avoid deadlocks. + pub nearest_tsconfig: OnceLock>>, } impl CachedPathImpl { @@ -53,6 +56,7 @@ impl CachedPathImpl { package_json: OnceLock::new(), tsconfig: OnceLock::new(), resolved_tsconfig: OnceLock::new(), + nearest_tsconfig: OnceLock::new(), } } } diff --git a/src/tsconfig_resolver.rs b/src/tsconfig_resolver.rs index ec0cd3f7..1eabcbd8 100644 --- a/src/tsconfig_resolver.rs +++ b/src/tsconfig_resolver.rs @@ -70,11 +70,11 @@ impl ResolverGeneric { if !cached_path.path.is_absolute() { return Ok(None); } - let span = tracing::debug_span!("find_tsconfig", path = %cached_path); - let _enter = span.enter(); cached_path .resolved_tsconfig .get_or_try_init(|| { + let span = tracing::debug_span!("find_tsconfig", path = %cached_path); + let _enter = span.enter(); self.find_tsconfig_impl(cached_path).map(|option_tsconfig| { option_tsconfig.map(|tsconfig| { let r = TsConfig::resolve_tsconfig_solution(tsconfig, cached_path.path()); @@ -108,7 +108,21 @@ impl ResolverGeneric { ) -> Result>, ResolveError> { let mut ctx = Ctx::default(); let mut cache_value = Some(cached_path.clone()); + // Track visited paths to propagate the walk result back. + // After a walk completes, all visited paths get the result cached in `nearest_tsconfig`. + // Future walks that reach any of these paths short-circuit in O(1). + // Pre allocate 8 slots for the most common cases. + let mut visited: Vec = Vec::with_capacity(8); while let Some(cv) = cache_value { + // if a previous walk already resolved this path, + // reuse that result and propagate to everything we visited on the way here. + if let Some(result) = cv.nearest_tsconfig.get() { + for v in &visited { + v.nearest_tsconfig.get_or_init(|| result.clone()); + } + return Ok(result.clone()); + } + visited.push(cv.clone()); if let Some(tsconfig) = cv.tsconfig.get_or_try_init(|| { let tsconfig_path = cv.path.join("tsconfig.json"); let tsconfig_path = self.cache.value(&tsconfig_path); @@ -124,10 +138,18 @@ impl ResolverGeneric { Ok(None) } })? { - return Ok(Some(Arc::clone(tsconfig))); + let result = Some(Arc::clone(tsconfig)); + for v in &visited { + v.nearest_tsconfig.get_or_init(|| result.clone()); + } + return Ok(result); } cache_value = cv.parent(&self.cache); } + // No tsconfig found — propagate None to all visited paths. + for v in &visited { + v.nearest_tsconfig.get_or_init(|| None); + } Ok(None) }