Skip to content

Commit 2663222

Browse files
authored
feat: merge synthetic task's input config with parent cache config (#296)
## Summary - When a synthetic task (e.g., `vp lint` → `oxlint`) specifies `input` globs in its cache config, those were silently dropped during the merge with the parent task's config. Now resolves and merges them into the parent's `ResolvedInputConfig`. - The parent's `includes_auto` flag is preserved — the user's explicit choice always takes precedence over the synthetic handler's preference. - Threads `package_dir` separately from `cwd` so glob patterns are resolved relative to the correct base directory. ## Test plan - [x] 8 unit tests covering all merge cases: no input, globs merged, auto ignored, negative globs, cache disabled, etc. - [x] All existing plan snapshot tests pass - [x] Cross-platform paths in tests (Unix + Windows) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
1 parent 8b4c8d3 commit 2663222

File tree

5 files changed

+268
-12
lines changed

5 files changed

+268
-12
lines changed

CLAUDE.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,17 @@ Tasks are defined in `vite-task.json`:
105105
}
106106
```
107107

108+
## Internal vs Public Naming
109+
110+
This repo uses internal names that differ from the public-facing product. In code comments, docs, and user-facing strings, use the public names:
111+
112+
| Internal (this repo) | Public (Vite+) |
113+
| -------------------- | --------------- |
114+
| `vt` | `vp` |
115+
| `vite-task.json` | `vite.config.*` |
116+
117+
`vite-task.json` and `vt` are fine in implementation code, test fixtures, and CLAUDE.md itself — just not in doc comments or user-facing messages.
118+
108119
## Code Constraints
109120

110121
### Required Patterns

crates/vite_task_graph/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use crate::display::TaskDisplay;
1919

2020
/// The type of a task dependency edge in the task graph.
2121
///
22-
/// Currently only `Explicit` is produced (from `dependsOn` in `vite-task.json`).
22+
/// Currently only `Explicit` is produced (from `dependsOn` in the task config).
2323
/// Topological ordering is handled at query time via the package subgraph rather
2424
/// than by pre-computing edges in the task graph.
2525
#[derive(Debug, Clone, Copy, Serialize)]

crates/vite_task_graph/src/query/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ pub struct TaskQuery {
5252
/// The task name to run within each selected package.
5353
pub task_name: Str,
5454

55-
/// Whether to include explicit `dependsOn` dependencies from `vite-task.json`.
55+
/// Whether to include explicit `dependsOn` dependencies from the task config.
5656
pub include_explicit_deps: bool,
5757
}
5858

crates/vite_task_plan/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ pub fn plan_synthetic(
256256
synthetic_plan_request,
257257
Some(execution_cache_key),
258258
cwd,
259+
cwd,
259260
ParentCacheConfig::None,
260261
)
261262
}

crates/vite_task_plan/src/plan.rs

Lines changed: 254 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ use vite_str::Str;
2020
use vite_task_graph::{
2121
TaskNodeIndex, TaskSource,
2222
config::{
23-
CacheConfig, ResolvedGlobalCacheConfig, ResolvedTaskOptions,
23+
CacheConfig, EnabledCacheConfig, ResolvedGlobalCacheConfig, ResolvedInputConfig,
24+
ResolvedTaskOptions,
2425
user::{UserCacheConfig, UserTaskOptions},
2526
},
2627
query::TaskQuery,
@@ -281,6 +282,7 @@ async fn plan_task_as_execution_node(
281282
synthetic_plan_request,
282283
Some(task_execution_cache_key),
283284
&cwd,
285+
package_path,
284286
parent_cache_config,
285287
)?;
286288
ExecutionItemKind::Leaf(LeafExecutionKind::Spawn(spawn_execution))
@@ -433,7 +435,7 @@ pub enum ParentCacheConfig {
433435
fn resolve_synthetic_cache_config(
434436
parent: ParentCacheConfig,
435437
synthetic_cache_config: UserCacheConfig,
436-
cwd: &Arc<AbsolutePath>,
438+
package_dir: &AbsolutePath,
437439
workspace_path: &AbsolutePath,
438440
) -> Result<Option<CacheConfig>, Error> {
439441
match parent {
@@ -445,7 +447,7 @@ fn resolve_synthetic_cache_config(
445447
cwd_relative_to_package: None,
446448
depends_on: None,
447449
},
448-
cwd,
450+
&package_dir.into(),
449451
workspace_path,
450452
)
451453
.map_err(Error::ResolveTaskConfig)?
@@ -458,12 +460,33 @@ fn resolve_synthetic_cache_config(
458460
Ok(match synthetic_cache_config {
459461
UserCacheConfig::Disabled { .. } => Option::None,
460462
UserCacheConfig::Enabled { enabled_cache_config, .. } => {
461-
if let Some(extra_envs) = enabled_cache_config.env {
462-
parent_config.env_config.fingerprinted_envs.extend(extra_envs.into_vec());
463-
}
464-
if let Some(extra_pts) = enabled_cache_config.untracked_env {
465-
parent_config.env_config.untracked_env.extend(extra_pts);
463+
let EnabledCacheConfig { env, untracked_env, input } = enabled_cache_config;
464+
parent_config.env_config.fingerprinted_envs.extend(env.unwrap_or_default());
465+
parent_config
466+
.env_config
467+
.untracked_env
468+
.extend(untracked_env.unwrap_or_default());
469+
470+
if let Some(input) = input {
471+
let synthetic_input = ResolvedInputConfig::from_user_config(
472+
Some(&input),
473+
package_dir,
474+
workspace_path,
475+
)
476+
.map_err(Error::ResolveTaskConfig)?;
477+
// Merge globs but preserve the parent's includes_auto —
478+
// the user's explicit choice takes precedence
479+
// over the synthetic handler's preference.
480+
parent_config
481+
.input_config
482+
.positive_globs
483+
.extend(synthetic_input.positive_globs);
484+
parent_config
485+
.input_config
486+
.negative_globs
487+
.extend(synthetic_input.negative_globs);
466488
}
489+
467490
Some(parent_config)
468491
}
469492
})
@@ -478,13 +501,18 @@ pub fn plan_synthetic_request(
478501
synthetic_plan_request: SyntheticPlanRequest,
479502
execution_cache_key: Option<ExecutionCacheKey>,
480503
cwd: &Arc<AbsolutePath>,
504+
package_dir: &AbsolutePath,
481505
parent_cache_config: ParentCacheConfig,
482506
) -> Result<SpawnExecution, Error> {
483507
let SyntheticPlanRequest { program, args, cache_config, envs } = synthetic_plan_request;
484508

485509
let program_path = which(&program, &envs, cwd)?;
486-
let resolved_cache_config =
487-
resolve_synthetic_cache_config(parent_cache_config, cache_config, cwd, workspace_path)?;
510+
let resolved_cache_config = resolve_synthetic_cache_config(
511+
parent_cache_config,
512+
cache_config,
513+
package_dir,
514+
workspace_path,
515+
)?;
488516
let resolved_options =
489517
ResolvedTaskOptions { cwd: Arc::clone(cwd), cache_config: resolved_cache_config };
490518

@@ -749,3 +777,219 @@ pub async fn plan_query_request(
749777
Error::CycleDependencyDetected(displays)
750778
})
751779
}
780+
781+
#[cfg(test)]
782+
mod tests {
783+
use std::collections::BTreeSet;
784+
785+
use rustc_hash::FxHashSet;
786+
use vite_path::AbsolutePathBuf;
787+
use vite_str::Str;
788+
use vite_task_graph::config::{
789+
CacheConfig, EnabledCacheConfig, EnvConfig, ResolvedInputConfig,
790+
user::{UserCacheConfig, UserInputEntry},
791+
};
792+
793+
use super::{ParentCacheConfig, resolve_synthetic_cache_config};
794+
795+
fn test_paths() -> (AbsolutePathBuf, AbsolutePathBuf) {
796+
if cfg!(windows) {
797+
(
798+
AbsolutePathBuf::new("C:\\workspace\\packages\\my-pkg".into()).unwrap(),
799+
AbsolutePathBuf::new("C:\\workspace".into()).unwrap(),
800+
)
801+
} else {
802+
(
803+
AbsolutePathBuf::new("/workspace/packages/my-pkg".into()).unwrap(),
804+
AbsolutePathBuf::new("/workspace".into()).unwrap(),
805+
)
806+
}
807+
}
808+
809+
fn parent_config(includes_auto: bool, positive_globs: &[&str]) -> CacheConfig {
810+
CacheConfig {
811+
env_config: EnvConfig {
812+
fingerprinted_envs: FxHashSet::default(),
813+
untracked_env: FxHashSet::default(),
814+
},
815+
input_config: ResolvedInputConfig {
816+
includes_auto,
817+
positive_globs: positive_globs.iter().map(|s| Str::from(*s)).collect(),
818+
negative_globs: BTreeSet::new(),
819+
},
820+
}
821+
}
822+
823+
fn globs(items: &[&str]) -> BTreeSet<Str> {
824+
items.iter().map(|s| Str::from(*s)).collect()
825+
}
826+
827+
#[test]
828+
fn synthetic_input_none_preserves_parent() {
829+
let (pkg, ws) = test_paths();
830+
let parent = parent_config(true, &["src/**"]);
831+
let result = resolve_synthetic_cache_config(
832+
ParentCacheConfig::Inherited(parent),
833+
UserCacheConfig::with_config(EnabledCacheConfig {
834+
env: None,
835+
untracked_env: None,
836+
input: None,
837+
}),
838+
&pkg,
839+
&ws,
840+
)
841+
.unwrap()
842+
.unwrap();
843+
assert!(result.input_config.includes_auto);
844+
assert_eq!(result.input_config.positive_globs, globs(&["src/**"]));
845+
assert_eq!(result.input_config.negative_globs, globs(&[]));
846+
}
847+
848+
#[test]
849+
fn synthetic_globs_merged_into_parent_default_auto() {
850+
let (pkg, ws) = test_paths();
851+
let parent = parent_config(true, &[]);
852+
let result = resolve_synthetic_cache_config(
853+
ParentCacheConfig::Inherited(parent),
854+
UserCacheConfig::with_config(EnabledCacheConfig {
855+
env: None,
856+
untracked_env: None,
857+
input: Some(vec![UserInputEntry::Glob("config/**".into())]),
858+
}),
859+
&pkg,
860+
&ws,
861+
)
862+
.unwrap()
863+
.unwrap();
864+
assert!(result.input_config.includes_auto);
865+
assert_eq!(result.input_config.positive_globs, globs(&["packages/my-pkg/config/**"]));
866+
assert_eq!(result.input_config.negative_globs, globs(&[]));
867+
}
868+
869+
#[test]
870+
fn synthetic_globs_merged_into_parent_explicit_input() {
871+
let (pkg, ws) = test_paths();
872+
let parent = parent_config(false, &["src/**"]);
873+
let result = resolve_synthetic_cache_config(
874+
ParentCacheConfig::Inherited(parent),
875+
UserCacheConfig::with_config(EnabledCacheConfig {
876+
env: None,
877+
untracked_env: None,
878+
input: Some(vec![UserInputEntry::Glob("config/**".into())]),
879+
}),
880+
&pkg,
881+
&ws,
882+
)
883+
.unwrap()
884+
.unwrap();
885+
assert!(!result.input_config.includes_auto);
886+
assert_eq!(
887+
result.input_config.positive_globs,
888+
globs(&["packages/my-pkg/config/**", "src/**"])
889+
);
890+
assert_eq!(result.input_config.negative_globs, globs(&[]));
891+
}
892+
893+
#[test]
894+
fn synthetic_auto_ignored_when_parent_has_explicit_input() {
895+
let (pkg, ws) = test_paths();
896+
let parent = parent_config(false, &["src/**"]);
897+
let result = resolve_synthetic_cache_config(
898+
ParentCacheConfig::Inherited(parent),
899+
UserCacheConfig::with_config(EnabledCacheConfig {
900+
env: None,
901+
untracked_env: None,
902+
input: Some(vec![
903+
UserInputEntry::Glob("config/**".into()),
904+
UserInputEntry::Auto(vite_task_graph::config::user::AutoInput { auto: true }),
905+
]),
906+
}),
907+
&pkg,
908+
&ws,
909+
)
910+
.unwrap()
911+
.unwrap();
912+
// User's explicit choice (no auto) takes precedence
913+
assert!(!result.input_config.includes_auto);
914+
assert_eq!(
915+
result.input_config.positive_globs,
916+
globs(&["packages/my-pkg/config/**", "src/**"])
917+
);
918+
assert_eq!(result.input_config.negative_globs, globs(&[]));
919+
}
920+
921+
#[test]
922+
fn parent_auto_preserved_with_synthetic_globs() {
923+
let (pkg, ws) = test_paths();
924+
let parent = parent_config(true, &["tests/**"]);
925+
let result = resolve_synthetic_cache_config(
926+
ParentCacheConfig::Inherited(parent),
927+
UserCacheConfig::with_config(EnabledCacheConfig {
928+
env: None,
929+
untracked_env: None,
930+
input: Some(vec![UserInputEntry::Glob("config/**".into())]),
931+
}),
932+
&pkg,
933+
&ws,
934+
)
935+
.unwrap()
936+
.unwrap();
937+
assert!(result.input_config.includes_auto);
938+
assert_eq!(
939+
result.input_config.positive_globs,
940+
globs(&["packages/my-pkg/config/**", "tests/**"])
941+
);
942+
assert_eq!(result.input_config.negative_globs, globs(&[]));
943+
}
944+
945+
#[test]
946+
fn parent_cache_disabled_ignores_synthetic_input() {
947+
let (pkg, ws) = test_paths();
948+
let result = resolve_synthetic_cache_config(
949+
ParentCacheConfig::Disabled,
950+
UserCacheConfig::with_config(EnabledCacheConfig {
951+
env: None,
952+
untracked_env: None,
953+
input: Some(vec![UserInputEntry::Glob("config/**".into())]),
954+
}),
955+
&pkg,
956+
&ws,
957+
)
958+
.unwrap();
959+
assert!(result.is_none());
960+
}
961+
962+
#[test]
963+
fn synthetic_disables_cache_despite_parent() {
964+
let (pkg, ws) = test_paths();
965+
let parent = parent_config(true, &["src/**"]);
966+
let result = resolve_synthetic_cache_config(
967+
ParentCacheConfig::Inherited(parent),
968+
UserCacheConfig::disabled(),
969+
&pkg,
970+
&ws,
971+
)
972+
.unwrap();
973+
assert!(result.is_none());
974+
}
975+
976+
#[test]
977+
fn synthetic_negative_globs_merged() {
978+
let (pkg, ws) = test_paths();
979+
let parent = parent_config(true, &["src/**"]);
980+
let result = resolve_synthetic_cache_config(
981+
ParentCacheConfig::Inherited(parent),
982+
UserCacheConfig::with_config(EnabledCacheConfig {
983+
env: None,
984+
untracked_env: None,
985+
input: Some(vec![UserInputEntry::Glob("!dist/**".into())]),
986+
}),
987+
&pkg,
988+
&ws,
989+
)
990+
.unwrap()
991+
.unwrap();
992+
assert_eq!(result.input_config.positive_globs, globs(&["src/**"]));
993+
assert_eq!(result.input_config.negative_globs, globs(&["packages/my-pkg/dist/**"]));
994+
}
995+
}

0 commit comments

Comments
 (0)