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
1 change: 1 addition & 0 deletions fixtures/tsconfig/cases/exports/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {};
1 change: 1 addition & 0 deletions fixtures/tsconfig/cases/extends-extension/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {};
1 change: 1 addition & 0 deletions fixtures/tsconfig/cases/extends-extensionless/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {};
1 change: 1 addition & 0 deletions fixtures/tsconfig/cases/extends-multiple/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {};
5 changes: 4 additions & 1 deletion fixtures/tsconfig/cases/extends-paths-outside/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
{
"extends": "../../tsconfig.json"
"extends": "../../tsconfig.json",
"compilerOptions": {
"allowJs": true
}
}
1 change: 1 addition & 0 deletions fixtures/tsconfig/cases/extends-paths/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {};
1 change: 1 addition & 0 deletions fixtures/tsconfig/cases/extensionless-file/sub/ccc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {};
8 changes: 8 additions & 0 deletions fixtures/tsconfig/cases/extensionless-file/sub/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"paths": {
"@y/*": ["./y/*"]
}
},
"include": ["*.ts"]
}
8 changes: 8 additions & 0 deletions fixtures/tsconfig/cases/extensionless-file/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"files": ["sub/ccc"],
"compilerOptions": {
"paths": {
"@x/*": ["./x/*"]
}
}
}
1 change: 1 addition & 0 deletions fixtures/tsconfig/cases/extensionless-file/x/foo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {};
1 change: 1 addition & 0 deletions fixtures/tsconfig/cases/index/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {};
1 change: 1 addition & 0 deletions fixtures/tsconfig/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {};
1 change: 1 addition & 0 deletions fixtures/tsconfig/nested/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {};
8 changes: 7 additions & 1 deletion src/tests/tsconfck.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ fn part_of_solution() {
("referenced-with-implicit-globs", "tests/foo.test.ts", "tsconfig.test.json"),
// not part of tsconfck
("referenced-files", "src/foo.ts", "tsconfig.foo.json"),
("referenced-files", "src/bar.ts", "tsconfig.json"),
("referenced-include", "src/foo.ts", "tsconfig.foo.json"),
("referenced-include", "src/bar.ts", "tsconfig.bar.json"),
("referenced-exclude", "src/foo.ts", "tsconfig.foo.json"),
Expand All @@ -93,6 +92,13 @@ fn part_of_solution() {
let tsconfig = resolver.find_tsconfig(dir.join(specifier)).unwrap().unwrap();
assert_eq!(tsconfig.path.clone(), dir.join(expected), "{dir:?} {specifier}");
}

// `src/bar.ts` is excluded from the only referenced project
// (`tsconfig.foo.json` lists `files: ["src/foo.ts"]`) and owned by no other,
// so discovery finds no config for it — matching `tsserver` / `typescript-go`,
// which leave such a file in an inferred project rather than the solution root.
let unowned = root.join("referenced-files").join("src/bar.ts");
assert!(resolver.find_tsconfig(unowned).unwrap().is_none());
}

// https://github.com/dominikg/tsconfck/blob/main/packages/tsconfck/tests/find.js
Expand Down
32 changes: 32 additions & 0 deletions src/tests/tsconfig_discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,38 @@ fn tsconfig_discovery() {
super::tsconfig_paths::tsconfig_resolve_impl(/* tsconfig_discovery */ true);
}

/// An extensionless file is owned through a `files` entry — a literal exact-path
/// match — even when a *nearer* `tsconfig.json` exists that does not list it (an
/// `include` glob cannot match an extensionless path). Ownership belongs to the
/// outer config that lists `sub/ccc` in `files`, so only its `@x/*` alias applies.
///
/// Previously `claims_ownership_of` returned `true` for every extensionless path,
/// so the nearer `sub/tsconfig.json` wrongly claimed `ccc` by proximity.
#[test]
fn extensionless_file_owned_via_files_array() {
let f = super::fixture_root().join("tsconfig/cases/extensionless-file");

let resolver = Resolver::new(ResolveOptions {
extensions: vec![".ts".into()],
tsconfig: Some(TsconfigDiscovery::Auto),
..ResolveOptions::default()
});

let importer = f.join("sub/ccc");

// Owned by the outer config (via `files`), not the nearer `sub/tsconfig.json`.
let tsconfig = resolver.find_tsconfig(&importer).unwrap().unwrap();
assert_eq!(tsconfig.path, f.join("tsconfig.json"));

// The outer `@x/*` alias applies.
let resolved_path = resolver.resolve_file(&importer, "@x/foo").map(|r| r.full_path());
assert_eq!(resolved_path, Ok(f.join("x/foo.ts")));

// The nearer config's `@y/*` alias does not (it does not own the file).
let not_owned = resolver.resolve_file(&importer, "@y/foo").map(|r| r.full_path());
assert_eq!(not_owned, Err(ResolveError::NotFound("@y/foo".into())));
}

#[test]
fn tsconfig_discovery_virtual_file_importer() {
let f = super::fixture_root().join("tsconfig");
Expand Down
32 changes: 18 additions & 14 deletions src/tests/tsconfig_paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,30 @@ use crate::{
pub fn tsconfig_resolve_impl(tsconfig_discovery: bool) {
let f = super::fixture_root().join("tsconfig");

// The second tuple element is the importer file, relative to the case
// directory. Auto-discovery only attaches a tsconfig to a file the config
// actually owns, so each importer is a real owned source file rather than
// the bare directory (which `resolve_file` is not meant to take).
#[rustfmt::skip]
let pass = [
(f.clone(), None, "ts-path", f.join("src/foo.js")),
(f.join("nested"), None, "ts-path", f.join("nested/test.js")),
(f.join("cases/extends-paths-outside"), Some("src/index.js"), "ts-path", f.join("src/foo.js")),
(f.join("cases/index"), None, "foo", f.join("node_modules/tsconfig-index/foo.js")),
(f.clone(), "main.ts", "ts-path", f.join("src/foo.js")),
(f.join("nested"), "main.ts", "ts-path", f.join("nested/test.js")),
(f.join("cases/extends-paths-outside"), "src/index.js", "ts-path", f.join("src/foo.js")),
(f.join("cases/index"), "main.ts", "foo", f.join("node_modules/tsconfig-index/foo.js")),
// This requires reading package.json.tsconfig field
// (f.join("cases/field"), "foo", f.join("node_modules/tsconfig-field/foo.js"))
(f.join("cases/exports"), None, "foo", f.join("node_modules/tsconfig-exports/foo.js")),
(f.join("cases/extends-extension"), None, "foo", f.join("cases/extends-extension/foo.js")),
(f.join("cases/extends-extensionless"), None, "foo", f.join("node_modules/tsconfig-field/foo.js")),
(f.join("cases/extends-paths"), Some("src"), "@/index", f.join("cases/extends-paths/src/index.js")),
(f.join("cases/extends-multiple"), None, "foo", f.join("cases/extends-multiple/foo.js")),
(f.join("cases/absolute-alias"), None, "/images/foo.js", f.join("cases/absolute-alias/public/images/foo.ts")),
(f.join("cases/references-extend"), Some("src/index.ts"), "ts-path", f.join("src/foo.js")),
(f.join("cases/exports"), "main.ts", "foo", f.join("node_modules/tsconfig-exports/foo.js")),
(f.join("cases/extends-extension"), "main.ts", "foo", f.join("cases/extends-extension/foo.js")),
(f.join("cases/extends-extensionless"), "main.ts", "foo", f.join("node_modules/tsconfig-field/foo.js")),
(f.join("cases/extends-paths"), "src/main.ts", "@/index", f.join("cases/extends-paths/src/index.js")),
(f.join("cases/extends-multiple"), "main.ts", "foo", f.join("cases/extends-multiple/foo.js")),
(f.join("cases/absolute-alias"), "test.ts", "/images/foo.js", f.join("cases/absolute-alias/public/images/foo.ts")),
(f.join("cases/references-extend"), "src/index.ts", "ts-path", f.join("src/foo.js")),
// Support `base_url` 3rd case <https://github.com/microsoft/TypeScript/issues/62207>
(f.join("cases/base-url"), Some("src/index.ts"), "foo.js", f.join("cases/base-url/src/foo.js")),
(f.join("cases/base-url"), "src/index.ts", "foo.js", f.join("cases/base-url/src/foo.js")),
];

for (dir, subdir, request, expected) in pass {
for (dir, importer, request, expected) in pass {
let resolver = Resolver::new(ResolveOptions {
tsconfig: Some(if tsconfig_discovery {
TsconfigDiscovery::Auto
Expand All @@ -43,7 +47,7 @@ pub fn tsconfig_resolve_impl(tsconfig_discovery: bool) {
extension_alias: vec![(".js".into(), vec![".js".into(), ".ts".into(), ".tsx".into()])],
..ResolveOptions::default()
});
let path = subdir.map_or_else(|| dir.clone(), |subdir| dir.join(subdir));
let path = dir.join(importer);
let resolved_path = resolver.resolve_file(&path, request).map(|f| f.full_path());
assert_eq!(resolved_path, Ok(expected), "{request} {path:?} {tsconfig_discovery}");
}
Expand Down
17 changes: 7 additions & 10 deletions src/tests/tsconfig_project_references.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,12 +317,10 @@ fn solution_style_non_ts_extensions() {
}

// ...while a file whose extension is not matched by any `include` glob
// (here `.css`) is owned by no project. It is never routed to the
// referenced project, so the `@/*` alias does not leak into it: auto-
// discovery walks past the solution root (which lists no `files`) up to the
// outermost ancestor tsconfig.
let tsconfig = resolver.find_tsconfig(f.join("src/styles.css")).unwrap().unwrap();
assert_eq!(tsconfig.path, super::fixture_root().join("tsconfig/tsconfig.json"));
// (here `.css`) is owned by no project. Discovery finds no config for it (it
// would land in an inferred project), so the `@/*` alias — declared only by
// the referenced project — does not leak into it.
assert!(resolver.find_tsconfig(f.join("src/styles.css")).unwrap().is_none());

// And so the `@/*` alias (declared only by the referenced project) does not
// apply to the `.css` file.
Expand Down Expand Up @@ -374,8 +372,7 @@ fn solution_style_nested_non_ts_walks_up() {

// A `.js` file with `allowJs` off is not part of the child's program, so
// the child does not own it and — like the `.vue` above — the walk
// continues up. No ancestor compiles a bare `.js` here, so it ends on the
// outermost tsconfig (auto-discovery's fallback).
let legacy = resolver.find_tsconfig(f.join("src/feature/legacy.js")).unwrap().unwrap();
assert_eq!(legacy.path, super::fixture_root().join("tsconfig/tsconfig.json"));
// continues up. No ancestor owns a bare `.js` either, so discovery finds no
// config for it (it would land in an inferred project).
assert!(resolver.find_tsconfig(f.join("src/feature/legacy.js")).unwrap().is_none());
}
8 changes: 0 additions & 8 deletions src/tsconfig.rs
Original file line number Diff line number Diff line change
Expand Up @@ -583,14 +583,6 @@ impl TsConfig {
/// doesn't actually cover the file via its `files` / `include` / `exclude`
/// or via a matching reference.
pub(crate) fn claims_ownership_of(&self, path: &Path) -> bool {
// A directory (extensionless importer) isn't a file, so `files` /
// `include` ownership doesn't apply — the nearest enclosing tsconfig
// governs it (its `paths` / `baseUrl` / `extends`). A genuine file is
// claimed only when owned (below), so an `allowJs`-off `.js` this
// config won't compile instead walks up.
if path.extension().is_none() {
return true;
}
// Any matching reference claims ownership (consistent with
// resolve_tsconfig_solution).
if self.references_resolved.iter().any(|r| r.is_file_included_in_tsconfig(path)) {
Expand Down
14 changes: 7 additions & 7 deletions src/tsconfig_resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,6 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
) -> Result<Option<Arc<TsConfig>>, ResolveError> {
let mut ctx = Ctx::default();
let mut cache_value = Some(cached_path.clone());
let mut fallback: Option<Arc<TsConfig>> = None;
while let Some(cv) = cache_value {
if let Some(tsconfig) = cv.tsconfig.get_or_try_init(|| {
let tsconfig_path = cv.path.join("tsconfig.json");
Expand All @@ -127,18 +126,19 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
Ok(None)
}
})? {
// Return the nearest tsconfig that owns the file (directly via
// `files`/`include`/`exclude`, or via a matching reference);
// otherwise keep walking up to an ancestor that does.
if tsconfig.claims_ownership_of(cached_path.path()) {
return Ok(Some(Arc::clone(tsconfig)));
}
// Remember the OUTERMOST non-claiming tsconfig as the fallback.
// typescript-go would return null here (no project owns), but
// TypeScript 6.0's tsserver returns the solution root, which we
// approximate by always preferring the topmost ancestor.
fallback = Some(Arc::clone(tsconfig));
}
cache_value = cv.parent(&self.cache);
}
Ok(fallback)
// No tsconfig owns the file. Both `tsserver` and `typescript-go` leave
// such a file in an inferred project (no `paths`/`baseUrl`), rather than
// applying an unrelated ancestor's `compilerOptions`, so return `None`.
Ok(None)
}

pub(crate) fn find_tsconfig_manual(
Expand Down
Loading