Skip to content

Commit b6d6226

Browse files
committed
fix: strip query and fragment before finding matched tsconfig file
1 parent 4d89b43 commit b6d6226

9 files changed

Lines changed: 102 additions & 2 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ cfg-if = "1"
7979
compact_str = "0.9"
8080
fast-glob = "1"
8181
indexmap = { version = "2", features = ["serde"] }
82+
memchr = "2"
8283
json-strip-comments = "3.1"
8384
nodejs-built-in-modules = "1.0.0"
8485
once_cell = "1" # Use `std::sync::OnceLock::get_or_try_init` when it is stable.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = "foo";
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import foo from "@alias/foo.js";
2+
export default foo;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"compilerOptions": {
3+
"composite": true,
4+
"paths": {
5+
"@alias/*": ["./src/*"]
6+
}
7+
},
8+
"include": ["src/**/*"]
9+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"include": [],
3+
"references": [
4+
{ "path": "./tsconfig.app.json" }
5+
]
6+
}

src/path.rs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,23 @@
33
//! Code adapted from the following libraries
44
//! * [path-absolutize](https://docs.rs/path-absolutize)
55
//! * [normalize_path](https://docs.rs/normalize-path)
6-
use std::path::{Component, Path, PathBuf};
6+
use std::{
7+
ffi::OsStr,
8+
path::{Component, Path, PathBuf},
9+
};
710

811
pub const SLASH_START: &[char; 2] = &['/', '\\'];
912

13+
/// Strip query parameters (`?...`) and hash fragments (`#...`) from a file path.
14+
pub fn strip_query_and_fragment(path: &Path) -> &Path {
15+
let bytes = path.as_os_str().as_encoded_bytes();
16+
let Some(end) = memchr::memchr2(b'?', b'#', bytes) else {
17+
return path;
18+
};
19+
// SAFETY: Splitting at ASCII `?` or `#` preserves valid OsStr encoding.
20+
Path::new(unsafe { OsStr::from_encoded_bytes_unchecked(&bytes[..end]) })
21+
}
22+
1023
/// Extension trait to add path normalization to std's [`Path`].
1124
pub trait PathUtil {
1225
/// Normalize this path without performing I/O.
@@ -157,3 +170,26 @@ fn normalize_relative() {
157170
assert_eq!(Path::new("foo../../..").normalize_relative(), Path::new(".."));
158171
assert_eq!(Path::new("jest-runner-../../").normalize_relative(), Path::new(""));
159172
}
173+
174+
#[test]
175+
fn test_strip_query_and_fragment() {
176+
assert_eq!(strip_query_and_fragment(Path::new("/src/foo.ts")), Path::new("/src/foo.ts"));
177+
assert_eq!(
178+
strip_query_and_fragment(Path::new("/src/foo.ts?custom=foo")),
179+
Path::new("/src/foo.ts")
180+
);
181+
assert_eq!(
182+
strip_query_and_fragment(Path::new("/src/foo.ts#fragment")),
183+
Path::new("/src/foo.ts")
184+
);
185+
assert_eq!(
186+
strip_query_and_fragment(Path::new("/src/foo.ts?key=val#frag")),
187+
Path::new("/src/foo.ts")
188+
);
189+
assert_eq!(
190+
strip_query_and_fragment(Path::new("/src/foo.ts#frag?key=val")),
191+
Path::new("/src/foo.ts")
192+
);
193+
assert_eq!(strip_query_and_fragment(Path::new("")), Path::new(""));
194+
assert_eq!(strip_query_and_fragment(Path::new("?query")), Path::new(""));
195+
}

src/tests/tsconfig_discovery.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,49 @@ fn tsconfig_discovery_virtual_file_importer() {
2626
assert_eq!(resolved_path, Err(ResolveError::NotFound("random-import".into())));
2727
}
2828

29+
/// When the importer path has query parameters (e.g. `file.tsx?custom=foo`),
30+
/// auto-discovery should strip them before walking parent directories
31+
/// and discover the correct tsconfig.json.
32+
///
33+
/// Uses a fixture with project references where the root tsconfig has `include: []`
34+
/// and a referenced tsconfig.app.json has `include: ["src/**/*"]` with path aliases.
35+
/// Without stripping query params, `resolve_tsconfig_solution` fails to match the
36+
/// file against the reference's include pattern, returning the wrong tsconfig.
37+
#[test]
38+
fn tsconfig_discovery_query_params() {
39+
let f = super::fixture_root().join("tsconfig/cases/query-params");
40+
let expected_tsconfig = f.join("tsconfig.app.json");
41+
42+
let resolver = Resolver::new(ResolveOptions {
43+
tsconfig: Some(TsconfigDiscovery::Auto),
44+
..ResolveOptions::default()
45+
});
46+
47+
let clean_path = f.join("src/index.ts");
48+
49+
// Baseline — clean path discovers tsconfig.app.json (via project references)
50+
let tsconfig = resolver.find_tsconfig(&clean_path).unwrap().unwrap();
51+
assert_eq!(tsconfig.path, expected_tsconfig, "baseline: should select referenced tsconfig");
52+
53+
// With query parameter — should discover the same referenced tsconfig
54+
let path_with_query = format!("{}?custom=foo", clean_path.display());
55+
let tsconfig = resolver.find_tsconfig(&path_with_query).unwrap().unwrap();
56+
assert_eq!(tsconfig.path, expected_tsconfig, "query param: should select referenced tsconfig");
57+
58+
// With fragment — should discover the same referenced tsconfig
59+
let path_with_fragment = format!("{}#fragment", clean_path.display());
60+
let tsconfig = resolver.find_tsconfig(&path_with_fragment).unwrap().unwrap();
61+
assert_eq!(tsconfig.path, expected_tsconfig, "fragment: should select referenced tsconfig");
62+
63+
// With both query and fragment — should discover the same referenced tsconfig
64+
let path_with_both = format!("{}?custom=foo#fragment", clean_path.display());
65+
let tsconfig = resolver.find_tsconfig(&path_with_both).unwrap().unwrap();
66+
assert_eq!(
67+
tsconfig.path, expected_tsconfig,
68+
"query+fragment: should select referenced tsconfig"
69+
);
70+
}
71+
2972
/// When a tsconfig.json exists but is not readable (e.g. permission denied),
3073
/// auto-discovery should skip it and return `Ok(None)` instead of erroring.
3174
#[test]

src/tsconfig_resolver.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
5353
&self,
5454
path: P,
5555
) -> Result<Option<Arc<TsConfig>>, ResolveError> {
56-
let path = path.as_ref();
56+
// Vite plugins may append query params to real file paths (e.g. `file.tsx?custom=foo`), which are not valid filesystem path components.
57+
let path = crate::path::strip_query_and_fragment(path.as_ref());
5758
let cached_path = self.cache.value(path);
5859
self.find_tsconfig_tracing(&cached_path)
5960
}

0 commit comments

Comments
 (0)