Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f0bdc49
feat(config): support task command shorthands
jong-kyung May 19, 2026
d4c8723
fix(config): reject empty command arrays
jong-kyung May 19, 2026
d13df64
fix(plan): parse task command arrays in planner
jong-kyung May 19, 2026
32ded09
docs(changelog): add task command shorthand entry
jong-kyung May 19, 2026
a73e602
fix(plan): run post-cd commands from current cwd
jong-kyung May 19, 2026
fc17f50
fix(plan): reject shell cd before array continuations
jong-kyung May 19, 2026
455b93b
fix(plan): reject compound shell cd before array continuations
jong-kyung May 19, 2026
0958327
refactor(shell): model cwd-changing commands
jong-kyung May 19, 2026
c900e31
Merge branch 'main' into feat/task-command-shorthands
jong-kyung May 20, 2026
a551135
fix(plan): normalize command arrays through existing shell path
jong-kyung May 21, 2026
2f99de1
test(plan): remove redundant array cache snapshot
jong-kyung May 21, 2026
57986aa
Merge remote-tracking branch 'origin/main' into feat/task-command-sho…
jong-kyung May 21, 2026
3d2e41a
fix(plan): preserve task command array boundaries
jong-kyung May 21, 2026
849bb5c
fix(plan): reject empty task command entries
jong-kyung May 21, 2026
9f42898
refactor(config): flatten UserTaskConfig variants behind a Command enum
branchseer May 24, 2026
5da6475
refactor(config): collapse UserTaskDefinition shorthands into a Command
branchseer May 24, 2026
9f4be7b
refactor(plan): inline command planning, drop PlannedCommand
branchseer May 24, 2026
7e96744
Merge remote-tracking branch 'origin/main' into feat/task-command-sho…
branchseer May 24, 2026
19027aa
test(plan): consolidate array cd snapshots into a single fixture
branchseer May 24, 2026
92cbf78
revert: restore crates/vite_task/docs to main
branchseer May 24, 2026
39ede39
test(plan): update windows-only task_graph snapshot to commands array
branchseer May 24, 2026
e7d293b
test(plan): add command_item_index to windows fixture query snapshots
branchseer May 24, 2026
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** task command shorthands for defining tasks as command strings or command string arrays ([#391](https://github.com/voidzero-dev/vite-task/pull/391))
- **Changed** Cached logs are stored with colors intact (`FORCE_COLOR=1` is auto-injected into spawned tasks). Colors are then stripped at display time when the terminal does not support them. Other color-related env vars (`NO_COLOR`, `COLORTERM`, `TERM`, `TERM_PROGRAM`) are no longer passed through by default. Opt in via a task's `env`/`untrackedEnv` ([#378](https://github.com/voidzero-dev/vite-task/pull/378))
- **Added** `output` field for cached tasks: archives matching files after a successful run and restores them on cache hit ([#375](https://github.com/voidzero-dev/vite-task/pull/375))
- **Fixed** Windows cached tasks can now run package shims rewritten through PowerShell; default env passthrough now preserves `PATHEXT` ([#366](https://github.com/voidzero-dev/vite-task/pull/366))
Expand Down
12 changes: 8 additions & 4 deletions crates/vite_task_graph/run-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export type AutoInput = {
*/
auto: boolean, };

export type Command = string | Array<string>;

export type GlobWithBase = {
/**
* The glob pattern (positive or negative starting with `!`)
Expand All @@ -20,9 +22,9 @@ export type InputBase = "package" | "workspace";

export type Task = {
/**
* The command to run for the task.
* Command to run, or an array of commands to run in order.
*/
command: string,
command: Command,
/**
* The working directory for the task, relative to the package root (not workspace root).
*/
Expand Down Expand Up @@ -68,6 +70,8 @@ output?: Array<string | GlobWithBase>, } | {
*/
cache: false, });

export type TaskDefinition = Task | Command;

export type UserGlobalCacheConfig = boolean | {
/**
* Enable caching for package.json scripts not defined in the `tasks` map.
Expand Down Expand Up @@ -98,9 +102,9 @@ export type RunConfig = {
*/
cache?: UserGlobalCacheConfig,
/**
* Task definitions
* Task definitions: full task objects, command strings, or command string arrays.
*/
tasks?: { [key in string]: Task },
tasks?: { [key in string]: TaskDefinition },
/**
* Whether to automatically run `preX`/`postX` package.json scripts as
* lifecycle hooks when script `X` is executed.
Expand Down
25 changes: 13 additions & 12 deletions crates/vite_task_graph/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ use monostate::MustBe;
use rustc_hash::FxHashSet;
use serde::Serialize;
pub use user::{
AutoInput, EnabledCacheConfig, GlobWithBase, InputBase, ResolvedGlobalCacheConfig,
AutoInput, Command, EnabledCacheConfig, GlobWithBase, InputBase, ResolvedGlobalCacheConfig,
UserCacheConfig, UserGlobalCacheConfig, UserInputEntry, UserInputsConfig, UserOutputEntry,
UserRunConfig, UserTaskConfig,
UserRunConfig, UserTaskConfig, UserTaskDefinition,
};
use vite_path::AbsolutePath;
use vite_str::Str;
Expand All @@ -28,10 +28,10 @@ use crate::config::user::UserTaskOptions;
/// `depends_on` is not included here because it's represented by the edges of the task graph.
#[derive(Debug, Serialize)]
pub struct ResolvedTaskConfig {
/// The command to run for this task, as a raw string.
/// The command or commands to run for this task.
///
/// The command may contain environment variables that need to be expanded later.
pub command: Str,
/// Commands may contain environment variables that need to be expanded later.
pub commands: Arc<[Str]>,

pub resolved_options: ResolvedTaskOptions,
}
Expand Down Expand Up @@ -360,7 +360,7 @@ impl ResolvedTaskConfig {
workspace_root: &AbsolutePath,
) -> Result<Self, ResolveTaskConfigError> {
Ok(Self {
command: package_json_script.into(),
commands: vec![package_json_script.into()].into(),
resolved_options: ResolvedTaskOptions::resolve(
UserTaskOptions::default(),
package_dir,
Expand All @@ -379,13 +379,14 @@ impl ResolvedTaskConfig {
package_dir: &Arc<AbsolutePath>,
workspace_root: &AbsolutePath,
) -> Result<Self, ResolveTaskConfigError> {
let UserTaskConfig { command, options } = user_config;
let commands = match command {
Command::Single(command) => Arc::from([command]),
Command::Array(commands) => commands,
};
Ok(Self {
command: Str::from(user_config.command.as_ref()),
resolved_options: ResolvedTaskOptions::resolve(
user_config.options,
package_dir,
workspace_root,
)?,
commands,
resolved_options: ResolvedTaskOptions::resolve(options, package_dir, workspace_root)?,
})
}
}
Expand Down
122 changes: 116 additions & 6 deletions crates/vite_task_graph/src/config/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,21 +192,44 @@ impl Default for UserTaskOptions {
}
}
}
/// The command to run for a task: a single string or a sequence of strings.
#[derive(Debug, Deserialize, PartialEq, Eq)]
// 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 Command {
/// A single command string.
Single(Str),
/// A sequence of command strings, run in order.
Array(Arc<[Str]>),
}

/// Full user-defined task configuration in `vite.config.*`, including the command and options.
#[derive(Debug, Deserialize, PartialEq, Eq)]
// TS derive macro generates code using std types that clippy disallows; skip derive during linting
#[cfg_attr(all(test, not(clippy)), derive(TS), ts(optional_fields, rename = "Task"))]
#[serde(rename_all = "camelCase")]
pub struct UserTaskConfig {
/// The command to run for the task.
pub command: Box<str>,
/// Command to run, or an array of commands to run in order.
pub command: Command,

/// Fields other than the command
/// Fields other than the command.
#[serde(flatten)]
pub options: UserTaskOptions,
}

/// User-defined task configuration or command-only shorthand in `vite.config.*`.
#[derive(Debug, Deserialize, PartialEq, Eq)]
// TS derive macro generates code using std types that clippy disallows; skip derive during linting
#[cfg_attr(all(test, not(clippy)), derive(TS), ts(rename = "TaskDefinition"))]
#[serde(untagged)]
pub enum UserTaskDefinition {
/// Full task object form.
Object(UserTaskConfig),
/// Command-only shorthand form using default task options.
CommandShorthand(Command),
}

/// Root-level cache configuration.
///
/// Controls caching behavior for the entire workspace.
Expand Down Expand Up @@ -281,8 +304,8 @@ pub struct UserRunConfig {
/// Setting it in a package's config will result in an error.
pub cache: Option<UserGlobalCacheConfig>,

/// Task definitions
pub tasks: Option<FxHashMap<Str, UserTaskConfig>>,
/// Task definitions: full task objects, command strings, or command string arrays.
pub tasks: Option<FxHashMap<Str, UserTaskDefinition>>,

/// Whether to automatically run `preX`/`postX` package.json scripts as
/// lifecycle hooks when script `X` is executed.
Expand Down Expand Up @@ -413,10 +436,97 @@ mod tests {
let user_config: UserTaskConfig = serde_json::from_value(user_config_json).unwrap();
assert_eq!(
user_config,
UserTaskConfig { command: "echo hello".into(), options: UserTaskOptions::default() }
UserTaskConfig {
command: Command::Single("echo hello".into()),
options: UserTaskOptions::default()
}
);
}

#[test]
fn test_command_array() {
let user_config_json = json!({
"command": ["echo one", "echo two", "echo three"]
});
let user_config: UserTaskConfig = serde_json::from_value(user_config_json).unwrap();
assert_eq!(
user_config.command,
Command::Array(Arc::from(["echo one".into(), "echo two".into(), "echo three".into()]))
);
assert_eq!(user_config.options, UserTaskOptions::default());
}

#[test]
fn test_task_string_shorthand() {
let user_config_json = json!({
"tasks": {
"build": "echo build"
}
});
let mut user_config: UserRunConfig = serde_json::from_value(user_config_json).unwrap();
let task = user_config.tasks.as_mut().unwrap().remove("build").unwrap();
assert_eq!(
task,
UserTaskDefinition::CommandShorthand(Command::Single("echo build".into()))
);
}

#[test]
fn test_task_array_shorthand() {
let user_config_json = json!({
"tasks": {
"build": ["echo one", "echo two", "echo three"]
}
});
let mut user_config: UserRunConfig = serde_json::from_value(user_config_json).unwrap();
let task = user_config.tasks.as_mut().unwrap().remove("build").unwrap();
assert_eq!(
task,
UserTaskDefinition::CommandShorthand(Command::Array(Arc::from([
"echo one".into(),
"echo two".into(),
"echo three".into()
])))
);
}

#[test]
fn test_command_array_with_options() {
let user_config_json = json!({
"command": ["echo one", "echo two"],
"cwd": "src",
"dependsOn": ["build"],
"cache": false
});
let user_config: UserTaskConfig = serde_json::from_value(user_config_json).unwrap();
assert_eq!(
user_config.command,
Command::Array(Arc::from(["echo one".into(), "echo two".into()]))
);
let options = user_config.options;
assert_eq!(options.cwd_relative_to_package.as_ref().unwrap().as_str(), "src");
assert_eq!(options.depends_on.as_ref().unwrap().as_ref(), [Str::from("build")]);
assert_eq!(options.cache_config, UserCacheConfig::Disabled { cache: MustBe!(false) });
}

#[test]
fn test_task_invalid_shorthand_error() {
let user_config_json = json!({
"tasks": {
"build": 123
}
});
assert!(serde_json::from_value::<UserRunConfig>(user_config_json).is_err());
}

#[test]
fn test_command_array_invalid_item_error() {
let user_config_json = json!({
"command": ["echo one", 123]
});
assert!(serde_json::from_value::<UserTaskConfig>(user_config_json).is_err());
}

#[test]
fn test_cwd_rename() {
let user_config_json = json!({
Expand Down
19 changes: 18 additions & 1 deletion crates/vite_task_graph/src/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,26 @@ impl IndexedTaskGraph {
let node = &self.task_graph()[idx];
TaskListEntry {
task_display: node.task_display.clone(),
command: node.resolved_config.command.clone(),
command: format_command_for_task_list(&node.resolved_config.commands),
}
})
.collect()
}
}

// Display-only formatting for task list/selector descriptions. Execution planning keeps
// command arrays structured and must not depend on this joined string.
fn format_command_for_task_list(commands: &Arc<[Str]>) -> Str {
if commands.len() == 1 {
commands[0].clone()
} else {
let mut display = Str::default();
for (index, command) in commands.iter().enumerate() {
if index > 0 {
display.push_str(" && ");
}
display.push_str(command.as_str());
}
display
}
}
13 changes: 11 additions & 2 deletions crates/vite_task_graph/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ mod specifier;

use std::{convert::Infallible, sync::Arc};

use config::{ResolvedGlobalCacheConfig, ResolvedTaskConfig, UserRunConfig};
use config::{
ResolvedGlobalCacheConfig, ResolvedTaskConfig, UserRunConfig, UserTaskConfig,
UserTaskDefinition,
};
use petgraph::graph::{DefaultIx, DiGraph, EdgeIndex, IndexType, NodeIndex};
use rustc_hash::{FxBuildHasher, FxHashMap};
use serde::Serialize;
Expand All @@ -15,7 +18,7 @@ use vite_path::AbsolutePath;
use vite_str::Str;
use vite_workspace::{PackageNodeIndex, WorkspaceRoot, package_graph::IndexedPackageGraph};

use crate::display::TaskDisplay;
use crate::{config::user::UserTaskOptions, display::TaskDisplay};

/// The type of a task dependency edge in the task graph.
///
Expand Down Expand Up @@ -303,6 +306,12 @@ impl IndexedTaskGraph {

let task_id = TaskId { task_name: task_name.clone(), package_index };

let task_user_config = match task_user_config {
UserTaskDefinition::Object(config) => config,
UserTaskDefinition::CommandShorthand(command) => {
UserTaskConfig { command, options: UserTaskOptions::default() }
}
};
let dependency_specifiers = task_user_config.options.depends_on.clone();

// Resolve the task configuration from the user config
Expand Down
4 changes: 3 additions & 1 deletion crates/vite_task_plan/src/cache_metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ pub enum ExecutionCacheKey {
UserTask {
/// The name of the user-defined task.
task_name: Str,
/// The index of the execution item in the task's command split by `&&`.
/// The index of the command item in the task's command array.
command_item_index: usize,
/// The index of the execution item within the command item split by `&&`.
/// This is to distinguish multiple execution items from the same task.
and_item_index: usize,
/// Extra args provided when invoking the user-defined task (`vp [task_name] [extra_args...]`).
Expand Down
Loading
Loading