Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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))
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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" }]
}
}
}
Expand Down
35 changes: 23 additions & 12 deletions crates/vite_task_graph/run-config.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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<string | GlobWithBase | AutoInput>;
}
| {
/**
Expand Down
150 changes: 126 additions & 24 deletions crates/vite_task_graph/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Str>,
negative_globs: &mut BTreeSet<Str>,
) -> 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.
Expand Down Expand Up @@ -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());
Expand All @@ -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());
Expand Down Expand Up @@ -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();
Expand All @@ -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);
}
Expand All @@ -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/**"));
}
}
Loading
Loading