Skip to content
Open
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** 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
6 changes: 3 additions & 3 deletions crates/vite_task/docs/task-cache.md
Original file line number Diff line number Diff line change
Expand Up @@ -550,13 +550,13 @@ Ensure commands produce identical outputs for identical inputs:

```json
{
"scripts": {
"build": "tsc && rollup -c && terser dist/bundle.js"
"tasks": {
"build": ["tsc", "rollup -c", "terser dist/bundle.js"]
}
}
```

Each `&&` separated command is cached independently. If only terser config changes, TypeScript and rollup will hit cache.
Each `&&` separated command is cached independently. Task command arrays use the same granular caching semantics. If only terser config changes, TypeScript and rollup will hit cache.

## Implementation Reference

Expand Down
17 changes: 11 additions & 6 deletions crates/vite_task/docs/terminologies.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,33 @@
{
"name": "app",
"scripts": {
"build": "echo build1 && echo build2",
},
"build": "echo build1 && echo build2"
}
}
```

```jsonc
// vite-task.json
{
"tasks": {
"lint": {
"command": "echo lint"
"lint": "echo lint",
"check": ["eslint .", "tsc --noEmit", "prettier --check ."]
}
}
```

In the example above, `build` and `lint` are **task group names**. A task group may define one task, or multiple tasks separated by `&&`.
In the example above, `build`, `lint`, and `check` are **task group names**. A task group may define one task, or multiple tasks separated by `&&`.

In `tasks`, command-only task groups can be written as a string or as an array. Object form with `command` and options is also supported.

The two task groups generates 3 tasks:
The three task groups generate these tasks:

- `app#build(subcommand 0)` (runs `echo build1`)
- `app#build` (runs `echo build2`)
- `app#lint` (runs `echo lint`)
- `app#check(subcommand 0)` (runs `eslint .`)
- `app#check(subcommand 1)` (runs `tsc --noEmit`)
- `app#check` (runs `prettier --check .`)

These are **task names**. They are for displaying and filtering.

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 @@ -20,9 +20,9 @@ export type InputBase = "package" | "workspace";

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

export type TaskCommand = string | Array<string>;

export type TaskDefinition = Task | TaskCommand;

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
14 changes: 7 additions & 7 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, EnabledCacheConfig, GlobWithBase, InputBase, ResolvedGlobalCacheConfig, TaskCommand,
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 command: TaskCommand,

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(),
command: TaskCommand::String(package_json_script.into()),
resolved_options: ResolvedTaskOptions::resolve(
UserTaskOptions::default(),
package_dir,
Expand All @@ -380,7 +380,7 @@ impl ResolvedTaskConfig {
workspace_root: &AbsolutePath,
) -> Result<Self, ResolveTaskConfigError> {
Ok(Self {
command: Str::from(user_config.command.as_ref()),
command: user_config.command,
resolved_options: ResolvedTaskOptions::resolve(
user_config.options,
package_dir,
Expand Down
129 changes: 124 additions & 5 deletions crates/vite_task_graph/src/config/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::sync::Arc;

use monostate::MustBe;
use rustc_hash::FxHashMap;
use serde::Deserialize;
use serde::{Deserialize, Serialize};
#[cfg(all(test, not(clippy)))]
use ts_rs::TS;
use vite_path::RelativePathBuf;
Expand Down Expand Up @@ -193,20 +193,56 @@ impl Default for UserTaskOptions {
}
}

/// Task command: a command string or a sequence of command strings.
#[derive(Debug, Deserialize, Serialize, 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 TaskCommand {
/// A single command string.
String(Str),
/// Command strings to run in order.
Array(Vec<Str>),
}

impl From<&str> for TaskCommand {
fn from(value: &str) -> Self {
Self::String(value.into())
}
}

impl From<Str> for TaskCommand {
fn from(value: Str) -> Self {
Self::String(value)
}
}

/// 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 string or sequence of command strings to run for the task.
pub command: TaskCommand,

/// 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.
Config(UserTaskConfig),
/// Command-only shorthand form using default task options.
Command(TaskCommand),
}

/// Root-level cache configuration.
///
/// Controls caching behavior for the entire workspace.
Expand Down Expand Up @@ -281,8 +317,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 @@ -417,6 +453,89 @@ mod tests {
);
}

#[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,
TaskCommand::Array(vec!["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::Command(TaskCommand::String("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::Command(TaskCommand::Array(vec![
"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,
TaskCommand::Array(vec!["echo one".into(), "echo two".into()])
);
assert_eq!(user_config.options.cwd_relative_to_package.as_ref().unwrap().as_str(), "src");
assert_eq!(user_config.options.depends_on.as_ref().unwrap().as_ref(), [Str::from("build")]);
assert_eq!(
user_config.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
22 changes: 20 additions & 2 deletions crates/vite_task_graph/src/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use serde::Serialize;
use vite_path::AbsolutePath;
use vite_str::Str;

use crate::{IndexedTaskGraph, TaskNodeIndex};
use crate::{IndexedTaskGraph, TaskNodeIndex, config::TaskCommand};

/// struct for printing a task in a human-readable way.
#[derive(Debug, Clone, Serialize)]
Expand Down Expand Up @@ -50,9 +50,27 @@ 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.command),
}
})
.collect()
}
}

// Display-only formatting for task list/selector descriptions. Execution planning keeps
// `TaskCommand` structured and must not depend on this joined string.
fn format_command_for_task_list(command: &TaskCommand) -> Str {
match command {
TaskCommand::String(command) => command.clone(),
TaskCommand::Array(commands) => {
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::Config(config) => config,
UserTaskDefinition::Command(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
3 changes: 3 additions & 0 deletions crates/vite_task_plan/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ pub enum Error {
#[error(transparent)]
TaskRecursionDetected(#[from] TaskRecursionError),

#[error("Invalid task command: {0}")]
InvalidTaskCommand(Str),

#[error("Invalid vite task command: {program} with args {args:?} under cwd {cwd:?}")]
ParsePlanRequest {
program: Str,
Expand Down
Loading