Summary
Resolver::find_tsconfig and Resolver::resolve_file apply the nearest ancestor tsconfig.json to a file even when that file is not included in the TypeScript project.
This can cause oxc_resolver to resolve imports using compilerOptions.paths for files that TypeScript does not consider part of the project.
Version
oxc_resolver@11.24.2
Reproduction
Given this structure:
.
├── Cargo.toml
├── src
│ ├── main.rs
│ └── works.ts
├── .test
│ └── fails.ts
└── tsconfig.json
tsconfig.json:
{
"compilerOptions": {
"module": "esnext",
"moduleResolution": "bundler",
"noEmit": true,
"paths": {
"~/*": ["./*"]
}
},
"include": ["**/*.ts"]
}
src/works.ts:
export const works = "resolved";
.test/fails.ts:
import { works } from "~/src/works";
console.log(works);
Cargo.toml:
[package]
name = "oxc-resolver-dotdir-repro"
version = "0.1.0"
edition = "2024"
[dependencies]
oxc_resolver = "=11.24.2"
src/main.rs:
use std::{env, error::Error, path::Path};
use oxc_resolver::{ResolveOptions, Resolver, TsconfigDiscovery};
fn print_result(resolver: &Resolver, file: &Path) -> Result<(), Box<dyn Error>> {
let tsconfig = resolver
.find_tsconfig(file)?
.map(|config| config.path().to_path_buf());
let resolved = resolver.resolve_file(file, "~/src/works")?;
println!("file: {}", file.display());
println!(
" tsconfig: {}",
tsconfig.as_deref().map_or_else(
|| "<none>".to_string(),
|path| path.display().to_string(),
)
);
println!(" ~/src/works: {}", resolved.full_path().display());
Ok(())
}
fn main() -> Result<(), Box<dyn Error>> {
let root = env::current_dir()?;
let resolver = Resolver::new(ResolveOptions {
tsconfig: Some(TsconfigDiscovery::Auto),
extensions: vec![".ts".into(), ".js".into(), ".json".into()],
..ResolveOptions::default()
});
print_result(&resolver, &root.join("src/works.ts"))?;
print_result(&resolver, &root.join(".test/fails.ts"))?;
Ok(())
}
Run:
Actual behavior
Both files receive the root tsconfig.json, and the alias resolves from both:
file: .../src/works.ts
tsconfig: .../tsconfig.json
~/src/works: .../src/works.ts
file: .../.test/fails.ts
tsconfig: .../tsconfig.json
~/src/works: .../src/works.ts
TypeScript behavior
TypeScript wildcard includes do not implicitly match dot-prefixed path components.
Running:
tsgo --project tsconfig.json --listFiles
includes:
but does not include:
Therefore, TypeScript does not consider .test/fails.ts part of this project.
Relevant TypeScript code:
https://github.com/microsoft/typescript-go/blob/2bd066d87f5bafd315be9f40889d0a60b9e58e0b/internal/vfs/vfsmatch/vfsmatch.go#L360-L365
Expected behavior
Automatic tsconfig discovery should ideally only return a config whose files/include/exclude rules contain the requested file.
For .test/fails.ts, I would therefore expect:
and for resolve_file not to apply the root config's paths mapping.
Summary
Resolver::find_tsconfigandResolver::resolve_fileapply the nearest ancestortsconfig.jsonto a file even when that file is not included in the TypeScript project.This can cause
oxc_resolverto resolve imports usingcompilerOptions.pathsfor files that TypeScript does not consider part of the project.Version
oxc_resolver@11.24.2Reproduction
Given this structure:
tsconfig.json:{ "compilerOptions": { "module": "esnext", "moduleResolution": "bundler", "noEmit": true, "paths": { "~/*": ["./*"] } }, "include": ["**/*.ts"] }src/works.ts:.test/fails.ts:Cargo.toml:src/main.rs:Run:
Actual behavior
Both files receive the root
tsconfig.json, and the alias resolves from both:TypeScript behavior
TypeScript wildcard includes do not implicitly match dot-prefixed path components.
Running:
includes:
but does not include:
Therefore, TypeScript does not consider
.test/fails.tspart of this project.Relevant TypeScript code:
https://github.com/microsoft/typescript-go/blob/2bd066d87f5bafd315be9f40889d0a60b9e58e0b/internal/vfs/vfsmatch/vfsmatch.go#L360-L365
Expected behavior
Automatic tsconfig discovery should ideally only return a config whose
files/include/excluderules contain the requested file.For
.test/fails.ts, I would therefore expect:and for
resolve_filenot to apply the root config'spathsmapping.