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
15 changes: 10 additions & 5 deletions crates/vite_task/docs/terminologies.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,24 @@
// 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
56 changes: 53 additions & 3 deletions crates/vite_task_graph/run-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export type InputBase = "package" | "workspace";

export type Task = {
/**
* The command to run for the task.
* Command string to run for the task.
*/
command: string,
/**
Expand Down Expand Up @@ -63,11 +63,61 @@ input?: Array<string | GlobWithBase | AutoInput>,
* - Negative patterns (e.g. `"!dist/cache/**"`) exclude matched files
*/
output?: Array<string | GlobWithBase>, } | {
/**
* Whether to cache the task
*/
cache: false, }) | {
/**
* Command strings to run for the task.
*/
command: Array<string>,
/**
* The working directory for the task, relative to the package root (not workspace root).
*/
cwd?: string,
/**
* Dependencies of this task. Use `package-name#task-name` to refer to tasks in other packages.
*/
dependsOn?: Array<string>, } & ({
/**
* Whether to cache the task
*/
cache?: true,
/**
* Environment variable names to be fingerprinted and passed to the task.
*/
env?: Array<string>,
/**
* Environment variable names to be passed to the task without fingerprinting.
*/
untrackedEnv?: Array<string>,
/**
* Files to include in the cache fingerprint.
*
* - Omitted: automatically tracks which files the task reads
* - `[]` (empty): disables file tracking entirely
* - 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
*/
input?: Array<string | GlobWithBase | AutoInput>,
/**
* Output files to archive after a successful run and restore on cache hit.
*
* - Omitted or `[]` (empty): no output archiving (default)
* - Glob patterns (e.g. `"dist/**"`) select specific output files, relative to the package directory
* - `{pattern: "...", base: "workspace" | "package"}` specifies a glob with an explicit base directory
* - Negative patterns (e.g. `"!dist/cache/**"`) exclude matched files
*/
output?: Array<string | GlobWithBase>, } | {
/**
* Whether to cache the task
*/
cache: false, });

export type TaskDefinition = Task | string | Array<string>;

export type UserGlobalCacheConfig = boolean | {
/**
* Enable caching for package.json scripts not defined in the `tasks` map.
Expand Down Expand Up @@ -98,9 +148,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
19 changes: 8 additions & 11 deletions crates/vite_task_graph/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use serde::Serialize;
pub use user::{
AutoInput, 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,10 @@ impl ResolvedTaskConfig {
package_dir: &Arc<AbsolutePath>,
workspace_root: &AbsolutePath,
) -> Result<Self, ResolveTaskConfigError> {
let (commands, options) = user_config.into_parts();
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
149 changes: 137 additions & 12 deletions crates/vite_task_graph/src/config/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,14 +197,57 @@ impl Default for UserTaskOptions {
#[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>,
#[serde(untagged, rename_all = "camelCase")]
pub enum UserTaskConfig {
/// Task object form with a single command string.
String {
/// Command string to run for the task.
command: Str,

/// Fields other than the command
#[serde(flatten)]
options: UserTaskOptions,
},
/// Task object form with a sequence of command strings.
Array {
/// Command strings to run for the task.
command: Arc<[Str]>,

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

impl UserTaskConfig {
#[must_use]
pub const fn options(&self) -> &UserTaskOptions {
match self {
Self::String { options, .. } | Self::Array { options, .. } => options,
}
}

#[must_use]
pub fn into_parts(self) -> (Arc<[Str]>, UserTaskOptions) {
match self {
Self::String { command, options } => (vec![command].into(), options),
Self::Array { command, options } => (command, options),
}
}
}

/// 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.
CommandString(Str),
/// Command sequence shorthand form using default task options.
CommandArray(Arc<[Str]>),
}

/// Root-level cache configuration.
Expand Down Expand Up @@ -281,8 +324,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,18 +456,100 @@ 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::String {
command: "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();
let (commands, options) = user_config.into_parts();
assert_eq!(
commands,
Arc::from(["echo one".into(), "echo two".into(), "echo three".into()])
);
assert_eq!(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::CommandString("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::CommandArray(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();
let (commands, options) = user_config.into_parts();
assert_eq!(commands, Arc::from(["echo one".into(), "echo two".into()]));
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!({
"command": "echo test",
"cwd": "src"
});
let user_config: UserTaskConfig = serde_json::from_value(user_config_json).unwrap();
assert_eq!(user_config.options.cwd_relative_to_package.as_ref().unwrap().as_str(), "src");
assert_eq!(user_config.options().cwd_relative_to_package.as_ref().unwrap().as_str(), "src");
}

#[test]
Expand All @@ -435,7 +560,7 @@ mod tests {
});
let user_config: UserTaskConfig = serde_json::from_value(user_config_json).unwrap();
assert_eq!(
user_config.options.cache_config,
user_config.options().cache_config,
UserCacheConfig::Disabled { cache: MustBe!(false) }
);
}
Expand Down
Loading