From 191c06c791790e870e4e26f1e8e58d56cbff28de Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Mar 2026 05:35:19 +0000 Subject: [PATCH 1/4] feat: add object form for input entries with pattern and base fields Allow glob patterns to resolve relative to the workspace root using `{ "pattern": "...", "base": "workspace" | "package" }` syntax. https://claude.ai/code/session_01KiaZHtCW4hCsdJyPuBNPnw --- CHANGELOG.md | 1 + CLAUDE.md | 2 +- crates/vite_task_graph/run-config.ts | 17 ++- crates/vite_task_graph/src/config/mod.rs | 130 +++++++++++++++--- crates/vite_task_graph/src/config/user.rs | 119 +++++++++++++++- .../input-workspace-base/package.json | 3 + .../input-workspace-base/snapshots.toml | 1 + .../snapshots/task graph.snap | 46 +++++++ .../input-workspace-base/vite-task.json | 13 ++ docs/inputs.md | 42 ++++-- 10 files changed, 339 insertions(+), 35 deletions(-) create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/input-workspace-base/package.json create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/input-workspace-base/snapshots.toml create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/input-workspace-base/snapshots/task graph.snap create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/input-workspace-base/vite-task.json diff --git a/CHANGELOG.md b/CHANGELOG.md index c2db2cde..743d31aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +- **Added** object form for `input` entries: `{ "pattern": "...", "base": "workspace" | "package" }` to resolve glob patterns relative to the workspace root instead of the package directory - **Fixed** arguments after the task name being consumed by `vp` instead of passed through to the task ([#286](https://github.com/voidzero-dev/vite-task/pull/286), [#290](https://github.com/voidzero-dev/vite-task/pull/290)) - **Changed** default untracked env patterns to align with Turborepo, covering more CI and platform-specific variables ([#262](https://github.com/voidzero-dev/vite-task/pull/262)) - **Added** `--log=interleaved|labeled|grouped` flag to control task output display: `interleaved` (default) streams directly, `labeled` prefixes lines with `[pkg#task]`, `grouped` buffers output per task ([#266](https://github.com/voidzero-dev/vite-task/pull/266)) diff --git a/CLAUDE.md b/CLAUDE.md index 3a3043df..4cf9eb15 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -99,7 +99,7 @@ Tasks are defined in `vite-task.json`: "cache": true, "env": ["NODE_ENV"], "untrackedEnv": ["CI"], - "input": ["src/**", "!dist/**", { "auto": true }] + "input": ["src/**", "!dist/**", { "auto": true }, { "pattern": "tsconfig.json", "base": "workspace" }] } } } diff --git a/crates/vite_task_graph/run-config.ts b/crates/vite_task_graph/run-config.ts index 3546917b..9097c51b 100644 --- a/crates/vite_task_graph/run-config.ts +++ b/crates/vite_task_graph/run-config.ts @@ -1,5 +1,7 @@ // This file is auto-generated by `cargo test`. Do not edit manually. +export type InputBase = 'package' | 'workspace'; + export type Task = { /** * The command to run for the task. @@ -32,14 +34,23 @@ export type Task = { * * - Omitted: automatically tracks which files the task reads * - `[]` (empty): disables file tracking entirely - * - Glob patterns (e.g. `"src/**"`) select specific files + * - Glob patterns (e.g. `"src/**"`) select specific files, relative to the package directory + * - `{pattern: "...", base: "workspace" | "package"}` specifies a glob with an explicit base directory * - `{auto: true}` enables automatic file tracking * - Negative patterns (e.g. `"!dist/**"`) exclude matched files - * - * Patterns are relative to the package directory. */ input?: Array< | string + | { + /** + * The glob pattern (positive or negative starting with `!`) + */ + pattern: string; + /** + * The base directory for resolving the pattern + */ + base: InputBase; + } | { /** * Automatically track which files the task reads diff --git a/crates/vite_task_graph/src/config/mod.rs b/crates/vite_task_graph/src/config/mod.rs index c2b5a23e..4dbe67a2 100644 --- a/crates/vite_task_graph/src/config/mod.rs +++ b/crates/vite_task_graph/src/config/mod.rs @@ -7,8 +7,8 @@ use monostate::MustBe; use rustc_hash::FxHashSet; use serde::Serialize; pub use user::{ - EnabledCacheConfig, ResolvedGlobalCacheConfig, UserCacheConfig, UserGlobalCacheConfig, - UserInputEntry, UserInputsConfig, UserRunConfig, UserTaskConfig, + EnabledCacheConfig, InputBase, ResolvedGlobalCacheConfig, UserCacheConfig, + UserGlobalCacheConfig, UserInputEntry, UserInputsConfig, UserRunConfig, UserTaskConfig, }; use vite_path::AbsolutePath; use vite_str::Str; @@ -156,27 +156,51 @@ impl ResolvedInputConfig { UserInputEntry::Auto { auto: true } => includes_auto = true, UserInputEntry::Auto { auto: false } => {} // Ignore {auto: false} UserInputEntry::Glob(pattern) => { - if let Some(negated) = pattern.strip_prefix('!') { - let resolved = resolve_glob_to_workspace_relative( - negated, - package_dir, - workspace_root, - )?; - negative_globs.insert(resolved); - } else { - let resolved = resolve_glob_to_workspace_relative( - pattern.as_str(), - package_dir, - workspace_root, - )?; - positive_globs.insert(resolved); - } + Self::insert_glob( + pattern.as_str(), + package_dir, + workspace_root, + &mut positive_globs, + &mut negative_globs, + )?; + } + UserInputEntry::GlobWithBase { pattern, base } => { + let base_dir = match base { + InputBase::Package => package_dir, + InputBase::Workspace => workspace_root, + }; + Self::insert_glob( + pattern.as_str(), + base_dir, + workspace_root, + &mut positive_globs, + &mut negative_globs, + )?; } } } Ok(Self { includes_auto, positive_globs, negative_globs }) } + + /// Insert a glob pattern into the appropriate set (positive or negative), + /// resolving it relative to the given base directory. + fn insert_glob( + pattern: &str, + base_dir: &AbsolutePath, + workspace_root: &AbsolutePath, + positive_globs: &mut BTreeSet, + negative_globs: &mut BTreeSet, + ) -> Result<(), ResolveTaskConfigError> { + if let Some(negated) = pattern.strip_prefix('!') { + let resolved = resolve_glob_to_workspace_relative(negated, base_dir, workspace_root)?; + negative_globs.insert(resolved); + } else { + let resolved = resolve_glob_to_workspace_relative(pattern, base_dir, workspace_root)?; + positive_globs.insert(resolved); + } + Ok(()) + } } /// Resolve a single glob pattern to be workspace-root-relative. @@ -524,4 +548,76 @@ mod tests { assert!(result.is_err()); assert!(matches!(result.unwrap_err(), ResolveTaskConfigError::GlobOutsideWorkspace { .. })); } + + #[test] + fn test_resolved_input_config_glob_with_workspace_base() { + let (pkg, ws) = test_paths(); + let user_inputs = vec![UserInputEntry::GlobWithBase { + pattern: "configs/tsconfig.json".into(), + base: InputBase::Workspace, + }]; + let config = ResolvedInputConfig::from_user_config(Some(&user_inputs), &pkg, &ws).unwrap(); + assert!(!config.includes_auto); + assert_eq!(config.positive_globs.len(), 1); + // Workspace-base: should NOT have the package prefix + assert!( + config.positive_globs.contains("configs/tsconfig.json"), + "expected 'configs/tsconfig.json', got {:?}", + config.positive_globs + ); + } + + #[test] + fn test_resolved_input_config_negative_glob_with_workspace_base() { + let (pkg, ws) = test_paths(); + let user_inputs = vec![UserInputEntry::GlobWithBase { + pattern: "!dist/**".into(), + base: InputBase::Workspace, + }]; + let config = ResolvedInputConfig::from_user_config(Some(&user_inputs), &pkg, &ws).unwrap(); + assert_eq!(config.negative_globs.len(), 1); + assert!( + config.negative_globs.contains("dist/**"), + "expected 'dist/**', got {:?}", + config.negative_globs + ); + } + + #[test] + fn test_resolved_input_config_glob_with_package_base_explicit() { + let (pkg, ws) = test_paths(); + // Explicit "package" base should behave same as bare string + let user_inputs = vec![UserInputEntry::GlobWithBase { + pattern: "src/**/*.ts".into(), + base: InputBase::Package, + }]; + let config = ResolvedInputConfig::from_user_config(Some(&user_inputs), &pkg, &ws).unwrap(); + assert_eq!(config.positive_globs.len(), 1); + assert!( + config.positive_globs.contains("packages/my-pkg/src/**/*.ts"), + "expected 'packages/my-pkg/src/**/*.ts', got {:?}", + config.positive_globs + ); + } + + #[test] + fn test_resolved_input_config_mixed_bases() { + let (pkg, ws) = test_paths(); + let user_inputs = vec![ + UserInputEntry::Glob("src/**".into()), + UserInputEntry::GlobWithBase { + pattern: "configs/**".into(), + base: InputBase::Workspace, + }, + UserInputEntry::Auto { auto: true }, + UserInputEntry::GlobWithBase { pattern: "!dist/**".into(), base: InputBase::Workspace }, + ]; + let config = ResolvedInputConfig::from_user_config(Some(&user_inputs), &pkg, &ws).unwrap(); + assert!(config.includes_auto); + assert_eq!(config.positive_globs.len(), 2); + assert!(config.positive_globs.contains("packages/my-pkg/src/**")); + assert!(config.positive_globs.contains("configs/**")); + assert_eq!(config.negative_globs.len(), 1); + assert!(config.negative_globs.contains("dist/**")); + } } diff --git a/crates/vite_task_graph/src/config/user.rs b/crates/vite_task_graph/src/config/user.rs index f568920b..866a354b 100644 --- a/crates/vite_task_graph/src/config/user.rs +++ b/crates/vite_task_graph/src/config/user.rs @@ -10,16 +10,37 @@ use ts_rs::TS; use vite_path::RelativePathBuf; use vite_str::Str; +/// The base directory for resolving a glob pattern. +#[derive(Debug, Deserialize, PartialEq, Eq, Clone)] +#[cfg_attr(all(test, not(clippy)), derive(TS))] +#[serde(rename_all = "lowercase")] +pub enum InputBase { + /// Resolve relative to the package directory (where `package.json` is located) + Package, + /// Resolve relative to the workspace root + Workspace, +} + /// A single input entry in the `input` array. /// -/// Inputs can be either glob patterns (strings) or auto-inference directives (`{auto: true}`). +/// Inputs can be: +/// - Glob patterns as strings (resolved relative to the package directory) +/// - Object form with explicit base: `{ "pattern": "...", "base": "workspace" | "package" }` +/// - Auto-inference directives: `{ "auto": true }` #[derive(Debug, Deserialize, PartialEq, Eq, Clone)] // TS derive macro generates code using std types that clippy disallows; skip derive during linting #[cfg_attr(all(test, not(clippy)), derive(TS))] #[serde(untagged)] pub enum UserInputEntry { - /// Glob pattern (positive or negative starting with `!`) + /// Glob pattern (positive or negative starting with `!`), resolved relative to package dir Glob(Str), + /// Glob pattern with explicit base directory + GlobWithBase { + /// The glob pattern (positive or negative starting with `!`) + pattern: Str, + /// The base directory for resolving the pattern + base: InputBase, + }, /// Auto-inference directive Auto { /// Automatically track which files the task reads @@ -85,11 +106,10 @@ pub struct EnabledCacheConfig { /// /// - Omitted: automatically tracks which files the task reads /// - `[]` (empty): disables file tracking entirely - /// - Glob patterns (e.g. `"src/**"`) select specific files + /// - Glob patterns (e.g. `"src/**"`) select specific files, relative to the package directory + /// - `{pattern: "...", base: "workspace" | "package"}` specifies a glob with an explicit base directory /// - `{auto: true}` enables automatic file tracking /// - Negative patterns (e.g. `"!dist/**"`) exclude matched files - /// - /// Patterns are relative to the package directory. #[serde(default)] #[cfg_attr(all(test, not(clippy)), ts(inline))] pub input: Option, @@ -509,6 +529,95 @@ mod tests { ); } + #[test] + fn test_input_glob_with_base_workspace() { + let user_config_json = json!({ + "input": [{ "pattern": "configs/tsconfig.json", "base": "workspace" }] + }); + let config: EnabledCacheConfig = serde_json::from_value(user_config_json).unwrap(); + assert_eq!( + config.input, + Some(vec![UserInputEntry::GlobWithBase { + pattern: "configs/tsconfig.json".into(), + base: InputBase::Workspace, + }]) + ); + } + + #[test] + fn test_input_glob_with_base_package() { + let user_config_json = json!({ + "input": [{ "pattern": "src/**", "base": "package" }] + }); + let config: EnabledCacheConfig = serde_json::from_value(user_config_json).unwrap(); + assert_eq!( + config.input, + Some(vec![UserInputEntry::GlobWithBase { + pattern: "src/**".into(), + base: InputBase::Package, + }]) + ); + } + + #[test] + fn test_input_negative_glob_with_base() { + let user_config_json = json!({ + "input": [{ "pattern": "!dist/**", "base": "workspace" }] + }); + let config: EnabledCacheConfig = serde_json::from_value(user_config_json).unwrap(); + assert_eq!( + config.input, + Some(vec![UserInputEntry::GlobWithBase { + pattern: "!dist/**".into(), + base: InputBase::Workspace, + }]) + ); + } + + #[test] + fn test_input_glob_with_base_missing_base_error() { + // { "pattern": "src/**" } without "base" should fail (base is required) + let user_config_json = json!({ + "input": [{ "pattern": "src/**" }] + }); + let result = serde_json::from_value::(user_config_json); + assert!(result.is_err(), "missing 'base' field should produce an error"); + } + + #[test] + fn test_input_glob_with_base_invalid_base_error() { + let user_config_json = json!({ + "input": [{ "pattern": "src/**", "base": "invalid" }] + }); + let result = serde_json::from_value::(user_config_json); + assert!(result.is_err(), "invalid 'base' value should produce an error"); + } + + #[test] + fn test_input_mixed_with_glob_base() { + let user_config_json = json!({ + "input": [ + "package.json", + { "pattern": "configs/**", "base": "workspace" }, + { "auto": true }, + "!node_modules/**" + ] + }); + let config: EnabledCacheConfig = serde_json::from_value(user_config_json).unwrap(); + assert_eq!( + config.input, + Some(vec![ + UserInputEntry::Glob("package.json".into()), + UserInputEntry::GlobWithBase { + pattern: "configs/**".into(), + base: InputBase::Workspace, + }, + UserInputEntry::Auto { auto: true }, + UserInputEntry::Glob("!node_modules/**".into()), + ]) + ); + } + #[test] fn test_input_with_cache_false_error() { // input with cache: false should produce a serde error due to deny_unknown_fields diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/input-workspace-base/package.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/input-workspace-base/package.json new file mode 100644 index 00000000..600e5f1a --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/input-workspace-base/package.json @@ -0,0 +1,3 @@ +{ + "name": "test" +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/input-workspace-base/snapshots.toml b/crates/vite_task_plan/tests/plan_snapshots/fixtures/input-workspace-base/snapshots.toml new file mode 100644 index 00000000..d27359b3 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/input-workspace-base/snapshots.toml @@ -0,0 +1 @@ +# Test glob patterns with workspace base directory diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/input-workspace-base/snapshots/task graph.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/input-workspace-base/snapshots/task graph.snap new file mode 100644 index 00000000..935ea2ff --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/input-workspace-base/snapshots/task graph.snap @@ -0,0 +1,46 @@ +--- +source: crates/vite_task_plan/tests/plan_snapshots/main.rs +expression: task_graph_json +input_file: crates/vite_task_plan/tests/plan_snapshots/fixtures/input-workspace-base +--- +[ + { + "key": [ + "/", + "build" + ], + "node": { + "task_display": { + "package_name": "test", + "task_name": "build", + "package_path": "/" + }, + "resolved_config": { + "command": "echo build", + "resolved_options": { + "cwd": "/", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": false, + "positive_globs": [ + "configs/tsconfig.json", + "src/**" + ], + "negative_globs": [ + "dist/**" + ] + } + } + } + }, + "source": "TaskConfig" + }, + "neighbors": [] + } +] diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/input-workspace-base/vite-task.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/input-workspace-base/vite-task.json new file mode 100644 index 00000000..4808e319 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/input-workspace-base/vite-task.json @@ -0,0 +1,13 @@ +{ + "tasks": { + "build": { + "command": "echo build", + "input": [ + "src/**", + { "pattern": "configs/tsconfig.json", "base": "workspace" }, + { "pattern": "!dist/**", "base": "workspace" } + ], + "cache": true + } + } +} diff --git a/docs/inputs.md b/docs/inputs.md index ddd10772..1f97e39a 100644 --- a/docs/inputs.md +++ b/docs/inputs.md @@ -62,6 +62,29 @@ Exclude files from tracking using `!` prefix: Negative patterns filter out files that would otherwise be matched by positive patterns or auto-inference. +### Glob with Base Directory + +By default, glob patterns are resolved relative to the package directory. Use the object form to resolve patterns relative to a different base: + +```json +{ + "inputs": [ + "src/**", + { "pattern": "configs/tsconfig.json", "base": "workspace" }, + { "pattern": "!dist/**", "base": "workspace" } + ] +} +``` + +The `base` field is required and accepts: + +- `"package"` — resolve relative to the package directory (same as bare string globs) +- `"workspace"` — resolve relative to the workspace root + +This is useful for tracking shared configuration files at the workspace root, or excluding workspace-level directories from cache fingerprinting. + +Negation (`!` prefix) works in both bare strings and the object form. + ## Configuration Examples ### Explicit Globs Only @@ -136,20 +159,21 @@ The cache will only invalidate when the command itself or environment variables ## Behavior Summary -| Configuration | Auto-Inference | File Tracking | -| ---------------------------------------- | -------------- | ----------------------------- | -| `inputs` omitted | Enabled | Inferred files | -| `inputs: [{ "auto": true }]` | Enabled | Inferred files | -| `inputs: ["src/**"]` | Disabled | Matched files only | -| `inputs: [{ "auto": true }, "!dist/**"]` | Enabled | Inferred files except `dist/` | -| `inputs: ["pkg.json", { "auto": true }]` | Enabled | `pkg.json` + inferred files | -| `inputs: []` | Disabled | No files tracked | +| Configuration | Auto-Inference | File Tracking | +| --------------------------------------------------------------- | -------------- | ---------------------------------- | +| `inputs` omitted | Enabled | Inferred files | +| `inputs: [{ "auto": true }]` | Enabled | Inferred files | +| `inputs: ["src/**"]` | Disabled | Matched files only | +| `inputs: [{ "auto": true }, "!dist/**"]` | Enabled | Inferred files except `dist/` | +| `inputs: ["pkg.json", { "auto": true }]` | Enabled | `pkg.json` + inferred files | +| `inputs: [{ "pattern": "tsconfig.json", "base": "workspace" }]` | Disabled | Matched files (workspace-relative) | +| `inputs: []` | Disabled | No files tracked | ## Important Notes ### Glob Base Directory -Glob patterns are resolved relative to the **package directory** (where `package.json` is located), not the task's working directory (`cwd`). +By default, glob patterns (bare strings) are resolved relative to the **package directory** (where `package.json` is located), not the task's working directory (`cwd`). To resolve relative to the workspace root instead, use the object form with `"base": "workspace"` (see [Glob with Base Directory](#glob-with-base-directory)). ```json { From 2fba34092117aca69ea0fca67f10fc400f6db071 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Mar 2026 05:36:35 +0000 Subject: [PATCH 2/4] docs: add PR link to changelog entry https://claude.ai/code/session_01KiaZHtCW4hCsdJyPuBNPnw --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 743d31aa..90fa2668 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -- **Added** object form for `input` entries: `{ "pattern": "...", "base": "workspace" | "package" }` to resolve glob patterns relative to the workspace root instead of the package directory +- **Added** object form for `input` entries: `{ "pattern": "...", "base": "workspace" | "package" }` to resolve glob patterns relative to the workspace root instead of the package directory ([#295](https://github.com/voidzero-dev/vite-task/pull/295)) - **Fixed** arguments after the task name being consumed by `vp` instead of passed through to the task ([#286](https://github.com/voidzero-dev/vite-task/pull/286), [#290](https://github.com/voidzero-dev/vite-task/pull/290)) - **Changed** default untracked env patterns to align with Turborepo, covering more CI and platform-specific variables ([#262](https://github.com/voidzero-dev/vite-task/pull/262)) - **Added** `--log=interleaved|labeled|grouped` flag to control task output display: `interleaved` (default) streams directly, `labeled` prefixes lines with `[pkg#task]`, `grouped` buffers output per task ([#266](https://github.com/voidzero-dev/vite-task/pull/266)) From b494a322521819f8f3b8097de6274afdc645bc69 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Mar 2026 05:42:50 +0000 Subject: [PATCH 3/4] fix: use subpackage in workspace-base fixture so paths diverge The fixture was a root package where package_dir == workspace_root, making workspace-base indistinguishable from package-base. Now uses a packages/app subpackage so the snapshot clearly shows the difference. https://claude.ai/code/session_01KiaZHtCW4hCsdJyPuBNPnw --- .../fixtures/input-workspace-base/package.json | 2 +- .../input-workspace-base/packages/app/package.json | 3 +++ .../{ => packages/app}/vite-task.json | 0 .../fixtures/input-workspace-base/pnpm-workspace.yaml | 2 ++ .../input-workspace-base/snapshots/task graph.snap | 10 +++++----- 5 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/input-workspace-base/packages/app/package.json rename crates/vite_task_plan/tests/plan_snapshots/fixtures/input-workspace-base/{ => packages/app}/vite-task.json (100%) create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/input-workspace-base/pnpm-workspace.yaml diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/input-workspace-base/package.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/input-workspace-base/package.json index 600e5f1a..7636edf1 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/input-workspace-base/package.json +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/input-workspace-base/package.json @@ -1,3 +1,3 @@ { - "name": "test" + "name": "root" } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/input-workspace-base/packages/app/package.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/input-workspace-base/packages/app/package.json new file mode 100644 index 00000000..fd1f8f63 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/input-workspace-base/packages/app/package.json @@ -0,0 +1,3 @@ +{ + "name": "app" +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/input-workspace-base/vite-task.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/input-workspace-base/packages/app/vite-task.json similarity index 100% rename from crates/vite_task_plan/tests/plan_snapshots/fixtures/input-workspace-base/vite-task.json rename to crates/vite_task_plan/tests/plan_snapshots/fixtures/input-workspace-base/packages/app/vite-task.json diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/input-workspace-base/pnpm-workspace.yaml b/crates/vite_task_plan/tests/plan_snapshots/fixtures/input-workspace-base/pnpm-workspace.yaml new file mode 100644 index 00000000..18ec407e --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/input-workspace-base/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - 'packages/*' diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/input-workspace-base/snapshots/task graph.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/input-workspace-base/snapshots/task graph.snap index 935ea2ff..3d2cb5bf 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/input-workspace-base/snapshots/task graph.snap +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/input-workspace-base/snapshots/task graph.snap @@ -6,19 +6,19 @@ input_file: crates/vite_task_plan/tests/plan_snapshots/fixtures/input-workspace- [ { "key": [ - "/", + "/packages/app", "build" ], "node": { "task_display": { - "package_name": "test", + "package_name": "app", "task_name": "build", - "package_path": "/" + "package_path": "/packages/app" }, "resolved_config": { "command": "echo build", "resolved_options": { - "cwd": "/", + "cwd": "/packages/app", "cache_config": { "env_config": { "fingerprinted_envs": [], @@ -30,7 +30,7 @@ input_file: crates/vite_task_plan/tests/plan_snapshots/fixtures/input-workspace- "includes_auto": false, "positive_globs": [ "configs/tsconfig.json", - "src/**" + "packages/app/src/**" ], "negative_globs": [ "dist/**" From 7ccb9af832289a8f3eb3869d866890a94b3f2c09 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Mar 2026 08:19:22 +0000 Subject: [PATCH 4/4] Address Copilot review: fix doc key name and add deny_unknown_fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix `"inputs"` → `"input"` in docs/inputs.md to match actual config field name - Extract GlobWithBase and AutoInput into dedicated structs with `#[serde(deny_unknown_fields)]` so ambiguous objects like `{ "auto": true, "pattern": "...", "base": "workspace" }` are rejected - Add test for mixed auto+glob fields rejection - Regenerate TypeScript type definitions https://claude.ai/code/session_01KiaZHtCW4hCsdJyPuBNPnw --- crates/vite_task_graph/run-config.ts | 38 ++++++------- crates/vite_task_graph/src/config/mod.rs | 46 +++++++++------- crates/vite_task_graph/src/config/user.rs | 66 +++++++++++++++-------- docs/inputs.md | 20 +++---- 4 files changed, 99 insertions(+), 71 deletions(-) diff --git a/crates/vite_task_graph/run-config.ts b/crates/vite_task_graph/run-config.ts index 9097c51b..69e4a661 100644 --- a/crates/vite_task_graph/run-config.ts +++ b/crates/vite_task_graph/run-config.ts @@ -1,5 +1,23 @@ // This file is auto-generated by `cargo test`. Do not edit manually. +export type AutoInput = { + /** + * Automatically track which files the task reads + */ + auto: boolean; +}; + +export type GlobWithBase = { + /** + * The glob pattern (positive or negative starting with `!`) + */ + pattern: string; + /** + * The base directory for resolving the pattern + */ + base: InputBase; +}; + export type InputBase = 'package' | 'workspace'; export type Task = { @@ -39,25 +57,7 @@ export type Task = { * - `{auto: true}` enables automatic file tracking * - Negative patterns (e.g. `"!dist/**"`) exclude matched files */ - input?: Array< - | string - | { - /** - * The glob pattern (positive or negative starting with `!`) - */ - pattern: string; - /** - * The base directory for resolving the pattern - */ - base: InputBase; - } - | { - /** - * Automatically track which files the task reads - */ - auto: boolean; - } - >; + input?: Array; } | { /** diff --git a/crates/vite_task_graph/src/config/mod.rs b/crates/vite_task_graph/src/config/mod.rs index 4dbe67a2..b70b1650 100644 --- a/crates/vite_task_graph/src/config/mod.rs +++ b/crates/vite_task_graph/src/config/mod.rs @@ -7,8 +7,9 @@ use monostate::MustBe; use rustc_hash::FxHashSet; use serde::Serialize; pub use user::{ - EnabledCacheConfig, InputBase, ResolvedGlobalCacheConfig, UserCacheConfig, - UserGlobalCacheConfig, UserInputEntry, UserInputsConfig, UserRunConfig, UserTaskConfig, + AutoInput, EnabledCacheConfig, GlobWithBase, InputBase, ResolvedGlobalCacheConfig, + UserCacheConfig, UserGlobalCacheConfig, UserInputEntry, UserInputsConfig, UserRunConfig, + UserTaskConfig, }; use vite_path::AbsolutePath; use vite_str::Str; @@ -153,8 +154,8 @@ impl ResolvedInputConfig { for entry in entries { match entry { - UserInputEntry::Auto { auto: true } => includes_auto = true, - UserInputEntry::Auto { auto: false } => {} // Ignore {auto: false} + UserInputEntry::Auto(AutoInput { auto: true }) => includes_auto = true, + UserInputEntry::Auto(AutoInput { auto: false }) => {} // Ignore {auto: false} UserInputEntry::Glob(pattern) => { Self::insert_glob( pattern.as_str(), @@ -164,7 +165,7 @@ impl ResolvedInputConfig { &mut negative_globs, )?; } - UserInputEntry::GlobWithBase { pattern, base } => { + UserInputEntry::GlobWithBase(GlobWithBase { pattern, base }) => { let base_dir = match base { InputBase::Package => package_dir, InputBase::Workspace => workspace_root, @@ -452,7 +453,7 @@ mod tests { #[test] fn test_resolved_input_config_auto_only() { let (pkg, ws) = test_paths(); - let user_inputs = vec![UserInputEntry::Auto { auto: true }]; + let user_inputs = vec![UserInputEntry::Auto(AutoInput { auto: true })]; let config = ResolvedInputConfig::from_user_config(Some(&user_inputs), &pkg, &ws).unwrap(); assert!(config.includes_auto); assert!(config.positive_globs.is_empty()); @@ -462,7 +463,7 @@ mod tests { #[test] fn test_resolved_input_config_auto_false_ignored() { let (pkg, ws) = test_paths(); - let user_inputs = vec![UserInputEntry::Auto { auto: false }]; + let user_inputs = vec![UserInputEntry::Auto(AutoInput { auto: false })]; let config = ResolvedInputConfig::from_user_config(Some(&user_inputs), &pkg, &ws).unwrap(); assert!(!config.includes_auto); assert!(config.positive_globs.is_empty()); @@ -506,7 +507,7 @@ mod tests { let (pkg, ws) = test_paths(); let user_inputs = vec![ UserInputEntry::Glob("package.json".into()), - UserInputEntry::Auto { auto: true }, + UserInputEntry::Auto(AutoInput { auto: true }), UserInputEntry::Glob("!node_modules/**".into()), ]; let config = ResolvedInputConfig::from_user_config(Some(&user_inputs), &pkg, &ws).unwrap(); @@ -521,8 +522,10 @@ mod tests { fn test_resolved_input_config_globs_with_auto() { let (pkg, ws) = test_paths(); // Globs with auto keeps inference enabled - let user_inputs = - vec![UserInputEntry::Glob("src/**/*.ts".into()), UserInputEntry::Auto { auto: true }]; + let user_inputs = vec![ + UserInputEntry::Glob("src/**/*.ts".into()), + UserInputEntry::Auto(AutoInput { auto: true }), + ]; let config = ResolvedInputConfig::from_user_config(Some(&user_inputs), &pkg, &ws).unwrap(); assert!(config.includes_auto); } @@ -552,10 +555,10 @@ mod tests { #[test] fn test_resolved_input_config_glob_with_workspace_base() { let (pkg, ws) = test_paths(); - let user_inputs = vec![UserInputEntry::GlobWithBase { + let user_inputs = vec![UserInputEntry::GlobWithBase(GlobWithBase { pattern: "configs/tsconfig.json".into(), base: InputBase::Workspace, - }]; + })]; let config = ResolvedInputConfig::from_user_config(Some(&user_inputs), &pkg, &ws).unwrap(); assert!(!config.includes_auto); assert_eq!(config.positive_globs.len(), 1); @@ -570,10 +573,10 @@ mod tests { #[test] fn test_resolved_input_config_negative_glob_with_workspace_base() { let (pkg, ws) = test_paths(); - let user_inputs = vec![UserInputEntry::GlobWithBase { + let user_inputs = vec![UserInputEntry::GlobWithBase(GlobWithBase { pattern: "!dist/**".into(), base: InputBase::Workspace, - }]; + })]; let config = ResolvedInputConfig::from_user_config(Some(&user_inputs), &pkg, &ws).unwrap(); assert_eq!(config.negative_globs.len(), 1); assert!( @@ -587,10 +590,10 @@ mod tests { fn test_resolved_input_config_glob_with_package_base_explicit() { let (pkg, ws) = test_paths(); // Explicit "package" base should behave same as bare string - let user_inputs = vec![UserInputEntry::GlobWithBase { + let user_inputs = vec![UserInputEntry::GlobWithBase(GlobWithBase { pattern: "src/**/*.ts".into(), base: InputBase::Package, - }]; + })]; let config = ResolvedInputConfig::from_user_config(Some(&user_inputs), &pkg, &ws).unwrap(); assert_eq!(config.positive_globs.len(), 1); assert!( @@ -605,12 +608,15 @@ mod tests { let (pkg, ws) = test_paths(); let user_inputs = vec![ UserInputEntry::Glob("src/**".into()), - UserInputEntry::GlobWithBase { + UserInputEntry::GlobWithBase(GlobWithBase { pattern: "configs/**".into(), base: InputBase::Workspace, - }, - UserInputEntry::Auto { auto: true }, - UserInputEntry::GlobWithBase { pattern: "!dist/**".into(), base: InputBase::Workspace }, + }), + UserInputEntry::Auto(AutoInput { auto: true }), + UserInputEntry::GlobWithBase(GlobWithBase { + pattern: "!dist/**".into(), + base: InputBase::Workspace, + }), ]; let config = ResolvedInputConfig::from_user_config(Some(&user_inputs), &pkg, &ws).unwrap(); assert!(config.includes_auto); diff --git a/crates/vite_task_graph/src/config/user.rs b/crates/vite_task_graph/src/config/user.rs index 866a354b..1eaf9fe1 100644 --- a/crates/vite_task_graph/src/config/user.rs +++ b/crates/vite_task_graph/src/config/user.rs @@ -21,6 +21,26 @@ pub enum InputBase { Workspace, } +/// Glob pattern with explicit base directory for resolution. +#[derive(Debug, Deserialize, PartialEq, Eq, Clone)] +#[cfg_attr(all(test, not(clippy)), derive(TS))] +#[serde(deny_unknown_fields)] +pub struct GlobWithBase { + /// The glob pattern (positive or negative starting with `!`) + pub pattern: Str, + /// The base directory for resolving the pattern + pub base: InputBase, +} + +/// Auto-inference directive for input tracking. +#[derive(Debug, Deserialize, PartialEq, Eq, Clone)] +#[cfg_attr(all(test, not(clippy)), derive(TS))] +#[serde(deny_unknown_fields)] +pub struct AutoInput { + /// Automatically track which files the task reads + pub auto: bool, +} + /// A single input entry in the `input` array. /// /// Inputs can be: @@ -35,17 +55,9 @@ pub enum UserInputEntry { /// Glob pattern (positive or negative starting with `!`), resolved relative to package dir Glob(Str), /// Glob pattern with explicit base directory - GlobWithBase { - /// The glob pattern (positive or negative starting with `!`) - pattern: Str, - /// The base directory for resolving the pattern - base: InputBase, - }, + GlobWithBase(GlobWithBase), /// Auto-inference directive - Auto { - /// Automatically track which files the task reads - auto: bool, - }, + Auto(AutoInput), } /// The inputs configuration for cache fingerprinting. @@ -471,7 +483,7 @@ mod tests { "input": [{ "auto": true }] }); let config: EnabledCacheConfig = serde_json::from_value(user_config_json).unwrap(); - assert_eq!(config.input, Some(vec![UserInputEntry::Auto { auto: true }])); + assert_eq!(config.input, Some(vec![UserInputEntry::Auto(AutoInput { auto: true })])); } #[test] @@ -480,7 +492,7 @@ mod tests { "input": [{ "auto": false }] }); let config: EnabledCacheConfig = serde_json::from_value(user_config_json).unwrap(); - assert_eq!(config.input, Some(vec![UserInputEntry::Auto { auto: false }])); + assert_eq!(config.input, Some(vec![UserInputEntry::Auto(AutoInput { auto: false })])); } #[test] @@ -523,7 +535,7 @@ mod tests { config.input, Some(vec![ UserInputEntry::Glob("package.json".into()), - UserInputEntry::Auto { auto: true }, + UserInputEntry::Auto(AutoInput { auto: true }), UserInputEntry::Glob("!node_modules/**".into()), ]) ); @@ -537,10 +549,10 @@ mod tests { let config: EnabledCacheConfig = serde_json::from_value(user_config_json).unwrap(); assert_eq!( config.input, - Some(vec![UserInputEntry::GlobWithBase { + Some(vec![UserInputEntry::GlobWithBase(GlobWithBase { pattern: "configs/tsconfig.json".into(), base: InputBase::Workspace, - }]) + })]) ); } @@ -552,10 +564,10 @@ mod tests { let config: EnabledCacheConfig = serde_json::from_value(user_config_json).unwrap(); assert_eq!( config.input, - Some(vec![UserInputEntry::GlobWithBase { + Some(vec![UserInputEntry::GlobWithBase(GlobWithBase { pattern: "src/**".into(), base: InputBase::Package, - }]) + })]) ); } @@ -567,10 +579,10 @@ mod tests { let config: EnabledCacheConfig = serde_json::from_value(user_config_json).unwrap(); assert_eq!( config.input, - Some(vec![UserInputEntry::GlobWithBase { + Some(vec![UserInputEntry::GlobWithBase(GlobWithBase { pattern: "!dist/**".into(), base: InputBase::Workspace, - }]) + })]) ); } @@ -593,6 +605,16 @@ mod tests { assert!(result.is_err(), "invalid 'base' value should produce an error"); } + #[test] + fn test_input_mixed_auto_and_glob_with_base_error() { + // An object with both "auto" and "pattern"/"base" should fail due to deny_unknown_fields + let user_config_json = json!({ + "input": [{ "auto": true, "pattern": "src/**", "base": "workspace" }] + }); + let result = serde_json::from_value::(user_config_json); + assert!(result.is_err(), "mixing auto and pattern/base fields should produce an error"); + } + #[test] fn test_input_mixed_with_glob_base() { let user_config_json = json!({ @@ -608,11 +630,11 @@ mod tests { config.input, Some(vec![ UserInputEntry::Glob("package.json".into()), - UserInputEntry::GlobWithBase { + UserInputEntry::GlobWithBase(GlobWithBase { pattern: "configs/**".into(), base: InputBase::Workspace, - }, - UserInputEntry::Auto { auto: true }, + }), + UserInputEntry::Auto(AutoInput { auto: true }), UserInputEntry::Glob("!node_modules/**".into()), ]) ); diff --git a/docs/inputs.md b/docs/inputs.md index 1f97e39a..1d5f772b 100644 --- a/docs/inputs.md +++ b/docs/inputs.md @@ -68,7 +68,7 @@ By default, glob patterns are resolved relative to the package directory. Use th ```json { - "inputs": [ + "input": [ "src/**", { "pattern": "configs/tsconfig.json", "base": "workspace" }, { "pattern": "!dist/**", "base": "workspace" } @@ -159,15 +159,15 @@ The cache will only invalidate when the command itself or environment variables ## Behavior Summary -| Configuration | Auto-Inference | File Tracking | -| --------------------------------------------------------------- | -------------- | ---------------------------------- | -| `inputs` omitted | Enabled | Inferred files | -| `inputs: [{ "auto": true }]` | Enabled | Inferred files | -| `inputs: ["src/**"]` | Disabled | Matched files only | -| `inputs: [{ "auto": true }, "!dist/**"]` | Enabled | Inferred files except `dist/` | -| `inputs: ["pkg.json", { "auto": true }]` | Enabled | `pkg.json` + inferred files | -| `inputs: [{ "pattern": "tsconfig.json", "base": "workspace" }]` | Disabled | Matched files (workspace-relative) | -| `inputs: []` | Disabled | No files tracked | +| Configuration | Auto-Inference | File Tracking | +| -------------------------------------------------------------- | -------------- | ---------------------------------- | +| `inputs` omitted | Enabled | Inferred files | +| `inputs: [{ "auto": true }]` | Enabled | Inferred files | +| `inputs: ["src/**"]` | Disabled | Matched files only | +| `inputs: [{ "auto": true }, "!dist/**"]` | Enabled | Inferred files except `dist/` | +| `inputs: ["pkg.json", { "auto": true }]` | Enabled | `pkg.json` + inferred files | +| `input: [{ "pattern": "tsconfig.json", "base": "workspace" }]` | Disabled | Matched files (workspace-relative) | +| `inputs: []` | Disabled | No files tracked | ## Important Notes