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
160 changes: 158 additions & 2 deletions crates/vite_shell/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ use brush_parser::{
Parser, ParserOptions,
ast::{
AndOr, Assignment, AssignmentName, AssignmentValue, Command, CommandPrefix,
CommandPrefixOrSuffixItem, CommandSuffix, CompoundListItem, Pipeline, Program,
SeparatorOperator, SimpleCommand, SourceLocation, Word,
CommandPrefixOrSuffixItem, CommandSuffix, CompoundCommand, CompoundList, CompoundListItem,
DoGroupCommand, Pipeline, Program, SeparatorOperator, SimpleCommand, SourceLocation, Word,
},
word::{WordPiece, WordPieceWithSource},
};
Expand Down Expand Up @@ -135,6 +135,139 @@ fn pipeline_to_command(pipeline: &Pipeline) -> Option<(TaskParsedCommand, Range<
Some((TaskParsedCommand { envs, program: unquote(program)?, args }, range))
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum CwdChangingCommand {
Cd,
Chdir,
Dot,
Source,
Eval,
}

impl CwdChangingCommand {
fn from_name(command_name: &str) -> Option<Self> {
Some(match command_name {
"cd" => Self::Cd,
"chdir" => Self::Chdir,
"." => Self::Dot,
"source" => Self::Source,
"eval" => Self::Eval,
_ => return None,
})
}
}

fn command_name_may_change_cwd(command_name: &str) -> bool {
CwdChangingCommand::from_name(command_name).is_some()
}

fn wrapper_command_may_change_cwd(suffix: Option<&CommandSuffix>) -> bool {
let Some(CommandSuffix(suffix_items)) = suffix else {
return false;
};

for suffix_item in suffix_items {
let CommandPrefixOrSuffixItem::Word(word) = suffix_item else {
continue;
};
let Some(arg) = unquote(word) else {
return true;
};
if arg == "--" || arg.starts_with('-') {
continue;
}
return command_name_may_change_cwd(arg.as_str());
}

false
}

fn simple_command_may_change_cwd(simple_command: &SimpleCommand) -> bool {
let Some(program) = &simple_command.word_or_name else {
return false;
};
let Some(program) = unquote(program) else {
return true;
};

match program.as_str() {
"command" | "builtin" | "time" => {
wrapper_command_may_change_cwd(simple_command.suffix.as_ref())
}
command_name => command_name_may_change_cwd(command_name),
}
}

fn and_or_list_may_change_cwd(and_or_list: &brush_parser::ast::AndOrList) -> bool {
pipeline_may_change_cwd(&and_or_list.first)
|| and_or_list.additional.iter().any(|and_or| {
let pipeline = match and_or {
AndOr::And(pipeline) | AndOr::Or(pipeline) => pipeline,
};
pipeline_may_change_cwd(pipeline)
})
}

fn compound_list_may_change_cwd(compound_list: &CompoundList) -> bool {
compound_list
.0
.iter()
.any(|CompoundListItem(and_or_list, _)| and_or_list_may_change_cwd(and_or_list))
}

fn do_group_may_change_cwd(do_group: &DoGroupCommand) -> bool {
compound_list_may_change_cwd(&do_group.list)
}

fn compound_command_may_change_cwd(compound_command: &CompoundCommand) -> bool {
match compound_command {
CompoundCommand::Arithmetic(_) | CompoundCommand::Subshell(_) => false,
CompoundCommand::ArithmeticForClause(command) => do_group_may_change_cwd(&command.body),
CompoundCommand::BraceGroup(command) => compound_list_may_change_cwd(&command.list),
CompoundCommand::ForClause(command) => do_group_may_change_cwd(&command.body),
CompoundCommand::CaseClause(command) => command
.cases
.iter()
.any(|case| case.cmd.as_ref().is_some_and(compound_list_may_change_cwd)),
CompoundCommand::IfClause(command) => {
compound_list_may_change_cwd(&command.condition)
|| compound_list_may_change_cwd(&command.then)
|| command.elses.as_ref().is_some_and(|elses| {
elses.iter().any(|else_clause| {
else_clause.condition.as_ref().is_some_and(compound_list_may_change_cwd)
|| compound_list_may_change_cwd(&else_clause.body)
})
})
}
CompoundCommand::WhileClause(command) | CompoundCommand::UntilClause(command) => {
compound_list_may_change_cwd(&command.0) || do_group_may_change_cwd(&command.1)
}
}
}

fn command_may_change_cwd(command: &Command) -> bool {
match command {
Command::Simple(simple_command) => simple_command_may_change_cwd(simple_command),
Command::Compound(compound_command, _) => compound_command_may_change_cwd(compound_command),
Command::Function(function) => compound_command_may_change_cwd(&function.body.0),
Command::ExtendedTest(..) => false,
}
}

fn pipeline_may_change_cwd(pipeline: &Pipeline) -> bool {
pipeline.seq.iter().any(command_may_change_cwd)
}

#[must_use]
pub fn shell_command_may_change_cwd(cmd: &str) -> bool {
let mut parser = Parser::new(cmd.as_bytes(), &PARSER_OPTIONS);
let Ok(Program { complete_commands }) = parser.parse_program() else {
return true;
};

complete_commands.iter().any(compound_list_may_change_cwd)
}

#[must_use]
pub fn try_parse_as_and_list(cmd: &str) -> Option<Vec<(TaskParsedCommand, Range<usize>)>> {
let mut parser = Parser::new(cmd.as_bytes(), &PARSER_OPTIONS);
Expand Down Expand Up @@ -162,6 +295,29 @@ pub fn try_parse_as_and_list(cmd: &str) -> Option<Vec<(TaskParsedCommand, Range<
mod tests {
use super::*;

#[test]
fn test_shell_command_may_change_cwd_with_unresolved_arg() {
assert!(shell_command_may_change_cwd(r#"cd "$APP_DIR""#));
assert!(shell_command_may_change_cwd(r#"echo ok && cd "$APP_DIR""#));
assert!(shell_command_may_change_cwd(r#"FOO=bar 'cd' "$APP_DIR""#));
}

#[test]
fn test_shell_command_may_change_cwd_in_compound_commands() {
assert!(shell_command_may_change_cwd("if true; then cd snapshots; fi"));
assert!(shell_command_may_change_cwd("{ cd snapshots; }"));
assert!(shell_command_may_change_cwd("f(){ cd snapshots; }; f"));
assert!(shell_command_may_change_cwd("command cd snapshots"));
assert!(shell_command_may_change_cwd("time cd snapshots"));
}

#[test]
fn test_shell_command_may_change_cwd_ignores_non_current_shell_cd() {
assert!(!shell_command_may_change_cwd(r#"echo "cd $APP_DIR""#));
assert!(!shell_command_may_change_cwd("cdtool $APP_DIR"));
assert!(!shell_command_may_change_cwd("(cd snapshots)"));
}

#[test]
fn test_parse_single_command() {
let source = r"A=B hello world";
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
Loading