Skip to content
Draft
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
16 changes: 16 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 @@ -78,6 +78,7 @@ name = "resolver"

[dependencies]
cfg-if = "1"
fast-glob = "1.0.0"
indexmap = { version = "2", features = ["serde"] }
json-strip-comments = "3"
once_cell = "1" # Use `std::sync::OnceLock::get_or_try_init` when it is stable.
Expand Down
Copy link
Member

Choose a reason for hiding this comment

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

It would be nice to add a test case where tsconfig.app.json extends tsconfig.json and tsconfig.json references tsconfig.app.json.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const pkgAIndex = 'pkg-a';
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"baseUrl": ".",
"paths": {
"@pkg-a/*": ["./src/*"]
}
},
"include": ["src/**/*"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const pkgBIndexJS = 'pkg-b-js';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const pkgBIndex = 'pkg-b';
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"composite": true,
"baseUrl": ".",
"allowJs": true,
"paths": {
"@pkg-b/*": ["./src/*"]
}
},
"include": ["src/**/*"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// This file should be excluded by pkg-c's exclude pattern
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const pkgCIndex = 'pkg-c';
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"composite": true,
"baseUrl": ".",
"paths": {
"@pkg-c/*": ["./lib/*"]
}
},
"include": ["lib/**/*"],
"exclude": ["lib/**/*.test.ts"]
}
1 change: 1 addition & 0 deletions fixtures/tsconfig/cases/solution_style/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const rootIndex = 'root';
15 changes: 15 additions & 0 deletions fixtures/tsconfig/cases/solution_style/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"baseUrl": ".",
"allowJs": true,
"paths": {
"root/*": ["./src/*"]
}
},
"include": ["src/**/*"],
"references": [
{ "path": "./packages/pkg-a" },
{ "path": "./packages/pkg-b" },
{ "path": "./packages/pkg-c" }
]
}
17 changes: 10 additions & 7 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,13 +199,16 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
///
/// * See [ResolveError]
pub fn resolve_tsconfig<P: AsRef<Path>>(&self, path: P) -> Result<Arc<TsConfig>, ResolveError> {
self.resolve_tsconfig_with_references(path, &TsconfigReferences::Auto)
}

pub fn resolve_tsconfig_with_references<P: AsRef<Path>>(
&self,
path: P,
references: &TsconfigReferences,
) -> Result<Arc<TsConfig>, ResolveError> {
let path = path.as_ref();
self.load_tsconfig(
true,
path,
&TsconfigReferences::Auto,
&mut TsconfigResolveContext::default(),
)
self.load_tsconfig(true, path, references, &mut TsconfigResolveContext::default())
}

/// Resolve `specifier` at absolute `path` with [ResolveContext]
Expand Down Expand Up @@ -1469,7 +1472,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
let Some(tsconfig) = self.find_tsconfig(cached_path, ctx)? else {
return Ok(None);
};
tsconfig
tsconfig.resolve_for_file(cached_path.path())
}
};

Expand Down
1 change: 1 addition & 0 deletions src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ mod tsconfig_discovery;
mod tsconfig_extends;
mod tsconfig_paths;
mod tsconfig_project_references;
mod tsconfig_solution_style;
#[cfg(target_os = "windows")]
mod windows;

Expand Down
174 changes: 174 additions & 0 deletions src/tests/tsconfig_solution_style.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
//! Tests for solution-style tsconfig resolution
//!
//! Tests the `TsConfig::resolve_for_file` method which implements solution-style
//! tsconfig resolution similar to tsconfck's `resolveSolutionTSConfig`.

use std::{path::Path, sync::Arc};

use crate::{ResolveOptions, Resolver, TsConfig};

/// Helper function to load a tsconfig with references
fn load_tsconfig(path: &Path) -> Arc<TsConfig> {
let resolver = Resolver::new(ResolveOptions::default());
resolver.resolve_tsconfig(path).unwrap()
}

#[test]
fn resolve_for_file_basic() {
let f = super::fixture_root().join("tsconfig/cases/solution_style");

// Load root tsconfig with references
let root_tsconfig = load_tsconfig(&f.join("tsconfig.json"));

// File in root src/ should use root tsconfig
let file_path = f.join("src/index.ts");
let resolved = root_tsconfig.resolve_for_file(&file_path);
assert_eq!(resolved.path(), root_tsconfig.path(), "File in src/ should use root tsconfig");

// File in pkg-a should use pkg-a's tsconfig
let file_path = f.join("packages/pkg-a/src/index.ts");
let resolved = root_tsconfig.resolve_for_file(&file_path);
assert_eq!(
resolved.path(),
f.join("packages/pkg-a/tsconfig.json"),
"File in pkg-a/src/ should use pkg-a's tsconfig"
);

// File in pkg-b should use pkg-b's tsconfig
let file_path = f.join("packages/pkg-b/src/index.ts");
let resolved = root_tsconfig.resolve_for_file(&file_path);
assert_eq!(
resolved.path(),
f.join("packages/pkg-b/tsconfig.json"),
"File in pkg-b/src/ should use pkg-b's tsconfig"
);

// File in pkg-c's lib/ should use pkg-c's tsconfig
let file_path = f.join("packages/pkg-c/lib/index.ts");
let resolved = root_tsconfig.resolve_for_file(&file_path);
assert_eq!(
resolved.path(),
f.join("packages/pkg-c/tsconfig.json"),
"File in pkg-c/lib/ should use pkg-c's tsconfig"
);
}

#[test]
fn resolve_for_file_exclude_patterns() {
let f = super::fixture_root().join("tsconfig/cases/solution_style");
let root_tsconfig = load_tsconfig(&f.join("tsconfig.json"));

// pkg-c excludes *.test.ts files
let excluded_file = f.join("packages/pkg-c/lib/index.test.ts");
let resolved = root_tsconfig.resolve_for_file(&excluded_file);

// Should fall back to root tsconfig since file is excluded from pkg-c
assert_eq!(
resolved.path(),
root_tsconfig.path(),
"Excluded files should fall back to root tsconfig"
);

// Non-excluded file should still use pkg-c's tsconfig
let included_file = f.join("packages/pkg-c/lib/index.ts");
let resolved = root_tsconfig.resolve_for_file(&included_file);
assert_eq!(
resolved.path(),
f.join("packages/pkg-c/tsconfig.json"),
"Non-excluded files should use pkg-c's tsconfig"
);
}

#[test]
fn resolve_for_file_allowjs() {
let f = super::fixture_root().join("tsconfig/cases/solution_style");
let root_tsconfig = load_tsconfig(&f.join("tsconfig.json"));

// pkg-b has allowJs: true, so .js files should use pkg-b's tsconfig
let js_file = f.join("packages/pkg-b/src/index.js");
let resolved = root_tsconfig.resolve_for_file(&js_file);
assert_eq!(
resolved.path(),
f.join("packages/pkg-b/tsconfig.json"),
"With allowJs, .js files should use pkg-b's tsconfig"
);

// pkg-a doesn't have allowJs, so .js files should fall back to root
let js_file = f.join("packages/pkg-a/src/script.js");
let resolved = root_tsconfig.resolve_for_file(&js_file);
assert_eq!(
resolved.path(),
root_tsconfig.path(),
"Without allowJs, .js files should fall back to root tsconfig"
);
}

#[test]
fn resolve_for_file_non_ts_js_files() {
let f = super::fixture_root().join("tsconfig/cases/solution_style");
let root_tsconfig = load_tsconfig(&f.join("tsconfig.json"));

// Non-TS/JS files should not trigger solution-style resolution
let json_file = f.join("packages/pkg-a/src/data.json");
let resolved = root_tsconfig.resolve_for_file(&json_file);
assert_eq!(resolved.path(), root_tsconfig.path(), "Non-TS/JS files should use root tsconfig");

// .css, .html, etc. should also fall back to root
let css_file = f.join("packages/pkg-a/src/styles.css");
let resolved = root_tsconfig.resolve_for_file(&css_file);
assert_eq!(resolved.path(), root_tsconfig.path(), "CSS files should use root tsconfig");
}

#[test]
fn resolve_for_file_no_references() {
let f = super::fixture_root().join("tsconfig/cases/solution_style");

// Load pkg-a's tsconfig which has no references
let pkg_a_tsconfig = load_tsconfig(&f.join("packages/pkg-a/tsconfig.json"));

// When a tsconfig has no references, it should always return itself
let file_in_pkg_a = f.join("packages/pkg-a/src/index.ts");
let resolved = pkg_a_tsconfig.resolve_for_file(&file_in_pkg_a);
assert_eq!(
resolved.path(),
pkg_a_tsconfig.path(),
"Tsconfig without references should always return itself"
);

// Even for files outside its directory
let file_in_pkg_b = f.join("packages/pkg-b/src/index.ts");
let resolved = pkg_a_tsconfig.resolve_for_file(&file_in_pkg_b);
assert_eq!(
resolved.path(),
pkg_a_tsconfig.path(),
"Tsconfig without references should return itself for any file"
);
}

#[test]
fn resolve_for_file_extensions() {
let f = super::fixture_root().join("tsconfig/cases/solution_style");
let root_tsconfig = load_tsconfig(&f.join("tsconfig.json"));

// Test all TS extensions
for ext in ["ts", "tsx", "mts", "cts"] {
let file = f.join(format!("packages/pkg-a/src/index.{ext}"));
let resolved = root_tsconfig.resolve_for_file(&file);
assert_eq!(
resolved.path(),
f.join("packages/pkg-a/tsconfig.json"),
".{ext} files should trigger solution-style resolution"
);
}

// Test JS extensions with allowJs
for ext in ["js", "jsx", "mjs", "cjs"] {
let file = f.join(format!("packages/pkg-b/src/index.{ext}"));
let resolved = root_tsconfig.resolve_for_file(&file);
assert_eq!(
resolved.path(),
f.join("packages/pkg-b/tsconfig.json"),
".{ext} files should trigger solution-style resolution with allowJs"
);
}
}
Loading
Loading