diff --git a/CHANGELOG.md b/CHANGELOG.md index c2db2cde..90fa2668 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 ([#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)) 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..69e4a661 100644 --- a/crates/vite_task_graph/run-config.ts +++ b/crates/vite_task_graph/run-config.ts @@ -1,5 +1,25 @@ // 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 = { /** * The command to run for the task. @@ -32,21 +52,12 @@ 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 - | { - /** - * 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 c2b5a23e..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, 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,30 +154,54 @@ 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) => { - 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(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. @@ -428,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()); @@ -438,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()); @@ -482,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(); @@ -497,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); } @@ -524,4 +551,79 @@ 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(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(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(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(GlobWithBase { + pattern: "configs/**".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); + 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..1eaf9fe1 100644 --- a/crates/vite_task_graph/src/config/user.rs +++ b/crates/vite_task_graph/src/config/user.rs @@ -10,21 +10,54 @@ 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, +} + +/// 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 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(GlobWithBase), /// Auto-inference directive - Auto { - /// Automatically track which files the task reads - auto: bool, - }, + Auto(AutoInput), } /// The inputs configuration for cache fingerprinting. @@ -85,11 +118,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, @@ -451,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] @@ -460,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] @@ -503,7 +535,106 @@ 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()), + ]) + ); + } + + #[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(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(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(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_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!({ + "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(GlobWithBase { + pattern: "configs/**".into(), + base: InputBase::Workspace, + }), + UserInputEntry::Auto(AutoInput { auto: true }), UserInputEntry::Glob("!node_modules/**".into()), ]) ); 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..7636edf1 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/input-workspace-base/package.json @@ -0,0 +1,3 @@ +{ + "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/packages/app/vite-task.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/input-workspace-base/packages/app/vite-task.json new file mode 100644 index 00000000..4808e319 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/input-workspace-base/packages/app/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/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.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..3d2cb5bf --- /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": [ + "/packages/app", + "build" + ], + "node": { + "task_display": { + "package_name": "app", + "task_name": "build", + "package_path": "/packages/app" + }, + "resolved_config": { + "command": "echo build", + "resolved_options": { + "cwd": "/packages/app", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": false, + "positive_globs": [ + "configs/tsconfig.json", + "packages/app/src/**" + ], + "negative_globs": [ + "dist/**" + ] + } + } + } + }, + "source": "TaskConfig" + }, + "neighbors": [] + } +] diff --git a/docs/inputs.md b/docs/inputs.md index ddd10772..1d5f772b 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 +{ + "input": [ + "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 | +| `input: [{ "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 {