Skip to content

Automatic tsconfig discovery applies compiler options to files excluded from the TypeScript project #1324

Description

@camc314

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:

cargo 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:

.../src/works.ts

but does not include:

.../.test/fails.ts

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:

tsconfig: <none>

and for resolve_file not to apply the root config's paths mapping.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Fields

Priority

None yet

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions