Skip to content

Commit 636351d

Browse files
jong-kyungwan9chiclaude
authored
feat(config): support task command shorthands (#391)
## Summary Closes #381. Adds task command shorthands: - `"build": "cmd"` - `"build": ["cmd1", "cmd2", ...]` - `{ "command": ["cmd1", "cmd2", ...] }` Arrays reuse the existing `&&` planning path, so cache behavior, `dependsOn`, and task options stay consistent. Empty arrays and empty/whitespace-only entries are rejected. --------- Co-authored-by: branchseer <dk4rest@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 942978f commit 636351d

91 files changed

Lines changed: 2746 additions & 425 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Changelog
22

3+
- **Added** task command shorthands for defining tasks as command strings or command string arrays ([#391](https://github.com/voidzero-dev/vite-task/pull/391))
34
- **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))
45
- **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))
56
- **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))

crates/vite_task_graph/run-config.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ export type AutoInput = {
66
*/
77
auto: boolean, };
88

9+
export type Command = string | Array<string>;
10+
911
export type GlobWithBase = {
1012
/**
1113
* The glob pattern (positive or negative starting with `!`)
@@ -20,9 +22,9 @@ export type InputBase = "package" | "workspace";
2022

2123
export type Task = {
2224
/**
23-
* The command to run for the task.
25+
* Command to run, or an array of commands to run in order.
2426
*/
25-
command: string,
27+
command: Command,
2628
/**
2729
* The working directory for the task, relative to the package root (not workspace root).
2830
*/
@@ -68,6 +70,8 @@ output?: Array<string | GlobWithBase>, } | {
6870
*/
6971
cache: false, });
7072

73+
export type TaskDefinition = Task | Command;
74+
7175
export type UserGlobalCacheConfig = boolean | {
7276
/**
7377
* Enable caching for package.json scripts not defined in the `tasks` map.
@@ -98,9 +102,9 @@ export type RunConfig = {
98102
*/
99103
cache?: UserGlobalCacheConfig,
100104
/**
101-
* Task definitions
105+
* Task definitions: full task objects, command strings, or command string arrays.
102106
*/
103-
tasks?: { [key in string]: Task },
107+
tasks?: { [key in string]: TaskDefinition },
104108
/**
105109
* Whether to automatically run `preX`/`postX` package.json scripts as
106110
* lifecycle hooks when script `X` is executed.

crates/vite_task_graph/src/config/mod.rs

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ use monostate::MustBe;
66
use rustc_hash::FxHashSet;
77
use serde::Serialize;
88
pub use user::{
9-
AutoInput, EnabledCacheConfig, GlobWithBase, InputBase, ResolvedGlobalCacheConfig,
9+
AutoInput, Command, EnabledCacheConfig, GlobWithBase, InputBase, ResolvedGlobalCacheConfig,
1010
UserCacheConfig, UserGlobalCacheConfig, UserInputEntry, UserInputsConfig, UserOutputEntry,
11-
UserRunConfig, UserTaskConfig,
11+
UserRunConfig, UserTaskConfig, UserTaskDefinition,
1212
};
1313
use vite_path::AbsolutePath;
1414
use vite_str::Str;
@@ -28,10 +28,10 @@ use crate::config::user::UserTaskOptions;
2828
/// `depends_on` is not included here because it's represented by the edges of the task graph.
2929
#[derive(Debug, Serialize)]
3030
pub struct ResolvedTaskConfig {
31-
/// The command to run for this task, as a raw string.
31+
/// The command or commands to run for this task.
3232
///
33-
/// The command may contain environment variables that need to be expanded later.
34-
pub command: Str,
33+
/// Commands may contain environment variables that need to be expanded later.
34+
pub commands: Arc<[Str]>,
3535

3636
pub resolved_options: ResolvedTaskOptions,
3737
}
@@ -360,7 +360,7 @@ impl ResolvedTaskConfig {
360360
workspace_root: &AbsolutePath,
361361
) -> Result<Self, ResolveTaskConfigError> {
362362
Ok(Self {
363-
command: package_json_script.into(),
363+
commands: vec![package_json_script.into()].into(),
364364
resolved_options: ResolvedTaskOptions::resolve(
365365
UserTaskOptions::default(),
366366
package_dir,
@@ -379,13 +379,14 @@ impl ResolvedTaskConfig {
379379
package_dir: &Arc<AbsolutePath>,
380380
workspace_root: &AbsolutePath,
381381
) -> Result<Self, ResolveTaskConfigError> {
382+
let UserTaskConfig { command, options } = user_config;
383+
let commands = match command {
384+
Command::Single(command) => Arc::from([command]),
385+
Command::Array(commands) => commands,
386+
};
382387
Ok(Self {
383-
command: Str::from(user_config.command.as_ref()),
384-
resolved_options: ResolvedTaskOptions::resolve(
385-
user_config.options,
386-
package_dir,
387-
workspace_root,
388-
)?,
388+
commands,
389+
resolved_options: ResolvedTaskOptions::resolve(options, package_dir, workspace_root)?,
389390
})
390391
}
391392
}

crates/vite_task_graph/src/config/user.rs

Lines changed: 116 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -192,21 +192,44 @@ impl Default for UserTaskOptions {
192192
}
193193
}
194194
}
195+
/// The command to run for a task: a single string or a sequence of strings.
196+
#[derive(Debug, Deserialize, PartialEq, Eq)]
197+
// TS derive macro generates code using std types that clippy disallows; skip derive during linting
198+
#[cfg_attr(all(test, not(clippy)), derive(TS))]
199+
#[serde(untagged)]
200+
pub enum Command {
201+
/// A single command string.
202+
Single(Str),
203+
/// A sequence of command strings, run in order.
204+
Array(Arc<[Str]>),
205+
}
195206

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

205-
/// Fields other than the command
216+
/// Fields other than the command.
206217
#[serde(flatten)]
207218
pub options: UserTaskOptions,
208219
}
209220

221+
/// User-defined task configuration or command-only shorthand in `vite.config.*`.
222+
#[derive(Debug, Deserialize, PartialEq, Eq)]
223+
// TS derive macro generates code using std types that clippy disallows; skip derive during linting
224+
#[cfg_attr(all(test, not(clippy)), derive(TS), ts(rename = "TaskDefinition"))]
225+
#[serde(untagged)]
226+
pub enum UserTaskDefinition {
227+
/// Full task object form.
228+
Object(UserTaskConfig),
229+
/// Command-only shorthand form using default task options.
230+
CommandShorthand(Command),
231+
}
232+
210233
/// Root-level cache configuration.
211234
///
212235
/// Controls caching behavior for the entire workspace.
@@ -281,8 +304,8 @@ pub struct UserRunConfig {
281304
/// Setting it in a package's config will result in an error.
282305
pub cache: Option<UserGlobalCacheConfig>,
283306

284-
/// Task definitions
285-
pub tasks: Option<FxHashMap<Str, UserTaskConfig>>,
307+
/// Task definitions: full task objects, command strings, or command string arrays.
308+
pub tasks: Option<FxHashMap<Str, UserTaskDefinition>>,
286309

287310
/// Whether to automatically run `preX`/`postX` package.json scripts as
288311
/// lifecycle hooks when script `X` is executed.
@@ -413,10 +436,97 @@ mod tests {
413436
let user_config: UserTaskConfig = serde_json::from_value(user_config_json).unwrap();
414437
assert_eq!(
415438
user_config,
416-
UserTaskConfig { command: "echo hello".into(), options: UserTaskOptions::default() }
439+
UserTaskConfig {
440+
command: Command::Single("echo hello".into()),
441+
options: UserTaskOptions::default()
442+
}
443+
);
444+
}
445+
446+
#[test]
447+
fn test_command_array() {
448+
let user_config_json = json!({
449+
"command": ["echo one", "echo two", "echo three"]
450+
});
451+
let user_config: UserTaskConfig = serde_json::from_value(user_config_json).unwrap();
452+
assert_eq!(
453+
user_config.command,
454+
Command::Array(Arc::from(["echo one".into(), "echo two".into(), "echo three".into()]))
455+
);
456+
assert_eq!(user_config.options, UserTaskOptions::default());
457+
}
458+
459+
#[test]
460+
fn test_task_string_shorthand() {
461+
let user_config_json = json!({
462+
"tasks": {
463+
"build": "echo build"
464+
}
465+
});
466+
let mut user_config: UserRunConfig = serde_json::from_value(user_config_json).unwrap();
467+
let task = user_config.tasks.as_mut().unwrap().remove("build").unwrap();
468+
assert_eq!(
469+
task,
470+
UserTaskDefinition::CommandShorthand(Command::Single("echo build".into()))
417471
);
418472
}
419473

474+
#[test]
475+
fn test_task_array_shorthand() {
476+
let user_config_json = json!({
477+
"tasks": {
478+
"build": ["echo one", "echo two", "echo three"]
479+
}
480+
});
481+
let mut user_config: UserRunConfig = serde_json::from_value(user_config_json).unwrap();
482+
let task = user_config.tasks.as_mut().unwrap().remove("build").unwrap();
483+
assert_eq!(
484+
task,
485+
UserTaskDefinition::CommandShorthand(Command::Array(Arc::from([
486+
"echo one".into(),
487+
"echo two".into(),
488+
"echo three".into()
489+
])))
490+
);
491+
}
492+
493+
#[test]
494+
fn test_command_array_with_options() {
495+
let user_config_json = json!({
496+
"command": ["echo one", "echo two"],
497+
"cwd": "src",
498+
"dependsOn": ["build"],
499+
"cache": false
500+
});
501+
let user_config: UserTaskConfig = serde_json::from_value(user_config_json).unwrap();
502+
assert_eq!(
503+
user_config.command,
504+
Command::Array(Arc::from(["echo one".into(), "echo two".into()]))
505+
);
506+
let options = user_config.options;
507+
assert_eq!(options.cwd_relative_to_package.as_ref().unwrap().as_str(), "src");
508+
assert_eq!(options.depends_on.as_ref().unwrap().as_ref(), [Str::from("build")]);
509+
assert_eq!(options.cache_config, UserCacheConfig::Disabled { cache: MustBe!(false) });
510+
}
511+
512+
#[test]
513+
fn test_task_invalid_shorthand_error() {
514+
let user_config_json = json!({
515+
"tasks": {
516+
"build": 123
517+
}
518+
});
519+
assert!(serde_json::from_value::<UserRunConfig>(user_config_json).is_err());
520+
}
521+
522+
#[test]
523+
fn test_command_array_invalid_item_error() {
524+
let user_config_json = json!({
525+
"command": ["echo one", 123]
526+
});
527+
assert!(serde_json::from_value::<UserTaskConfig>(user_config_json).is_err());
528+
}
529+
420530
#[test]
421531
fn test_cwd_rename() {
422532
let user_config_json = json!({

crates/vite_task_graph/src/display.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,26 @@ impl IndexedTaskGraph {
5050
let node = &self.task_graph()[idx];
5151
TaskListEntry {
5252
task_display: node.task_display.clone(),
53-
command: node.resolved_config.command.clone(),
53+
command: format_command_for_task_list(&node.resolved_config.commands),
5454
}
5555
})
5656
.collect()
5757
}
5858
}
59+
60+
// Display-only formatting for task list/selector descriptions. Execution planning keeps
61+
// command arrays structured and must not depend on this joined string.
62+
fn format_command_for_task_list(commands: &Arc<[Str]>) -> Str {
63+
if commands.len() == 1 {
64+
commands[0].clone()
65+
} else {
66+
let mut display = Str::default();
67+
for (index, command) in commands.iter().enumerate() {
68+
if index > 0 {
69+
display.push_str(" && ");
70+
}
71+
display.push_str(command.as_str());
72+
}
73+
display
74+
}
75+
}

crates/vite_task_graph/src/lib.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ mod specifier;
66

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

9-
use config::{ResolvedGlobalCacheConfig, ResolvedTaskConfig, UserRunConfig};
9+
use config::{
10+
ResolvedGlobalCacheConfig, ResolvedTaskConfig, UserRunConfig, UserTaskConfig,
11+
UserTaskDefinition,
12+
};
1013
use petgraph::graph::{DefaultIx, DiGraph, EdgeIndex, IndexType, NodeIndex};
1114
use rustc_hash::{FxBuildHasher, FxHashMap};
1215
use serde::Serialize;
@@ -15,7 +18,7 @@ use vite_path::AbsolutePath;
1518
use vite_str::Str;
1619
use vite_workspace::{PackageNodeIndex, WorkspaceRoot, package_graph::IndexedPackageGraph};
1720

18-
use crate::display::TaskDisplay;
21+
use crate::{config::user::UserTaskOptions, display::TaskDisplay};
1922

2023
/// The type of a task dependency edge in the task graph.
2124
///
@@ -303,6 +306,12 @@ impl IndexedTaskGraph {
303306

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

309+
let task_user_config = match task_user_config {
310+
UserTaskDefinition::Object(config) => config,
311+
UserTaskDefinition::CommandShorthand(command) => {
312+
UserTaskConfig { command, options: UserTaskOptions::default() }
313+
}
314+
};
306315
let dependency_specifiers = task_user_config.options.depends_on.clone();
307316

308317
// Resolve the task configuration from the user config

crates/vite_task_plan/src/cache_metadata.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ pub enum ExecutionCacheKey {
1515
UserTask {
1616
/// The name of the user-defined task.
1717
task_name: Str,
18-
/// The index of the execution item in the task's command split by `&&`.
18+
/// The index of the command item in the task's command array.
19+
command_item_index: usize,
20+
/// The index of the execution item within the command item split by `&&`.
1921
/// This is to distinguish multiple execution items from the same task.
2022
and_item_index: usize,
2123
/// Extra args provided when invoking the user-defined task (`vp [task_name] [extra_args...]`).

0 commit comments

Comments
 (0)