Skip to content

Conversation

@Boshen
Copy link
Member

@Boshen Boshen commented Oct 28, 2025

Implements support for TypeScript's include, exclude, and files fields in tsconfig.json, enabling proper file filtering for tools like Vite and Rolldown.

Key Features

  • Pattern matching with glob support (**, *, ?, character sets)
  • Default values:
    • include: ["**/*"] when not specified
    • exclude: ["node_modules", "bower_components", "jspm_packages"] + outDir
  • Files array priority: Files in the files array override exclude patterns
  • Template variable substitution: ${configDir} support in patterns
  • outDir auto-exclusion: Automatically excludes compiler output directory
  • Platform-specific matching:
    • Case-insensitive on Windows
    • Case-sensitive on Unix
  • Special handling: Empty files array + no include matches nothing

Implementation Details

  • Uses fast-glob crate (v1.0.0) for high-performance pattern matching
  • Aligns with vite-tsconfig-paths and TypeScript compiler behavior
  • Comprehensive test coverage: 68+ test cases covering various scenarios

API

Added public method TsConfig::matches_file():

pub fn matches_file(&self, path: &Path) -> bool

Tests if a file path matches the tsconfig's include/exclude/files patterns.

Test Coverage

Tests ported and augmented from vite-tsconfig-paths:

  • Basic include/exclude patterns
  • Files array priority
  • Default values
  • Globstar patterns (**)
  • Wildcard patterns (*, ?)
  • Character sets ([A-Z])
  • Monorepo patterns
  • outDir exclusion
  • Complex nested patterns
  • Extends inheritance
  • Project references
  • Case sensitivity (Unix/Windows)

Closes #764


🤖 Generated with Claude Code

@graphite-app
Copy link

graphite-app bot commented Oct 28, 2025

How to use the Graphite Merge Queue

Add the label merge to this PR to add it to the merge queue.

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

@Boshen Boshen force-pushed the test/tsconfig-include-exclude branch from 64b5156 to 5465eed Compare October 28, 2025 04:51
@codecov
Copy link

codecov bot commented Oct 28, 2025

Codecov Report

❌ Patch coverage is 96.47059% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.77%. Comparing base (9de3088) to head (1fd4a65).

Files with missing lines Patch % Lines
src/tsconfig/mod.rs 90.47% 4 Missing ⚠️
src/tsconfig/file_matcher.rs 98.38% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #790      +/-   ##
==========================================
+ Coverage   93.59%   93.77%   +0.17%     
==========================================
  Files          17       18       +1     
  Lines        3093     3262     +169     
==========================================
+ Hits         2895     3059     +164     
- Misses        198      203       +5     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@codspeed-hq
Copy link

codspeed-hq bot commented Oct 28, 2025

CodSpeed Performance Report

Merging #790 will not alter performance

Comparing test/tsconfig-include-exclude (1fd4a65) with main (9de3088)

Summary

✅ 10 untouched
⏩ 5 skipped1

Footnotes

  1. 5 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

Add comprehensive test suite for TypeScript's include, exclude, and files
fields from tsconfig.json. Tests are ported from vite-tsconfig-paths.

This is part of implementing issue #764 to support include/exclude/files
for Vite/Rolldown integration.

Test Coverage:
- Basic include pattern matching (src/**/*.ts)
- Exclude pattern filtering (**/*.test.ts)
- Files field priority over exclude
- Default include/exclude values
- ${configDir} template variable substitution
- Extends inheritance behavior
- Empty files/include arrays
- Globstar (**) and wildcard (*) patterns
- Character sets ([A-Z]*.ts)
- Monorepo patterns (packages/*/src/**)
- Project references with include/exclude
- Paths outside project root
- outDir auto-exclusion
- Complex multi-glob patterns
- Case sensitivity handling

Fixtures:
- 19 test fixture directories with tsconfig.json and source files
- Covers real-world patterns from vite-tsconfig-paths

Tests currently fail as expected (TDD approach) - implementation follows.

Related: #764

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@Boshen Boshen changed the title test: add tests for tsconfig include/exclude/files support feat: implement tsconfig include/exclude/files matching Oct 28, 2025
Refactors tests to use full resolution flow and implements actual
include/exclude filtering during module resolution.

## Changes

### Core Implementation (src/lib.rs)
- Add include/exclude check in `load_tsconfig_paths()` before applying
  path mappings
- Only check actual files, not directories (to support directory-based
  resolution)
- If importer doesn't match tsconfig patterns, skip that tsconfig's
  path mappings

### File Matcher Creation (src/tsconfig/mod.rs)
- Ensure file_matcher is always created for non-empty tsconfigs
- Defaults to include: ["**/*"] when no patterns specified
- All tsconfigs now have a file_matcher (except explicit empty case)

### Test Strategy (src/tests/tsconfig_include_exclude.rs)
- Refactor from testing `matches_file()` directly to testing full
  `resolver.resolve()`
- Verify path aliases work when importer is included
- Verify path aliases don't work when importer is excluded
- Test cases cover: include patterns, exclude patterns, default behavior

### Fixtures
- Add imports to test fixtures (index.ts files)
- Add path aliases (baseUrl/paths) to tsconfig.json files
- Create helper.ts for exclude_basic fixture

## Testing
All 158 tests pass, including new resolution-based include/exclude tests.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@Boshen Boshen force-pushed the test/tsconfig-include-exclude branch from 7166911 to 2a6e0a9 Compare October 28, 2025 09:15
Add comprehensive unit tests that directly test pattern matching logic
in isolation, using the previously unused fixtures.

## Changes

- Create `src/tests/tsconfig_file_matcher.rs` with 10 unit tests
- Each test covers a specific pattern type:
  - `test_globstar_patterns` - Tests **/*.ts (matches at any depth)
  - `test_wildcard_patterns` - Tests src/*.ts (single level)
  - `test_character_set_patterns` - Tests [A-Z]*.ts (character ranges)
  - `test_complex_patterns` - Tests src/**/test/**/*.spec.ts (nested)
  - `test_monorepo_patterns` - Tests packages/*/src/**/* (monorepo)
  - `test_files_priority` - Tests files array overrides exclude
  - `test_outdir_exclude` - Tests automatic outDir exclusion
  - `test_absolute_patterns` - Tests absolute path handling
  - `test_configdir_syntax` - Tests ${configDir} template variable
  - `test_without_baseurl` - Tests default include/exclude

- Helper function `create_matcher_from_fixture()` reads tsconfig.json
  and creates TsconfigFileMatcher with template substitution
- All 10 fixtures that were created but unused are now tested
- Total: 168 tests passing (10 new unit tests added)

## Benefits

- Tests pattern matching in isolation (faster than integration tests)
- Clear documentation of all supported pattern types
- Better coverage of edge cases
- Reuses existing fixture infrastructure

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@Boshen Boshen marked this pull request as ready for review October 28, 2025 09:49
@Boshen Boshen requested a review from sapphi-red October 28, 2025 09:49
Comment on lines 200 to 207
// 1. Check files array first (absolute priority)
if let Some(files) = &self.files {
for file in files {
// Check both exact match and ends_with
if normalized == *file || normalized.ends_with(file) {
return true; // Files overrides exclude
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The files array matching logic has a critical bug. The ends_with check on line 204 will cause false positives. For example, if files contains "test.ts", it will incorrectly match "./another_test.ts", "./src/my_test.ts", etc.

Additionally, the files are not normalized in the constructor (line 100), but the matched path is normalized (prefixed with ./). If files contains "test.ts" and the path is "./test.ts", the exact match on line 204 will fail.

Fix:

// In constructor, normalize files:
files: files.map(|f| Self::normalize_patterns(f, &tsconfig_dir)),

// In matches method, use exact match only:
if let Some(files) = &self.files {
    for file in files {
        if normalized == *file {
            return true;
        }
    }
    if self.include_patterns.is_empty() {
        return false;
    }
}

Spotted by Graphite Agent

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sounds valid.

Comment on lines +1482 to +1488
// Check if the importer file matches the tsconfig's include/exclude patterns
// Only check for actual files, not directories (directories are used when there's no specific importer)
// If the importer doesn't match, don't use this tsconfig's path mappings
let is_file = cached_path.meta(&self.cache.fs).is_some_and(|m| m.is_file);
if is_file && !tsconfig.matches_file(cached_path.path()) {
return Ok(None);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this does not work for "Solution Style" tsconfigs, as we need to pick the referenced tsconfig that matches the include & exclude (can be done in a separate PR).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this file is not used and can be removed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this file is also not used and can be removed.

Comment on lines +28 to +31
// Default include (no include specified, defaults to **/*) - Exclude: [dist]
// All files except dist/ can use path mappings
("with_baseurl", "index.ts", "~/log", true, "file in root can use path mapping"),
("with_baseurl", "index.ts", "log", true, "file in root can use baseUrl"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need a test case that ensures dist cannot use path mappings. (That would cover the case where the value is a directory)

let fixture_dir = f.join(fixture);
let resolver = Resolver::new(ResolveOptions {
tsconfig: Some(TsconfigDiscovery::Manual(TsconfigOptions {
config_file: fixture_dir.join("tsconfig.json"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should include / exclude be respected when the file path is specified? (I guess no?)

Comment on lines 200 to 207
// 1. Check files array first (absolute priority)
if let Some(files) = &self.files {
for file in files {
// Check both exact match and ends_with
if normalized == *file || normalized.ends_with(file) {
return true; // Files overrides exclude
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sounds valid.

Comment on lines +208 to +212
// If files specified but no match, continue to include/exclude
// (unless include is empty)
if self.include_patterns.is_empty() {
return false;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// If files specified but no match, continue to include/exclude
// (unless include is empty)
if self.include_patterns.is_empty() {
return false;
}

This looks duplicated.


// SPECIAL CASE: empty files + no include = skip file matching
let is_empty_case = self.files.as_ref().is_some_and(std::vec::Vec::is_empty)
&& self.include.as_ref().is_none_or(std::vec::Vec::is_empty);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
&& self.include.as_ref().is_none_or(std::vec::Vec::is_empty);
&& self.include.as_ref().is_some_and(std::vec::Vec::is_empty);

include defaults to **/* so None should not be treated as the same with vec![].

use std::path::PathBuf;

/// Helper to create a TsconfigFileMatcher from a fixture directory
fn create_matcher_from_fixture(fixture_name: &str) -> (TsconfigFileMatcher, PathBuf) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this function should exist. If this is a unit test, the TsconfigFileMatcher should be constructed directly. If this is an integration test, the function in the src should be used to construct TsconfigFileMatcher.

Comment on lines 108 to 122
#[test]
fn test_character_set_patterns() {
let (matcher, fixture_dir) = create_matcher_from_fixture("character_set_patterns");

let test_cases = [
("App.ts", true, "character set matches uppercase start"),
("Button.ts", true, "character set matches uppercase start"),
];

for (file_path, should_match, comment) in test_cases {
let full_path = fixture_dir.join(file_path);
let result = matcher.matches(&full_path);
assert_eq!(result, should_match, "{comment}: path={file_path}");
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TypeScript does not support this pattern ([A-Z]*.ts) and will treat this as a literal.

- Fix critical bug in files array matching: normalize files in constructor and use exact match only (prevents false positives)
- Fix include empty check: None should default to **/* not be treated as empty
- Remove duplicate empty pattern check
- Add tests for fixture files in dist/ and node_modules/
- Fix test assertions for extends inheritance and paths outside root
- Update character set pattern test (TypeScript doesn't support [A-Z] syntax)
- Fix outdir_exclude fixture to include .js and .d.ts files
- Add test case verifying excluded dist directory cannot use path mappings

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@shulaoda
Copy link
Member

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Respect tsconfig include & exclude & references

4 participants