feat: tsconfig compilerOptions.paths — aliased bare specifiers join the graph and follow moves

- 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
This commit is contained in:
loki5512344 2026-09-15 19:17:06 +02:00
parent f8004e669b
commit b1af375763
Signed by: boba
GPG key ID: 253067914055423B
14 changed files with 400 additions and 5 deletions

37
tests/cli_alias.rs Normal file
View file

@ -0,0 +1,37 @@
//! End-to-end tests for tsconfig `paths` aliases: aliased imports take
//! part in `mv` and keep their alias shape inside the mapped tree.
mod common;
use common::{copy_fixture, in_root, jmove, read};
use predicates::prelude::*;
const APP: &str = "src/app.ts";
#[test]
fn aliased_imports_follow_the_move_and_keep_the_alias_shape() {
let tmp = copy_fixture("typescript", "aliased");
jmove(&tmp, &["check"]).success(); // "@utils/str" resolves: not "broken"
jmove(&tmp, &["mv", "src/utils/str.ts", "src/utils/text.ts"])
.success()
.stdout(predicate::str::contains("updated 1 import"));
let app = read(&in_root(tmp.path(), APP));
assert!(app.contains("from \"@utils/text\""), "{app}");
assert!(!app.contains("./utils/text"), "{app}");
// Moving out of the alias tree falls back to a relative specifier.
jmove(&tmp, &["mv", "src/utils/text.ts", "src/core/text.ts"]).success();
let app = read(&in_root(tmp.path(), APP));
assert!(app.contains("from \"./core/text\""), "{app}");
jmove(&tmp, &["check"]).success();
}
#[test]
fn exact_alias_key_falls_back_when_it_no_longer_matches() {
let tmp = copy_fixture("typescript", "aliased");
jmove(&tmp, &["mv", "src/config.ts", "src/settings.ts"]).success();
let app = read(&in_root(tmp.path(), APP));
// "@cfg" would now resolve to nothing: the rewrite must be relative.
assert!(app.contains("from \"./settings\""), "{app}");
assert!(!app.contains("@cfg"), "{app}");
jmove(&tmp, &["check"]).success();
}