- index/tsconfig: JSONC sanitize (comments, trailing commas), baseUrl + star/exact path keys, first candidate only, longest-prefix wins; invalid or missing tsconfig silently means no aliases - resolution pass: '.'-prefixed specifiers resolve relatively as before, bare ones go through the alias table and join the importer graph - planner: remap keeps the alias shape while the moved file stays inside the mapped tree (morphological, since destinations do not exist yet), exact keys keep meaning only while still resolving to the target; otherwise the previous relative rewrite applies unchanged - resolve_base extracted from resolve_module (shared extension/index guessing); MODULE_EXTS is the exact inverse for remapping - e2e: @utils/str.ts -> @utils/text.ts inside alias, -> ./core/text.ts when leaving it, @cfg falls back to ./settings; check passes on aliases
80 lines
2.2 KiB
Rust
80 lines
2.2 KiB
Rust
use super::{PathAliases, sanitize_jsonc};
|
|
use crate::core::index::FileSet;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
fn files(paths: &[&str]) -> FileSet {
|
|
let mut set = FileSet::default();
|
|
for p in paths {
|
|
set.add(PathBuf::from(p));
|
|
}
|
|
set
|
|
}
|
|
|
|
fn aliases(json: &str) -> PathAliases {
|
|
PathAliases::parse(&sanitize_jsonc(json))
|
|
}
|
|
|
|
const TSCONFIG: &str = r#"{
|
|
// comment with a brace }
|
|
"compilerOptions": {
|
|
"baseUrl": "./",
|
|
"paths": {
|
|
"@/*": ["src/*"],
|
|
"@utils/*": ["src/shared/utils/*"],
|
|
"@cfg": ["src/config.ts"],
|
|
},
|
|
},
|
|
"include": ["src/**/*"],
|
|
}"#;
|
|
|
|
#[test]
|
|
fn jsonc_comments_and_trailing_commas_parse() {
|
|
let a = aliases(TSCONFIG);
|
|
assert_eq!(a.entries.len(), 3);
|
|
}
|
|
|
|
#[test]
|
|
fn resolution_prefers_the_longest_matching_prefix() {
|
|
let set = files(&[
|
|
"src/shared/utils/str.ts",
|
|
"src/utils/str.ts",
|
|
"src/config.ts",
|
|
]);
|
|
let a = aliases(TSCONFIG);
|
|
assert_eq!(
|
|
a.resolve("@utils/str", &set).as_deref(),
|
|
Some(Path::new("src/shared/utils/str.ts"))
|
|
);
|
|
assert_eq!(
|
|
a.resolve("@/utils/str", &set).as_deref(),
|
|
Some(Path::new("src/utils/str.ts"))
|
|
);
|
|
assert_eq!(
|
|
a.resolve("@cfg", &set).as_deref(),
|
|
Some(Path::new("src/config.ts"))
|
|
);
|
|
assert_eq!(a.resolve("react", &set), None);
|
|
}
|
|
|
|
#[test]
|
|
fn remap_keeps_alias_shape_inside_and_falls_back_outside() {
|
|
let set = files(&["src/utils/str.ts", "src/deep/str.ts", "src/config.ts"]);
|
|
let a = aliases(r#"{"compilerOptions": {"paths": {"@u/*": ["src/utils/*"]}}}"#);
|
|
// Destination need not exist yet: the plan runs before the move.
|
|
assert_eq!(
|
|
a.remap("@u/str", Path::new("src/utils/other.ts"), &set)
|
|
.as_deref(),
|
|
Some("@u/other")
|
|
);
|
|
// Target outside the alias tree: caller must fall back to relative.
|
|
assert!(a.remap("@u/str", Path::new("lib/other.ts"), &set).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn string_contents_survive_sanitizing() {
|
|
let json = r#"{"url": "http://x.com//y", "a": [1, 2,], /* c */ "b": "/*n*/"}"#;
|
|
let cleaned = sanitize_jsonc(json);
|
|
let value: serde_json::Value = serde_json::from_str(&cleaned).unwrap();
|
|
assert_eq!(value["url"], "http://x.com//y");
|
|
assert_eq!(value["b"], "/*n*/");
|
|
}
|