Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
Empty file.
7 changes: 7 additions & 0 deletions fixtures/tsconfig/cases/simple-paths/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"compilerOptions": {
"paths": {
"bar/*": ["./bar/*"]
}
}
}
55 changes: 46 additions & 9 deletions src/cache/cache_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,19 @@ pub struct Cache<Fs> {
pub(crate) fs: Fs,
pub(crate) paths: HashSet<CachedPath, BuildHasherDefault<IdentityHasher>>,
pub(crate) package_jsons: RwLock<Vec<Arc<PackageJson>>>,
pub(crate) tsconfigs: HashMap<PathBuf, Arc<TsConfig>, BuildHasherDefault<FxHasher>>,
/// Cache for raw/unbuilt tsconfigs (used when extending).
pub(crate) tsconfigs_raw: HashMap<PathBuf, Arc<TsConfig>, BuildHasherDefault<FxHasher>>,
/// Cache for built/resolved tsconfigs (used for resolution).
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.pin().clear();
self.tsconfigs_raw.pin().clear();
self.tsconfigs_built.pin().clear();
self.package_jsons.write().clear();
}

Expand Down Expand Up @@ -210,10 +214,24 @@ impl<Fs: FileSystem> Cache<Fs> {
path: &Path,
callback: F, // callback for modifying tsconfig with `extends`
) -> Result<Arc<TsConfig>, ResolveError> {
let tsconfigs = self.tsconfigs.pin();
if let Some(tsconfig) = tsconfigs.get(path) {
return Ok(Arc::clone(tsconfig));
// For root=true (caller tsconfig), check built cache first
if root {
let tsconfigs_built = self.tsconfigs_built.pin();
if let Some(tsconfig) = tsconfigs_built.get(path) {
return Ok(Arc::clone(tsconfig));
}
}

// Check raw cache (callback applied, not built) - only for root=false
// For root=true, we need to run the callback to ensure extends are processed
if !root {
let tsconfigs_raw = self.tsconfigs_raw.pin();
if let Some(tsconfig) = tsconfigs_raw.get(path) {
return Ok(Arc::clone(tsconfig));
}
}

// Not in any cache, parse from file
let meta = self.fs.metadata(path).ok();
let tsconfig_path = if meta.is_some_and(|m| m.is_file) {
Cow::Borrowed(path)
Expand All @@ -232,10 +250,25 @@ impl<Fs: FileSystem> Cache<Fs> {
TsConfig::parse(root, &tsconfig_path, tsconfig_string).map_err(|error| {
ResolveError::from_serde_json_error(tsconfig_path.to_path_buf(), &error)
})?;

// Run callback (extends/references processing)
callback(&mut tsconfig)?;
let tsconfig = Arc::new(tsconfig.build());
tsconfigs.insert(path.to_path_buf(), Arc::clone(&tsconfig));
Ok(tsconfig)

// Cache raw version (callback applied, not built)
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 {
// Build and cache built version
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 {
// Return unbuilt version
Ok(raw_tsconfig)
}
}

#[cfg(feature = "yarn_pnp")]
Expand Down Expand Up @@ -274,7 +307,11 @@ impl<Fs: FileSystem> Cache<Fs> {
.resize_mode(papaya::ResizeMode::Blocking)
.build(),
package_jsons: RwLock::new(Vec::with_capacity(512)),
tsconfigs: HashMap::builder()
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(),
Expand Down
48 changes: 48 additions & 0 deletions src/tests/tsconfig_paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,54 @@ fn test_parent_base_url() {
}
}

#[test]
fn test_tsconfig_mixed_root_non_root_cache() {
let f = super::fixture_root().join("tsconfig");
let f2 = f.join("cases").join("simple-paths");

// NOTE: should remove this line before merging as it's not thread safe
std::env::set_current_dir(&f2).unwrap();
Comment on lines +299 to +301
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Boshen I think you want to remove this as it may cause flaky failures.


let resolver = Resolver::new(ResolveOptions {
tsconfig: Some(TsconfigDiscovery::Auto),
..ResolveOptions::default()
});
resolver.cache.get_tsconfig(false, &f2.join("tsconfig.json"), |_| Ok(())).unwrap();
let resolved_path =
resolver.resolve_file(f2.join("foo.ts"), "bar/index.ts").map(|f| f.full_path());
assert_eq!(resolved_path, Ok(f2.join("bar/index.ts")));
}

#[test]
fn test_tsconfig_mixed_root_non_root_cache2() {
let f = super::fixture_root().join("tsconfig");
let f2 = f.join("cases").join("extends-paths");

let resolver = Resolver::new(ResolveOptions {
tsconfig: Some(TsconfigDiscovery::Auto),
..ResolveOptions::default()
});
resolver.cache.get_tsconfig(true, &f2.join("tsconfig.base.json"), |_| Ok(())).unwrap();
let resolved_path =
resolver.resolve_file(f2.join("test.ts"), "@/index.js").map(|f| f.full_path());
assert_eq!(resolved_path, Ok(f2.join("src/index.js")));
}

#[test]
fn test_tsconfig_mixed_root_non_root_cache3() {
let f = super::fixture_root().join("tsconfig");
let f2 = f.join("cases").join("extends-paths");

let resolver = Resolver::new(ResolveOptions {
tsconfig: Some(TsconfigDiscovery::Auto),
..ResolveOptions::default()
});
resolver.cache.get_tsconfig(false, &f2.join("tsconfig.json"), |_| Ok(())).unwrap();
let resolved_path =
resolver.resolve_file(f2.join("test.ts"), "@/index.js").map(|f| f.full_path());
assert_eq!(resolved_path, Ok(f2.join("src/index.js")));
}

#[cfg(not(target_os = "windows"))] // MemoryFS's path separator is always `/` so the test will not pass in windows.
mod windows_test {
use std::path::{Path, PathBuf};
Expand Down
27 changes: 22 additions & 5 deletions src/tsconfig.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,24 @@ pub type CompilerOptionsPathsMap = IndexMap<String, Vec<PathBuf>, BuildHasherDef
/// Project Reference
///
/// <https://www.typescriptlang.org/docs/handbook/project-references.html>
#[derive(Debug, Deserialize)]
#[derive(Clone, Debug, Deserialize)]
pub struct ProjectReference {
pub path: PathBuf,
}

#[derive(Debug, Default, Deserialize)]
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TsConfig {
/// Whether this is the caller tsconfig.
/// Used for final template variable substitution when all configs are extended and merged.
#[serde(skip)]
pub root: bool,

/// Whether `build()` should normalize paths.
/// Set to true when caching to ensure paths are always normalized regardless of `root`.
#[serde(skip)]
should_build: bool,

/// Path to `tsconfig.json`. Contains the `tsconfig.json` filename.
#[serde(skip)]
pub path: PathBuf,
Expand Down Expand Up @@ -109,6 +114,17 @@ impl TsConfig {
self.root
}

/// Whether `build()` should normalize paths.
#[must_use]
pub fn should_build(&self) -> bool {
self.should_build
}

/// Set whether `build()` should normalize paths.
pub fn set_should_build(&mut self, should_build: bool) {
self.should_build = should_build;
}

/// Returns the path where the `tsconfig.json` was found.
///
/// Contains the `tsconfig.json` filename.
Expand Down Expand Up @@ -294,8 +310,9 @@ impl TsConfig {
/// * `baseUrl` to absolute path
#[must_use]
pub(crate) fn build(mut self) -> Self {
// Only the root tsconfig requires paths resolution.
if !self.root() {
// Only build if should_build is true.
// This is controlled separately from `root` to avoid cache pollution.
if !self.should_build {
return self;
}

Expand Down Expand Up @@ -444,7 +461,7 @@ impl TsConfig {
/// Compiler Options
///
/// <https://www.typescriptlang.org/tsconfig#compilerOptions>
#[derive(Debug, Default, Deserialize)]
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CompilerOptions {
pub base_url: Option<PathBuf>,
Expand Down
Loading