From c34a8778831ea0d6956bfa457e484fb6dec6d8cd Mon Sep 17 00:00:00 2001 From: Elad Bezalel Date: Sun, 8 Feb 2026 15:11:14 +0200 Subject: [PATCH 1/2] feat: add include pattern matching for test file detection Implement traf's `changedIncludedFilesPackages` mechanism in domino. Changed files matching configurable regex patterns now directly mark their owning project as affected, regardless of whether the file is parsed by the semantic analyzer. Default pattern matches test/spec files: `\.(spec|test)\.(ts|js)x?$` This ensures that when only test files change for a project, the project is still correctly detected as affected -- matching traf's behavior. Co-authored-by: Cursor --- src/core.rs | 61 +++++++++++++++++++++ src/types.rs | 4 +- tests/fixtures/monorepo | 2 +- tests/integration_test.rs | 110 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 174 insertions(+), 3 deletions(-) diff --git a/src/core.rs b/src/core.rs index da77bc7..044a525 100644 --- a/src/core.rs +++ b/src/core.rs @@ -7,12 +7,16 @@ use crate::types::{ TrueAffectedConfig, }; use crate::utils; +use regex::Regex; use rustc_hash::{FxHashMap, FxHashSet}; use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::sync::Arc; use tracing::debug; +/// Default include pattern: matches test/spec files (.spec.ts, .test.tsx, etc.) +const DEFAULT_INCLUDE_PATTERN: &str = r"\.(spec|test)\.(ts|js)x?$"; + /// Mutable state for tracking affected symbols during analysis struct AffectedState<'a> { affected_packages: &'a mut FxHashSet, @@ -69,6 +73,41 @@ fn find_affected_internal( let mut affected_packages = FxHashSet::default(); let mut project_causes: FxHashMap> = FxHashMap::default(); + // Step 4b: Process included file patterns (e.g., test files) + // Files matching include patterns directly mark their owning project as affected, + // matching traf's changedIncludedFilesPackages behavior. + let default_include = vec![DEFAULT_INCLUDE_PATTERN.to_string()]; + let include_patterns = if config.include.is_empty() { + &default_include + } else { + &config.include + }; + + let compiled_patterns: Vec = include_patterns + .iter() + .filter_map(|p| match Regex::new(p) { + Ok(re) => Some(re), + Err(e) => { + debug!("Invalid include pattern '{}': {}", p, e); + None + } + }) + .collect(); + + for changed_file in &changed_files { + let file_str = changed_file.file_path.to_string_lossy(); + if compiled_patterns.iter().any(|re| re.is_match(&file_str)) { + if let Some(pkg) = utils::get_package_name_by_path(&changed_file.file_path, &config.projects) + { + debug!( + "Include pattern matched {:?}, adding package '{}'", + changed_file.file_path, pkg + ); + affected_packages.insert(pkg); + } + } + } + // Step 5: Partition changed files into source and non-source let (source_files, asset_files): (Vec<&ChangedFile>, Vec<&ChangedFile>) = changed_files .iter() @@ -680,4 +719,26 @@ mod tests { assert!(affected.contains("lib1")); assert!(affected.contains("app")); // Should be added as implicit dependent } + + #[test] + fn test_default_include_pattern_matches_test_files() { + let re = Regex::new(DEFAULT_INCLUDE_PATTERN).expect("Default pattern should compile"); + + // Should match spec/test files + assert!(re.is_match("proj1/utils.spec.ts")); + assert!(re.is_match("proj1/utils.test.ts")); + assert!(re.is_match("proj1/component.spec.tsx")); + assert!(re.is_match("proj1/component.test.tsx")); + assert!(re.is_match("proj1/helper.spec.js")); + assert!(re.is_match("proj1/helper.test.jsx")); + assert!(re.is_match("tests/validation/roof.step.test.ts")); + + // Should NOT match regular source files + assert!(!re.is_match("proj1/utils.ts")); + assert!(!re.is_match("proj1/component.tsx")); + assert!(!re.is_match("proj1/index.js")); + assert!(!re.is_match("proj1/styles.css")); + assert!(!re.is_match("proj1/data.json")); + assert!(!re.is_match("proj1/test-utils.ts")); // "test" in name but not .test.ts + } } diff --git a/src/types.rs b/src/types.rs index 7aff07c..73e76bd 100644 --- a/src/types.rs +++ b/src/types.rs @@ -95,8 +95,8 @@ pub struct TrueAffectedConfig { pub root_ts_config: Option, /// Projects in the workspace pub projects: Vec, - /// Additional file patterns to include - #[allow(dead_code)] + /// Additional file patterns to include (regex patterns). + /// When empty, defaults to matching test files: `\.(spec|test)\.(ts|js)x?$` pub include: Vec, /// Paths to ignore #[allow(dead_code)] diff --git a/tests/fixtures/monorepo b/tests/fixtures/monorepo index 0d6eb7a..10ec84a 160000 --- a/tests/fixtures/monorepo +++ b/tests/fixtures/monorepo @@ -1 +1 @@ -Subproject commit 0d6eb7a3c5c9e4e0e19ec9a0cd80e017c55a9c59 +Subproject commit 10ec84a6d723c01d9fbca21e983373b552ef5bd6 diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 9c80743..b94e65d 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -1479,3 +1479,113 @@ export function anotherFn() { "proj2 should be affected via asset → constant → export chain" ); } + +/// Helper to get affected projects with custom include patterns +fn get_affected_with_include(include: Vec) -> Vec { + let config = TrueAffectedConfig { + cwd: fixture_path(), + base: "main".to_string(), + root_ts_config: Some(PathBuf::from("tsconfig.json")), + projects: vec![ + Project { + name: "proj1".to_string(), + source_root: PathBuf::from("proj1"), + ts_config: Some(PathBuf::from("proj1/tsconfig.json")), + implicit_dependencies: vec![], + targets: vec![], + }, + Project { + name: "proj2".to_string(), + source_root: PathBuf::from("proj2"), + ts_config: Some(PathBuf::from("proj2/tsconfig.json")), + implicit_dependencies: vec![], + targets: vec![], + }, + Project { + name: "proj3".to_string(), + source_root: PathBuf::from("proj3"), + ts_config: Some(PathBuf::from("proj3/tsconfig.json")), + implicit_dependencies: vec!["proj1".to_string()], + targets: vec![], + }, + ], + include, + ignored_paths: vec![], + }; + + let profiler = Arc::new(Profiler::new(false)); + + find_affected(config, profiler) + .expect("Failed to find affected projects") + .affected_projects +} + +#[test] +fn test_included_test_file_marks_project_affected() { + let branch = TestBranch::new("test-include-spec-file"); + + // Create a .spec.ts file in proj1 (test file matching default include pattern) + branch.make_change( + "proj1/utils.spec.ts", + r#"import { proj1 } from './index'; + +describe('proj1', () => { + it('should work', () => { + expect(proj1()).toBe('proj1'); + }); +}); +"#, + ); + + // Now modify only the test file + branch.make_change( + "proj1/utils.spec.ts", + r#"import { proj1 } from './index'; + +describe('proj1', () => { + it('should work correctly', () => { + expect(proj1()).toBe('proj1-modified'); + }); +}); +"#, + ); + + // Use default include (empty vec triggers default test file pattern) + let affected = get_affected_with_include(vec![]); + + // proj1 should be affected because the .spec.ts file matches the default include pattern + assert!( + affected.contains(&"proj1".to_string()), + "proj1 should be affected when a .spec.ts file changes (default include pattern). Got: {:?}", + affected + ); +} + +#[test] +fn test_included_file_with_custom_pattern() { + let branch = TestBranch::new("test-include-custom"); + + // Create a .stories.ts file in proj2 + branch.make_change( + "proj2/button.stories.ts", + r#"export const Primary = { args: { label: 'Click' } }; +"#, + ); + + // Modify the stories file + branch.make_change( + "proj2/button.stories.ts", + r#"export const Primary = { args: { label: 'Click Me' } }; +"#, + ); + + // Use a custom include pattern that matches .stories.ts files + let affected = get_affected_with_include(vec![r"\.stories\.(ts|js)x?$".to_string()]); + + // proj2 should be affected because the .stories.ts file matches the custom pattern + assert!( + affected.contains(&"proj2".to_string()), + "proj2 should be affected when a .stories.ts file changes (custom include pattern). Got: {:?}", + affected + ); +} From acd8ab4f9f43eb446d6ef09b743ebd1e7995e78a Mon Sep 17 00:00:00 2001 From: Elad Bezalel Date: Mon, 9 Feb 2026 10:06:41 +0200 Subject: [PATCH 2/2] fix: respect ignored_paths in include matching and allow opt-out - Include pattern matching now skips files matching ignored_paths - Passing a single empty string in include disables default patterns - Documented opt-out mechanism in TrueAffectedConfig Co-authored-by: Cursor --- src/core.rs | 22 +++++++++++++++++++--- src/types.rs | 1 + 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/core.rs b/src/core.rs index 044a525..52b48dd 100644 --- a/src/core.rs +++ b/src/core.rs @@ -17,6 +17,18 @@ use tracing::debug; /// Default include pattern: matches test/spec files (.spec.ts, .test.tsx, etc.) const DEFAULT_INCLUDE_PATTERN: &str = r"\.(spec|test)\.(ts|js)x?$"; +fn is_ignored_path(file_path: &Path, ignored_paths: &[String]) -> bool { + if ignored_paths.is_empty() { + return false; + } + + let file_str = file_path.to_string_lossy(); + ignored_paths + .iter() + .filter(|ignored| !ignored.trim().is_empty()) + .any(|ignored| file_str.contains(ignored)) +} + /// Mutable state for tracking affected symbols during analysis struct AffectedState<'a> { affected_packages: &'a mut FxHashSet, @@ -76,11 +88,12 @@ fn find_affected_internal( // Step 4b: Process included file patterns (e.g., test files) // Files matching include patterns directly mark their owning project as affected, // matching traf's changedIncludedFilesPackages behavior. - let default_include = vec![DEFAULT_INCLUDE_PATTERN.to_string()]; let include_patterns = if config.include.is_empty() { - &default_include + vec![DEFAULT_INCLUDE_PATTERN.to_string()] + } else if config.include.len() == 1 && config.include[0].trim().is_empty() { + vec![] } else { - &config.include + config.include.clone() }; let compiled_patterns: Vec = include_patterns @@ -95,6 +108,9 @@ fn find_affected_internal( .collect(); for changed_file in &changed_files { + if is_ignored_path(&changed_file.file_path, &config.ignored_paths) { + continue; + } let file_str = changed_file.file_path.to_string_lossy(); if compiled_patterns.iter().any(|re| re.is_match(&file_str)) { if let Some(pkg) = utils::get_package_name_by_path(&changed_file.file_path, &config.projects) diff --git a/src/types.rs b/src/types.rs index 73e76bd..5d2650b 100644 --- a/src/types.rs +++ b/src/types.rs @@ -97,6 +97,7 @@ pub struct TrueAffectedConfig { pub projects: Vec, /// Additional file patterns to include (regex patterns). /// When empty, defaults to matching test files: `\.(spec|test)\.(ts|js)x?$` + /// Use a single empty string to disable default include patterns. pub include: Vec, /// Paths to ignore #[allow(dead_code)]