From f0bdc49d4454947022052f40e03a9d40346cb8d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A2=85=EA=B2=BD?= Date: Tue, 19 May 2026 19:19:12 +0900 Subject: [PATCH 01/10] feat(config): support task command shorthands Allow task definitions to use command string and command array shorthands, and allow object-form command arrays to normalize through the existing && command planning path. Update generated config types, docs, changelog, and plan snapshots for the new syntax. --- crates/vite_task/docs/task-cache.md | 6 +- crates/vite_task/docs/terminologies.md | 17 +- crates/vite_task_graph/run-config.ts | 16 +- crates/vite_task_graph/src/config/mod.rs | 6 +- crates/vite_task_graph/src/config/user.rs | 185 +++++++++- crates/vite_task_graph/src/lib.rs | 1 + .../task_command_shorthands/package.json | 4 + .../task_command_shorthands/snapshots.toml | 15 + .../snapshots/query_array_shorthand.jsonc | 226 +++++++++++++ .../query_object_array_cache_false.jsonc | 115 +++++++ .../query_object_array_depends_on.jsonc | 315 ++++++++++++++++++ .../snapshots/query_string_shorthand.jsonc | 90 +++++ .../snapshots/task_graph.jsonc | 186 +++++++++++ .../task_command_shorthands/vite-task.json | 15 + .../package.json | 4 + .../snapshots.toml | 7 + ...query_array_shorthand_cache_disabled.jsonc | 84 +++++ ...uery_string_shorthand_cache_disabled.jsonc | 53 +++ .../snapshots/task_graph.jsonc | 81 +++++ .../vite-task.json | 9 + 20 files changed, 1415 insertions(+), 20 deletions(-) create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/package.json create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots.toml create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_shorthand.jsonc create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_object_array_cache_false.jsonc create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_object_array_depends_on.jsonc create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_string_shorthand.jsonc create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/task_graph.jsonc create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/vite-task.json create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/package.json create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/snapshots.toml create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/snapshots/query_array_shorthand_cache_disabled.jsonc create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/snapshots/query_string_shorthand_cache_disabled.jsonc create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/snapshots/task_graph.jsonc create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/vite-task.json diff --git a/crates/vite_task/docs/task-cache.md b/crates/vite_task/docs/task-cache.md index 440901ccd..bd71db2f9 100644 --- a/crates/vite_task/docs/task-cache.md +++ b/crates/vite_task/docs/task-cache.md @@ -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 diff --git a/crates/vite_task/docs/terminologies.md b/crates/vite_task/docs/terminologies.md index 6118fc52e..62e3ebb71 100644 --- a/crates/vite_task/docs/terminologies.md +++ b/crates/vite_task/docs/terminologies.md @@ -7,8 +7,8 @@ { "name": "app", "scripts": { - "build": "echo build1 && echo build2", - }, + "build": "echo build1 && echo build2" + } } ``` @@ -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. diff --git a/crates/vite_task_graph/run-config.ts b/crates/vite_task_graph/run-config.ts index 1fa4ee868..23b36d35b 100644 --- a/crates/vite_task_graph/run-config.ts +++ b/crates/vite_task_graph/run-config.ts @@ -20,9 +20,11 @@ export type InputBase = "package" | "workspace"; export type Task = { /** - * The command to run for the task. + * Command string, or command snippets joined with ` && `. + * + * Arrays are not argv-style and element boundaries are not preserved after joining. */ -command: string, +command: TaskCommand, /** * The working directory for the task, relative to the package root (not workspace root). */ @@ -68,6 +70,10 @@ output?: Array, } | { */ cache: false, }); +export type TaskCommand = string | Array; + +export type TaskDefinition = Task | TaskCommand; + export type UserGlobalCacheConfig = boolean | { /** * Enable caching for package.json scripts not defined in the `tasks` map. @@ -98,9 +104,11 @@ export type RunConfig = { */ cache?: UserGlobalCacheConfig, /** - * Task definitions + * Task definitions: full task objects, command strings, or command arrays. + * + * Arrays are command snippets joined with ` && `, not argv-style arguments. */ -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. diff --git a/crates/vite_task_graph/src/config/mod.rs b/crates/vite_task_graph/src/config/mod.rs index a68efbecd..c19724ccc 100644 --- a/crates/vite_task_graph/src/config/mod.rs +++ b/crates/vite_task_graph/src/config/mod.rs @@ -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; @@ -380,7 +380,7 @@ impl ResolvedTaskConfig { workspace_root: &AbsolutePath, ) -> Result { Ok(Self { - command: Str::from(user_config.command.as_ref()), + command: user_config.command.into_command_string(), resolved_options: ResolvedTaskOptions::resolve( user_config.options, package_dir, diff --git a/crates/vite_task_graph/src/config/user.rs b/crates/vite_task_graph/src/config/user.rs index aeee38608..590d1d41d 100644 --- a/crates/vite_task_graph/src/config/user.rs +++ b/crates/vite_task_graph/src/config/user.rs @@ -193,20 +193,92 @@ impl Default for UserTaskOptions { } } +/// Task command: a string, or command snippets joined with ` && `. +/// +/// Arrays are not argv-style and element boundaries are not preserved after joining. +#[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 TaskCommand { + /// A raw command string. + String(Str), + /// Command snippets to join with ` && `. + Array(Vec), +} + +impl From<&str> for TaskCommand { + fn from(value: &str) -> Self { + Self::String(value.into()) + } +} + +impl From for TaskCommand { + fn from(value: Str) -> Self { + Self::String(value) + } +} + +impl TaskCommand { + #[must_use] + pub fn into_command_string(self) -> Str { + match self { + Self::String(command) => command, + Self::Array(commands) => { + let mut commands = commands.into_iter(); + let Some(mut command) = commands.next() else { + return Str::default(); + }; + for item in commands { + command.push_str(" && "); + command.push_str(item.as_str()); + } + command + } + } + } +} + /// 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, + /// Command string, or command snippets joined with ` && `. + /// + /// Arrays are not argv-style and element boundaries are not preserved after joining. + 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), +} + +impl UserTaskDefinition { + #[must_use] + pub fn into_config(self) -> UserTaskConfig { + match self { + Self::Config(config) => config, + Self::Command(command) => { + UserTaskConfig { command, options: UserTaskOptions::default() } + } + } + } +} + /// Root-level cache configuration. /// /// Controls caching behavior for the entire workspace. @@ -281,8 +353,10 @@ pub struct UserRunConfig { /// Setting it in a package's config will result in an error. pub cache: Option, - /// Task definitions - pub tasks: Option>, + /// Task definitions: full task objects, command strings, or command arrays. + /// + /// Arrays are command snippets joined with ` && `, not argv-style arguments. + pub tasks: Option>, /// Whether to automatically run `preX`/`postX` package.json scripts as /// lifecycle hooks when script `X` is executed. @@ -417,6 +491,109 @@ 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.into_command_string(), "echo one && echo two && echo three"); + assert_eq!(user_config.options, UserTaskOptions::default()); + } + + #[test] + fn test_command_array_preserves_empty_entries() { + let user_config_json = json!({ + "command": ["", "echo done", ""] + }); + let user_config: UserTaskConfig = serde_json::from_value(user_config_json).unwrap(); + assert_eq!(user_config.command.into_command_string(), " && echo done && "); + } + + #[test] + fn test_command_array_empty() { + let user_config_json = json!({ + "command": [] + }); + let user_config: UserTaskConfig = serde_json::from_value(user_config_json).unwrap(); + assert_eq!(user_config.command.into_command_string(), ""); + } + + #[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().into_config(); + assert_eq!(task.command.into_command_string(), "echo build"); + assert_eq!(task.options, UserTaskOptions::default()); + } + + #[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().into_config(); + assert_eq!(task.command.into_command_string(), "echo one && echo two && echo three"); + assert_eq!(task.options, UserTaskOptions::default()); + } + + #[test] + fn test_task_array_shorthand_empty() { + let user_config_json = json!({ + "tasks": { + "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().into_config(); + assert_eq!(task.command.into_command_string(), ""); + assert_eq!(task.options, UserTaskOptions::default()); + } + + #[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.into_command_string(), "echo one && echo two"); + 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::(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::(user_config_json).is_err()); + } + #[test] fn test_cwd_rename() { let user_config_json = json!({ diff --git a/crates/vite_task_graph/src/lib.rs b/crates/vite_task_graph/src/lib.rs index 98a7c3d62..b8cdde03b 100644 --- a/crates/vite_task_graph/src/lib.rs +++ b/crates/vite_task_graph/src/lib.rs @@ -303,6 +303,7 @@ impl IndexedTaskGraph { let task_id = TaskId { task_name: task_name.clone(), package_index }; + let task_user_config = task_user_config.into_config(); let dependency_specifiers = task_user_config.options.depends_on.clone(); // Resolve the task configuration from the user config diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/package.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/package.json new file mode 100644 index 000000000..a2538e229 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/package.json @@ -0,0 +1,4 @@ +{ + "name": "@test/task-command-shorthands", + "private": true +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots.toml b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots.toml new file mode 100644 index 000000000..f32eec089 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots.toml @@ -0,0 +1,15 @@ +[[plan]] +name = "string_shorthand" +args = ["run", "string_shorthand"] + +[[plan]] +name = "array_shorthand" +args = ["run", "array_shorthand"] + +[[plan]] +name = "object_array_cache_false" +args = ["run", "object_array_cache_false"] + +[[plan]] +name = "object_array_depends_on" +args = ["run", "object_array_depends_on"] diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_shorthand.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_shorthand.jsonc new file mode 100644 index 000000000..6fc10ef65 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_shorthand.jsonc @@ -0,0 +1,226 @@ +// run array_shorthand +{ + "graph": [ + { + "key": [ + "/", + "array_shorthand" + ], + "node": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "array_shorthand", + "package_path": "/" + }, + "items": [ + { + "execution_item_display": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "array_shorthand", + "package_path": "/" + }, + "command": "vtt print-file package.json", + "and_item_index": 0, + "cwd": "/" + }, + "kind": { + "Leaf": { + "Spawn": { + "cache_metadata": { + "spawn_fingerprint": { + "cwd": "", + "program_fingerprint": { + "OutsideWorkspace": { + "program_name": "vtt" + } + }, + "args": [ + "print-file", + "package.json" + ], + "env_fingerprints": { + "fingerprinted_envs": {}, + "untracked_env_config": [ + "" + ] + } + }, + "execution_cache_key": { + "UserTask": { + "task_name": "array_shorthand", + "and_item_index": 0, + "extra_args": [], + "package_path": "" + } + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": false, + "positive_globs": [], + "negative_globs": [] + } + }, + "spawn_command": { + "program_path": "/vtt", + "args": [ + "print-file", + "package.json" + ], + "all_envs": { + "FORCE_COLOR": "1", + "PATH": "/node_modules/.bin:" + }, + "cwd": "/" + } + } + } + } + }, + { + "execution_item_display": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "array_shorthand", + "package_path": "/" + }, + "command": "vtt print-file vite-task.json", + "and_item_index": 1, + "cwd": "/" + }, + "kind": { + "Leaf": { + "Spawn": { + "cache_metadata": { + "spawn_fingerprint": { + "cwd": "", + "program_fingerprint": { + "OutsideWorkspace": { + "program_name": "vtt" + } + }, + "args": [ + "print-file", + "vite-task.json" + ], + "env_fingerprints": { + "fingerprinted_envs": {}, + "untracked_env_config": [ + "" + ] + } + }, + "execution_cache_key": { + "UserTask": { + "task_name": "array_shorthand", + "and_item_index": 1, + "extra_args": [], + "package_path": "" + } + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": false, + "positive_globs": [], + "negative_globs": [] + } + }, + "spawn_command": { + "program_path": "/vtt", + "args": [ + "print-file", + "vite-task.json" + ], + "all_envs": { + "FORCE_COLOR": "1", + "PATH": "/node_modules/.bin:" + }, + "cwd": "/" + } + } + } + } + }, + { + "execution_item_display": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "array_shorthand", + "package_path": "/" + }, + "command": "vtt print-file package.json", + "and_item_index": 2, + "cwd": "/" + }, + "kind": { + "Leaf": { + "Spawn": { + "cache_metadata": { + "spawn_fingerprint": { + "cwd": "", + "program_fingerprint": { + "OutsideWorkspace": { + "program_name": "vtt" + } + }, + "args": [ + "print-file", + "package.json" + ], + "env_fingerprints": { + "fingerprinted_envs": {}, + "untracked_env_config": [ + "" + ] + } + }, + "execution_cache_key": { + "UserTask": { + "task_name": "array_shorthand", + "and_item_index": 2, + "extra_args": [], + "package_path": "" + } + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": false, + "positive_globs": [], + "negative_globs": [] + } + }, + "spawn_command": { + "program_path": "/vtt", + "args": [ + "print-file", + "package.json" + ], + "all_envs": { + "FORCE_COLOR": "1", + "PATH": "/node_modules/.bin:" + }, + "cwd": "/" + } + } + } + } + } + ] + }, + "neighbors": [] + } + ], + "concurrency_limit": 4 +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_object_array_cache_false.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_object_array_cache_false.jsonc new file mode 100644 index 000000000..810dc4e25 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_object_array_cache_false.jsonc @@ -0,0 +1,115 @@ +// run object_array_cache_false +{ + "graph": [ + { + "key": [ + "/", + "object_array_cache_false" + ], + "node": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "object_array_cache_false", + "package_path": "/" + }, + "items": [ + { + "execution_item_display": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "object_array_cache_false", + "package_path": "/" + }, + "command": "vtt print-file package.json", + "and_item_index": 0, + "cwd": "/" + }, + "kind": { + "Leaf": { + "Spawn": { + "cache_metadata": null, + "spawn_command": { + "program_path": "/vtt", + "args": [ + "print-file", + "package.json" + ], + "all_envs": { + "NO_COLOR": "1", + "PATH": "/node_modules/.bin:" + }, + "cwd": "/" + } + } + } + } + }, + { + "execution_item_display": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "object_array_cache_false", + "package_path": "/" + }, + "command": "vtt print-file vite-task.json", + "and_item_index": 1, + "cwd": "/" + }, + "kind": { + "Leaf": { + "Spawn": { + "cache_metadata": null, + "spawn_command": { + "program_path": "/vtt", + "args": [ + "print-file", + "vite-task.json" + ], + "all_envs": { + "NO_COLOR": "1", + "PATH": "/node_modules/.bin:" + }, + "cwd": "/" + } + } + } + } + }, + { + "execution_item_display": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "object_array_cache_false", + "package_path": "/" + }, + "command": "vtt print-file package.json", + "and_item_index": 2, + "cwd": "/" + }, + "kind": { + "Leaf": { + "Spawn": { + "cache_metadata": null, + "spawn_command": { + "program_path": "/vtt", + "args": [ + "print-file", + "package.json" + ], + "all_envs": { + "NO_COLOR": "1", + "PATH": "/node_modules/.bin:" + }, + "cwd": "/" + } + } + } + } + } + ] + }, + "neighbors": [] + } + ], + "concurrency_limit": 4 +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_object_array_depends_on.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_object_array_depends_on.jsonc new file mode 100644 index 000000000..9f14954cd --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_object_array_depends_on.jsonc @@ -0,0 +1,315 @@ +// run object_array_depends_on +{ + "graph": [ + { + "key": [ + "/", + "object_array_depends_on" + ], + "node": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "object_array_depends_on", + "package_path": "/" + }, + "items": [ + { + "execution_item_display": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "object_array_depends_on", + "package_path": "/" + }, + "command": "vtt print-file package.json", + "and_item_index": 0, + "cwd": "/" + }, + "kind": { + "Leaf": { + "Spawn": { + "cache_metadata": { + "spawn_fingerprint": { + "cwd": "", + "program_fingerprint": { + "OutsideWorkspace": { + "program_name": "vtt" + } + }, + "args": [ + "print-file", + "package.json" + ], + "env_fingerprints": { + "fingerprinted_envs": {}, + "untracked_env_config": [ + "" + ] + } + }, + "execution_cache_key": { + "UserTask": { + "task_name": "object_array_depends_on", + "and_item_index": 0, + "extra_args": [], + "package_path": "" + } + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": false, + "positive_globs": [], + "negative_globs": [] + } + }, + "spawn_command": { + "program_path": "/vtt", + "args": [ + "print-file", + "package.json" + ], + "all_envs": { + "FORCE_COLOR": "1", + "PATH": "/node_modules/.bin:" + }, + "cwd": "/" + } + } + } + } + }, + { + "execution_item_display": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "object_array_depends_on", + "package_path": "/" + }, + "command": "vtt print-file vite-task.json", + "and_item_index": 1, + "cwd": "/" + }, + "kind": { + "Leaf": { + "Spawn": { + "cache_metadata": { + "spawn_fingerprint": { + "cwd": "", + "program_fingerprint": { + "OutsideWorkspace": { + "program_name": "vtt" + } + }, + "args": [ + "print-file", + "vite-task.json" + ], + "env_fingerprints": { + "fingerprinted_envs": {}, + "untracked_env_config": [ + "" + ] + } + }, + "execution_cache_key": { + "UserTask": { + "task_name": "object_array_depends_on", + "and_item_index": 1, + "extra_args": [], + "package_path": "" + } + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": false, + "positive_globs": [], + "negative_globs": [] + } + }, + "spawn_command": { + "program_path": "/vtt", + "args": [ + "print-file", + "vite-task.json" + ], + "all_envs": { + "FORCE_COLOR": "1", + "PATH": "/node_modules/.bin:" + }, + "cwd": "/" + } + } + } + } + }, + { + "execution_item_display": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "object_array_depends_on", + "package_path": "/" + }, + "command": "vtt print-file package.json", + "and_item_index": 2, + "cwd": "/" + }, + "kind": { + "Leaf": { + "Spawn": { + "cache_metadata": { + "spawn_fingerprint": { + "cwd": "", + "program_fingerprint": { + "OutsideWorkspace": { + "program_name": "vtt" + } + }, + "args": [ + "print-file", + "package.json" + ], + "env_fingerprints": { + "fingerprinted_envs": {}, + "untracked_env_config": [ + "" + ] + } + }, + "execution_cache_key": { + "UserTask": { + "task_name": "object_array_depends_on", + "and_item_index": 2, + "extra_args": [], + "package_path": "" + } + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": false, + "positive_globs": [], + "negative_globs": [] + } + }, + "spawn_command": { + "program_path": "/vtt", + "args": [ + "print-file", + "package.json" + ], + "all_envs": { + "FORCE_COLOR": "1", + "PATH": "/node_modules/.bin:" + }, + "cwd": "/" + } + } + } + } + } + ] + }, + "neighbors": [ + [ + "/", + "string_shorthand" + ] + ] + }, + { + "key": [ + "/", + "string_shorthand" + ], + "node": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "string_shorthand", + "package_path": "/" + }, + "items": [ + { + "execution_item_display": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "string_shorthand", + "package_path": "/" + }, + "command": "vtt print-file package.json", + "and_item_index": null, + "cwd": "/" + }, + "kind": { + "Leaf": { + "Spawn": { + "cache_metadata": { + "spawn_fingerprint": { + "cwd": "", + "program_fingerprint": { + "OutsideWorkspace": { + "program_name": "vtt" + } + }, + "args": [ + "print-file", + "package.json" + ], + "env_fingerprints": { + "fingerprinted_envs": {}, + "untracked_env_config": [ + "" + ] + } + }, + "execution_cache_key": { + "UserTask": { + "task_name": "string_shorthand", + "and_item_index": 0, + "extra_args": [], + "package_path": "" + } + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": false, + "positive_globs": [], + "negative_globs": [] + } + }, + "spawn_command": { + "program_path": "/vtt", + "args": [ + "print-file", + "package.json" + ], + "all_envs": { + "FORCE_COLOR": "1", + "PATH": "/node_modules/.bin:" + }, + "cwd": "/" + } + } + } + } + } + ] + }, + "neighbors": [] + } + ], + "concurrency_limit": 4 +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_string_shorthand.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_string_shorthand.jsonc new file mode 100644 index 000000000..eb265c51c --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_string_shorthand.jsonc @@ -0,0 +1,90 @@ +// run string_shorthand +{ + "graph": [ + { + "key": [ + "/", + "string_shorthand" + ], + "node": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "string_shorthand", + "package_path": "/" + }, + "items": [ + { + "execution_item_display": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "string_shorthand", + "package_path": "/" + }, + "command": "vtt print-file package.json", + "and_item_index": null, + "cwd": "/" + }, + "kind": { + "Leaf": { + "Spawn": { + "cache_metadata": { + "spawn_fingerprint": { + "cwd": "", + "program_fingerprint": { + "OutsideWorkspace": { + "program_name": "vtt" + } + }, + "args": [ + "print-file", + "package.json" + ], + "env_fingerprints": { + "fingerprinted_envs": {}, + "untracked_env_config": [ + "" + ] + } + }, + "execution_cache_key": { + "UserTask": { + "task_name": "string_shorthand", + "and_item_index": 0, + "extra_args": [], + "package_path": "" + } + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": false, + "positive_globs": [], + "negative_globs": [] + } + }, + "spawn_command": { + "program_path": "/vtt", + "args": [ + "print-file", + "package.json" + ], + "all_envs": { + "FORCE_COLOR": "1", + "PATH": "/node_modules/.bin:" + }, + "cwd": "/" + } + } + } + } + } + ] + }, + "neighbors": [] + } + ], + "concurrency_limit": 4 +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/task_graph.jsonc new file mode 100644 index 000000000..b3b60a664 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/task_graph.jsonc @@ -0,0 +1,186 @@ +// task graph +[ + { + "key": [ + "/", + "array_shorthand" + ], + "node": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "array_shorthand", + "package_path": "/" + }, + "resolved_config": { + "command": "vtt print-file package.json && vtt print-file vite-task.json && vtt print-file package.json", + "resolved_options": { + "cwd": "/", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": false, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "TaskConfig" + }, + "neighbors": [] + }, + { + "key": [ + "/", + "empty_array_shorthand" + ], + "node": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "empty_array_shorthand", + "package_path": "/" + }, + "resolved_config": { + "command": "", + "resolved_options": { + "cwd": "/", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": false, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "TaskConfig" + }, + "neighbors": [] + }, + { + "key": [ + "/", + "object_array_cache_false" + ], + "node": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "object_array_cache_false", + "package_path": "/" + }, + "resolved_config": { + "command": "vtt print-file package.json && vtt print-file vite-task.json && vtt print-file package.json", + "resolved_options": { + "cwd": "/", + "cache_config": null + } + }, + "source": "TaskConfig" + }, + "neighbors": [] + }, + { + "key": [ + "/", + "object_array_depends_on" + ], + "node": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "object_array_depends_on", + "package_path": "/" + }, + "resolved_config": { + "command": "vtt print-file package.json && vtt print-file vite-task.json && vtt print-file package.json", + "resolved_options": { + "cwd": "/", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": false, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "TaskConfig" + }, + "neighbors": [ + [ + "/", + "string_shorthand" + ] + ] + }, + { + "key": [ + "/", + "string_shorthand" + ], + "node": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "string_shorthand", + "package_path": "/" + }, + "resolved_config": { + "command": "vtt print-file package.json", + "resolved_options": { + "cwd": "/", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": false, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "TaskConfig" + }, + "neighbors": [] + } +] diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/vite-task.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/vite-task.json new file mode 100644 index 000000000..8c5f519e5 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/vite-task.json @@ -0,0 +1,15 @@ +{ + "tasks": { + "string_shorthand": "vtt print-file package.json", + "array_shorthand": ["vtt print-file package.json", "vtt print-file vite-task.json", "vtt print-file package.json"], + "object_array_cache_false": { + "command": ["vtt print-file package.json", "vtt print-file vite-task.json", "vtt print-file package.json"], + "cache": false + }, + "object_array_depends_on": { + "command": ["vtt print-file package.json", "vtt print-file vite-task.json", "vtt print-file package.json"], + "dependsOn": ["string_shorthand"] + }, + "empty_array_shorthand": [] + } +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/package.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/package.json new file mode 100644 index 000000000..c2b5e2749 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/package.json @@ -0,0 +1,4 @@ +{ + "name": "@test/task-command-shorthands-cache-disabled", + "private": true +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/snapshots.toml b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/snapshots.toml new file mode 100644 index 000000000..9bfaabf9d --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/snapshots.toml @@ -0,0 +1,7 @@ +[[plan]] +name = "string_shorthand_cache_disabled" +args = ["run", "string_shorthand"] + +[[plan]] +name = "array_shorthand_cache_disabled" +args = ["run", "array_shorthand"] diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/snapshots/query_array_shorthand_cache_disabled.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/snapshots/query_array_shorthand_cache_disabled.jsonc new file mode 100644 index 000000000..1f51a25f7 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/snapshots/query_array_shorthand_cache_disabled.jsonc @@ -0,0 +1,84 @@ +// run array_shorthand +{ + "graph": [ + { + "key": [ + "/", + "array_shorthand" + ], + "node": { + "task_display": { + "package_name": "@test/task-command-shorthands-cache-disabled", + "task_name": "array_shorthand", + "package_path": "/" + }, + "items": [ + { + "execution_item_display": { + "task_display": { + "package_name": "@test/task-command-shorthands-cache-disabled", + "task_name": "array_shorthand", + "package_path": "/" + }, + "command": "vtt print-file package.json", + "and_item_index": 0, + "cwd": "/" + }, + "kind": { + "Leaf": { + "Spawn": { + "cache_metadata": null, + "spawn_command": { + "program_path": "/vtt", + "args": [ + "print-file", + "package.json" + ], + "all_envs": { + "NO_COLOR": "1", + "PATH": "/node_modules/.bin:" + }, + "cwd": "/" + } + } + } + } + }, + { + "execution_item_display": { + "task_display": { + "package_name": "@test/task-command-shorthands-cache-disabled", + "task_name": "array_shorthand", + "package_path": "/" + }, + "command": "vtt print-file vite-task.json", + "and_item_index": 1, + "cwd": "/" + }, + "kind": { + "Leaf": { + "Spawn": { + "cache_metadata": null, + "spawn_command": { + "program_path": "/vtt", + "args": [ + "print-file", + "vite-task.json" + ], + "all_envs": { + "NO_COLOR": "1", + "PATH": "/node_modules/.bin:" + }, + "cwd": "/" + } + } + } + } + } + ] + }, + "neighbors": [] + } + ], + "concurrency_limit": 4 +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/snapshots/query_string_shorthand_cache_disabled.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/snapshots/query_string_shorthand_cache_disabled.jsonc new file mode 100644 index 000000000..c072a0f98 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/snapshots/query_string_shorthand_cache_disabled.jsonc @@ -0,0 +1,53 @@ +// run string_shorthand +{ + "graph": [ + { + "key": [ + "/", + "string_shorthand" + ], + "node": { + "task_display": { + "package_name": "@test/task-command-shorthands-cache-disabled", + "task_name": "string_shorthand", + "package_path": "/" + }, + "items": [ + { + "execution_item_display": { + "task_display": { + "package_name": "@test/task-command-shorthands-cache-disabled", + "task_name": "string_shorthand", + "package_path": "/" + }, + "command": "vtt print-file package.json", + "and_item_index": null, + "cwd": "/" + }, + "kind": { + "Leaf": { + "Spawn": { + "cache_metadata": null, + "spawn_command": { + "program_path": "/vtt", + "args": [ + "print-file", + "package.json" + ], + "all_envs": { + "NO_COLOR": "1", + "PATH": "/node_modules/.bin:" + }, + "cwd": "/" + } + } + } + } + } + ] + }, + "neighbors": [] + } + ], + "concurrency_limit": 4 +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/snapshots/task_graph.jsonc new file mode 100644 index 000000000..237a5d824 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/snapshots/task_graph.jsonc @@ -0,0 +1,81 @@ +// task graph +[ + { + "key": [ + "/", + "array_shorthand" + ], + "node": { + "task_display": { + "package_name": "@test/task-command-shorthands-cache-disabled", + "task_name": "array_shorthand", + "package_path": "/" + }, + "resolved_config": { + "command": "vtt print-file package.json && vtt print-file vite-task.json", + "resolved_options": { + "cwd": "/", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": false, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "TaskConfig" + }, + "neighbors": [] + }, + { + "key": [ + "/", + "string_shorthand" + ], + "node": { + "task_display": { + "package_name": "@test/task-command-shorthands-cache-disabled", + "task_name": "string_shorthand", + "package_path": "/" + }, + "resolved_config": { + "command": "vtt print-file package.json", + "resolved_options": { + "cwd": "/", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": false, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "TaskConfig" + }, + "neighbors": [] + } +] diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/vite-task.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/vite-task.json new file mode 100644 index 000000000..9b75b53d4 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/vite-task.json @@ -0,0 +1,9 @@ +{ + "cache": { + "tasks": false + }, + "tasks": { + "string_shorthand": "vtt print-file package.json", + "array_shorthand": ["vtt print-file package.json", "vtt print-file vite-task.json"] + } +} From d4c8723898c57e7c672fb54100a9c64be4af7849 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A2=85=EA=B2=BD?= Date: Tue, 19 May 2026 19:36:45 +0900 Subject: [PATCH 02/10] fix(config): reject empty command arrays --- crates/vite_task_graph/src/config/user.rs | 65 +++++++++++++------ .../snapshots/task_graph.jsonc | 39 ----------- .../task_command_shorthands/vite-task.json | 3 +- 3 files changed, 47 insertions(+), 60 deletions(-) diff --git a/crates/vite_task_graph/src/config/user.rs b/crates/vite_task_graph/src/config/user.rs index 590d1d41d..a1e759be2 100644 --- a/crates/vite_task_graph/src/config/user.rs +++ b/crates/vite_task_graph/src/config/user.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use monostate::MustBe; use rustc_hash::FxHashMap; -use serde::Deserialize; +use serde::{Deserialize, Deserializer, de::Error as _}; #[cfg(all(test, not(clippy)))] use ts_rs::TS; use vite_path::RelativePathBuf; @@ -196,10 +196,9 @@ impl Default for UserTaskOptions { /// Task command: a string, or command snippets joined with ` && `. /// /// Arrays are not argv-style and element boundaries are not preserved after joining. -#[derive(Debug, Deserialize, PartialEq, Eq)] +#[derive(Debug, 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)] +#[cfg_attr(all(test, not(clippy)), derive(TS), ts(type = "string | Array"))] pub enum TaskCommand { /// A raw command string. String(Str), @@ -207,6 +206,33 @@ pub enum TaskCommand { Array(Vec), } +impl<'de> Deserialize<'de> for TaskCommand { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(untagged)] + enum TaskCommandInput { + String(Str), + Array(Vec), + } + + match TaskCommandInput::deserialize(deserializer)? { + TaskCommandInput::String(command) => Ok(Self::String(command)), + TaskCommandInput::Array(commands) => { + if commands.is_empty() { + return Err(D::Error::custom("command array must not be empty")); + } + if commands.iter().any(|command| command.as_str().trim().is_empty()) { + return Err(D::Error::custom("command array entries must not be empty")); + } + Ok(Self::Array(commands)) + } + } + } +} + impl From<&str> for TaskCommand { fn from(value: &str) -> Self { Self::String(value.into()) @@ -226,9 +252,7 @@ impl TaskCommand { Self::String(command) => command, Self::Array(commands) => { let mut commands = commands.into_iter(); - let Some(mut command) = commands.next() else { - return Str::default(); - }; + let mut command = commands.next().expect("command arrays are non-empty"); for item in commands { command.push_str(" && "); command.push_str(item.as_str()); @@ -502,21 +526,27 @@ mod tests { } #[test] - fn test_command_array_preserves_empty_entries() { + fn test_command_array_empty_item_error() { let user_config_json = json!({ - "command": ["", "echo done", ""] + "command": ["", "echo done"] }); - let user_config: UserTaskConfig = serde_json::from_value(user_config_json).unwrap(); - assert_eq!(user_config.command.into_command_string(), " && echo done && "); + assert!(serde_json::from_value::(user_config_json).is_err()); + } + + #[test] + fn test_command_array_whitespace_item_error() { + let user_config_json = json!({ + "command": ["echo done", " "] + }); + assert!(serde_json::from_value::(user_config_json).is_err()); } #[test] - fn test_command_array_empty() { + fn test_command_array_empty_error() { let user_config_json = json!({ "command": [] }); - let user_config: UserTaskConfig = serde_json::from_value(user_config_json).unwrap(); - assert_eq!(user_config.command.into_command_string(), ""); + assert!(serde_json::from_value::(user_config_json).is_err()); } #[test] @@ -546,16 +576,13 @@ mod tests { } #[test] - fn test_task_array_shorthand_empty() { + fn test_task_array_shorthand_empty_error() { let user_config_json = json!({ "tasks": { "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().into_config(); - assert_eq!(task.command.into_command_string(), ""); - assert_eq!(task.options, UserTaskOptions::default()); + assert!(serde_json::from_value::(user_config_json).is_err()); } #[test] diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/task_graph.jsonc index b3b60a664..4c0dd63e5 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/task_graph.jsonc @@ -39,45 +39,6 @@ }, "neighbors": [] }, - { - "key": [ - "/", - "empty_array_shorthand" - ], - "node": { - "task_display": { - "package_name": "@test/task-command-shorthands", - "task_name": "empty_array_shorthand", - "package_path": "/" - }, - "resolved_config": { - "command": "", - "resolved_options": { - "cwd": "/", - "cache_config": { - "env_config": { - "fingerprinted_envs": [], - "untracked_env": [ - "" - ] - }, - "input_config": { - "includes_auto": true, - "positive_globs": [], - "negative_globs": [] - }, - "output_config": { - "includes_auto": false, - "positive_globs": [], - "negative_globs": [] - } - } - } - }, - "source": "TaskConfig" - }, - "neighbors": [] - }, { "key": [ "/", diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/vite-task.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/vite-task.json index 8c5f519e5..f36439d39 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/vite-task.json +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/vite-task.json @@ -9,7 +9,6 @@ "object_array_depends_on": { "command": ["vtt print-file package.json", "vtt print-file vite-task.json", "vtt print-file package.json"], "dependsOn": ["string_shorthand"] - }, - "empty_array_shorthand": [] + } } } From d13df64af3705e2cecac9d800322286ff72475a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A2=85=EA=B2=BD?= Date: Tue, 19 May 2026 20:40:34 +0900 Subject: [PATCH 03/10] fix(plan): parse task command arrays in planner Keep task command arrays as config data through graph loading and parse each array entry in the planner. This preserves quoting boundaries, supports nested vp runs and mixed && entries, and reports empty array entries during planning. --- crates/vite_task_graph/run-config.ts | 8 +- crates/vite_task_graph/src/config/mod.rs | 10 +- crates/vite_task_graph/src/config/user.rs | 141 +---- crates/vite_task_graph/src/display.rs | 22 +- crates/vite_task_graph/src/lib.rs | 14 +- crates/vite_task_plan/src/error.rs | 3 + crates/vite_task_plan/src/plan.rs | 536 ++++++++++-------- .../task_command_shorthands/snapshots.toml | 16 + .../snapshots/query_array_with_and.jsonc | 226 ++++++++ .../snapshots/query_empty_array_error.snap | 1 + .../snapshots/query_empty_item_error.snap | 1 + .../snapshots/query_nested_vt_array.jsonc | 193 +++++++ .../snapshots/task_graph.jsonc | 183 +++++- .../task_command_shorthands/vite-task.json | 6 +- .../package.json | 4 - .../snapshots.toml | 7 - ...query_array_shorthand_cache_disabled.jsonc | 84 --- ...uery_string_shorthand_cache_disabled.jsonc | 53 -- .../snapshots/task_graph.jsonc | 81 --- .../vite-task.json | 9 - 20 files changed, 982 insertions(+), 616 deletions(-) create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_with_and.jsonc create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_empty_array_error.snap create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_empty_item_error.snap create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_nested_vt_array.jsonc delete mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/package.json delete mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/snapshots.toml delete mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/snapshots/query_array_shorthand_cache_disabled.jsonc delete mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/snapshots/query_string_shorthand_cache_disabled.jsonc delete mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/snapshots/task_graph.jsonc delete mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/vite-task.json diff --git a/crates/vite_task_graph/run-config.ts b/crates/vite_task_graph/run-config.ts index 23b36d35b..258f0ff7e 100644 --- a/crates/vite_task_graph/run-config.ts +++ b/crates/vite_task_graph/run-config.ts @@ -20,9 +20,7 @@ export type InputBase = "package" | "workspace"; export type Task = { /** - * Command string, or command snippets joined with ` && `. - * - * Arrays are not argv-style and element boundaries are not preserved after joining. + * Command string or sequence of command strings to run for the task. */ command: TaskCommand, /** @@ -104,9 +102,7 @@ export type RunConfig = { */ cache?: UserGlobalCacheConfig, /** - * Task definitions: full task objects, command strings, or command arrays. - * - * Arrays are command snippets joined with ` && `, not argv-style arguments. + * Task definitions: full task objects, command strings, or command string arrays. */ tasks?: { [key in string]: TaskDefinition }, /** diff --git a/crates/vite_task_graph/src/config/mod.rs b/crates/vite_task_graph/src/config/mod.rs index c19724ccc..2bf6fd646 100644 --- a/crates/vite_task_graph/src/config/mod.rs +++ b/crates/vite_task_graph/src/config/mod.rs @@ -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, } @@ -360,7 +360,7 @@ impl ResolvedTaskConfig { workspace_root: &AbsolutePath, ) -> Result { Ok(Self { - command: package_json_script.into(), + command: TaskCommand::String(package_json_script.into()), resolved_options: ResolvedTaskOptions::resolve( UserTaskOptions::default(), package_dir, @@ -380,7 +380,7 @@ impl ResolvedTaskConfig { workspace_root: &AbsolutePath, ) -> Result { Ok(Self { - command: user_config.command.into_command_string(), + command: user_config.command, resolved_options: ResolvedTaskOptions::resolve( user_config.options, package_dir, diff --git a/crates/vite_task_graph/src/config/user.rs b/crates/vite_task_graph/src/config/user.rs index a1e759be2..5c801313b 100644 --- a/crates/vite_task_graph/src/config/user.rs +++ b/crates/vite_task_graph/src/config/user.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use monostate::MustBe; use rustc_hash::FxHashMap; -use serde::{Deserialize, Deserializer, de::Error as _}; +use serde::{Deserialize, Serialize}; #[cfg(all(test, not(clippy)))] use ts_rs::TS; use vite_path::RelativePathBuf; @@ -193,46 +193,18 @@ impl Default for UserTaskOptions { } } -/// Task command: a string, or command snippets joined with ` && `. -/// -/// Arrays are not argv-style and element boundaries are not preserved after joining. -#[derive(Debug, PartialEq, Eq)] +/// 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), ts(type = "string | Array"))] +#[cfg_attr(all(test, not(clippy)), derive(TS))] +#[serde(untagged)] pub enum TaskCommand { - /// A raw command string. + /// A single command string. String(Str), - /// Command snippets to join with ` && `. + /// Command strings to run in order. Array(Vec), } -impl<'de> Deserialize<'de> for TaskCommand { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - #[derive(Deserialize)] - #[serde(untagged)] - enum TaskCommandInput { - String(Str), - Array(Vec), - } - - match TaskCommandInput::deserialize(deserializer)? { - TaskCommandInput::String(command) => Ok(Self::String(command)), - TaskCommandInput::Array(commands) => { - if commands.is_empty() { - return Err(D::Error::custom("command array must not be empty")); - } - if commands.iter().any(|command| command.as_str().trim().is_empty()) { - return Err(D::Error::custom("command array entries must not be empty")); - } - Ok(Self::Array(commands)) - } - } - } -} - impl From<&str> for TaskCommand { fn from(value: &str) -> Self { Self::String(value.into()) @@ -245,33 +217,13 @@ impl From for TaskCommand { } } -impl TaskCommand { - #[must_use] - pub fn into_command_string(self) -> Str { - match self { - Self::String(command) => command, - Self::Array(commands) => { - let mut commands = commands.into_iter(); - let mut command = commands.next().expect("command arrays are non-empty"); - for item in commands { - command.push_str(" && "); - command.push_str(item.as_str()); - } - command - } - } - } -} - /// 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 { - /// Command string, or command snippets joined with ` && `. - /// - /// Arrays are not argv-style and element boundaries are not preserved after joining. + /// Command string or sequence of command strings to run for the task. pub command: TaskCommand, /// Fields other than the command @@ -291,18 +243,6 @@ pub enum UserTaskDefinition { Command(TaskCommand), } -impl UserTaskDefinition { - #[must_use] - pub fn into_config(self) -> UserTaskConfig { - match self { - Self::Config(config) => config, - Self::Command(command) => { - UserTaskConfig { command, options: UserTaskOptions::default() } - } - } - } -} - /// Root-level cache configuration. /// /// Controls caching behavior for the entire workspace. @@ -377,9 +317,7 @@ pub struct UserRunConfig { /// Setting it in a package's config will result in an error. pub cache: Option, - /// Task definitions: full task objects, command strings, or command arrays. - /// - /// Arrays are command snippets joined with ` && `, not argv-style arguments. + /// Task definitions: full task objects, command strings, or command string arrays. pub tasks: Option>, /// Whether to automatically run `preX`/`postX` package.json scripts as @@ -521,34 +459,13 @@ mod tests { "command": ["echo one", "echo two", "echo three"] }); let user_config: UserTaskConfig = serde_json::from_value(user_config_json).unwrap(); - assert_eq!(user_config.command.into_command_string(), "echo one && echo two && echo three"); + 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_command_array_empty_item_error() { - let user_config_json = json!({ - "command": ["", "echo done"] - }); - assert!(serde_json::from_value::(user_config_json).is_err()); - } - - #[test] - fn test_command_array_whitespace_item_error() { - let user_config_json = json!({ - "command": ["echo done", " "] - }); - assert!(serde_json::from_value::(user_config_json).is_err()); - } - - #[test] - fn test_command_array_empty_error() { - let user_config_json = json!({ - "command": [] - }); - assert!(serde_json::from_value::(user_config_json).is_err()); - } - #[test] fn test_task_string_shorthand() { let user_config_json = json!({ @@ -557,9 +474,8 @@ mod tests { } }); let mut user_config: UserRunConfig = serde_json::from_value(user_config_json).unwrap(); - let task = user_config.tasks.as_mut().unwrap().remove("build").unwrap().into_config(); - assert_eq!(task.command.into_command_string(), "echo build"); - assert_eq!(task.options, UserTaskOptions::default()); + let task = user_config.tasks.as_mut().unwrap().remove("build").unwrap(); + assert_eq!(task, UserTaskDefinition::Command(TaskCommand::String("echo build".into()))); } #[test] @@ -570,19 +486,15 @@ mod tests { } }); let mut user_config: UserRunConfig = serde_json::from_value(user_config_json).unwrap(); - let task = user_config.tasks.as_mut().unwrap().remove("build").unwrap().into_config(); - assert_eq!(task.command.into_command_string(), "echo one && echo two && echo three"); - assert_eq!(task.options, UserTaskOptions::default()); - } - - #[test] - fn test_task_array_shorthand_empty_error() { - let user_config_json = json!({ - "tasks": { - "build": [] - } - }); - assert!(serde_json::from_value::(user_config_json).is_err()); + 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] @@ -594,7 +506,10 @@ mod tests { "cache": false }); let user_config: UserTaskConfig = serde_json::from_value(user_config_json).unwrap(); - assert_eq!(user_config.command.into_command_string(), "echo one && echo two"); + 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!( diff --git a/crates/vite_task_graph/src/display.rs b/crates/vite_task_graph/src/display.rs index fbc06dd29..99051f010 100644 --- a/crates/vite_task_graph/src/display.rs +++ b/crates/vite_task_graph/src/display.rs @@ -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)] @@ -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 + } + } +} diff --git a/crates/vite_task_graph/src/lib.rs b/crates/vite_task_graph/src/lib.rs index b8cdde03b..feb488b1c 100644 --- a/crates/vite_task_graph/src/lib.rs +++ b/crates/vite_task_graph/src/lib.rs @@ -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; @@ -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. /// @@ -303,7 +306,12 @@ impl IndexedTaskGraph { let task_id = TaskId { task_name: task_name.clone(), package_index }; - let task_user_config = task_user_config.into_config(); + 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 diff --git a/crates/vite_task_plan/src/error.rs b/crates/vite_task_plan/src/error.rs index 7a255b8df..d7014697b 100644 --- a/crates/vite_task_plan/src/error.rs +++ b/crates/vite_task_plan/src/error.rs @@ -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, diff --git a/crates/vite_task_plan/src/plan.rs b/crates/vite_task_plan/src/plan.rs index 89e892b48..dfff5673d 100644 --- a/crates/vite_task_plan/src/plan.rs +++ b/crates/vite_task_plan/src/plan.rs @@ -8,6 +8,7 @@ use std::{ collections::BTreeMap, env::home_dir, ffi::OsStr, + ops::Range, sync::{Arc, LazyLock}, }; @@ -15,13 +16,13 @@ use futures_util::FutureExt; use petgraph::Direction; use rustc_hash::FxHashMap; use vite_path::{AbsolutePath, AbsolutePathBuf, RelativePathBuf, relative::InvalidPathDataError}; -use vite_shell::try_parse_as_and_list; +use vite_shell::{TaskParsedCommand, try_parse_as_and_list}; use vite_str::Str; use vite_task_graph::{ TaskNodeIndex, TaskSource, config::{ CacheConfig, EnabledCacheConfig, ResolvedGlobConfig, ResolvedGlobalCacheConfig, - ResolvedTaskOptions, + ResolvedTaskOptions, TaskCommand, user::{UserCacheConfig, UserTaskOptions}, }, query::TaskQuery, @@ -80,6 +81,45 @@ fn effective_cache_config( if enabled { task_cache_config.cloned() } else { None } } +enum PlannedCommand { + Parsed { and_item: TaskParsedCommand, display: Str, stack_frame: Range }, + Shell(Str), +} + +#[expect(clippy::result_large_err, reason = "Error is large for diagnostics")] +fn planned_commands(command: &TaskCommand) -> Result, Error> { + let snippets: Box + '_> = match command { + TaskCommand::String(command) => Box::new(std::iter::once(command)), + TaskCommand::Array(commands) => { + if commands.is_empty() { + return Err(Error::InvalidTaskCommand("command array must not be empty".into())); + } + if commands.iter().any(|command| command.as_str().trim().is_empty()) { + return Err(Error::InvalidTaskCommand( + "command array entries must not be empty".into(), + )); + } + Box::new(commands.iter()) + } + }; + + let mut planned = Vec::new(); + for snippet in snippets { + if let Some(parsed) = try_parse_as_and_list(snippet.as_str()) { + for (and_item, range) in parsed { + planned.push(PlannedCommand::Parsed { + display: Str::from(&snippet.as_str()[range.clone()]), + and_item, + stack_frame: range, + }); + } + } else { + planned.push(PlannedCommand::Shell(snippet.clone())); + } + } + Ok(planned) +} + /// - `with_hooks`: whether to look up `preX`/`postX` lifecycle hooks for this task. /// `false` when the task itself is being executed as a hook, so that hooks are /// never expanded more than one level deep (matching npm behavior). @@ -93,7 +133,6 @@ async fn plan_task_as_execution_node( context.check_recursion(task_node_index)?; let task_node = &context.indexed_task_graph().task_graph()[task_node_index]; - let command_str = task_node.resolved_config.command.as_str(); let package_path = context.indexed_task_graph().get_package_path_for_task(task_node_index); // Prepend {package_path}/node_modules/.bin to PATH @@ -128,264 +167,271 @@ async fn plan_task_as_execution_node( let mut cwd = Arc::clone(&task_node.resolved_config.resolved_options.cwd); // TODO: variable expansion (https://crates.io/crates/shellexpand) BEFORE parsing - // Try to parse the command string as a list of subcommands separated by `&&` - if let Some(parsed_subcommands) = try_parse_as_and_list(command_str) { - let and_item_count = parsed_subcommands.len(); - for (index, (and_item, add_item_span)) in parsed_subcommands.into_iter().enumerate() { - // Duplicate the context before modifying it for each and_item - let mut context = context.duplicate(); - context.push_stack_frame(task_node_index, add_item_span.clone()); - - let mut args = and_item.args; - let extra_args = if index == and_item_count - 1 { - // For the last and_item, append extra args from the plan context - Arc::clone(context.extra_args()) - } else { - Arc::new([]) - }; - args.extend(extra_args.iter().cloned()); + let planned_commands = planned_commands(&task_node.resolved_config.command)?; + let and_item_count = planned_commands.len(); + for (index, planned_command) in planned_commands.into_iter().enumerate() { + match planned_command { + PlannedCommand::Parsed { and_item, display, stack_frame } => { + // Duplicate the context before modifying it for each and_item + let mut context = context.duplicate(); + context.push_stack_frame(task_node_index, stack_frame); + + let mut args = and_item.args; + let extra_args = if index == and_item_count - 1 { + // For the last and_item, append extra args from the plan context + Arc::clone(context.extra_args()) + } else { + Arc::new([]) + }; + args.extend(extra_args.iter().cloned()); + + // Handle `cd` builtin command + if and_item.program == "cd" { + #[expect( + clippy::disallowed_types, + reason = "Path is needed for std::env::home_dir return type and AbsolutePath::join" + )] + let cd_target: Cow<'_, Path> = match args.as_slice() { + // No args, go to home directory + [] => home_dir() + .ok_or(Error::CdCommand(CdCommandError::NoHomeDirectory))? + .into(), + [dir] => Path::new(dir.as_str()).into(), + _ => { + return Err(Error::CdCommand(CdCommandError::TooManyArgs)); + } + }; + cwd = cwd.join(cd_target.as_ref()).into(); + continue; + } - // Handle `cd` builtin command - if and_item.program == "cd" { - #[expect( - clippy::disallowed_types, - reason = "Path is needed for std::env::home_dir return type and AbsolutePath::join" - )] - let cd_target: Cow<'_, Path> = match args.as_slice() { - // No args, go to home directory - [] => { - home_dir().ok_or(Error::CdCommand(CdCommandError::NoHomeDirectory))?.into() - } - [dir] => Path::new(dir.as_str()).into(), - _ => { - return Err(Error::CdCommand(CdCommandError::TooManyArgs)); - } + // Build execution display + let execution_item_display = ExecutionItemDisplay { + command: { + let mut command = display.clone(); + for arg in extra_args.iter() { + command.push(' '); + command.push_str(shell_escape::escape(arg.as_str().into()).as_ref()); + } + command + }, + and_item_index: if and_item_count > 1 { Some(index) } else { None }, + cwd: Arc::clone(&cwd), + task_display: task_node.task_display.clone(), }; - cwd = cwd.join(cd_target.as_ref()).into(); - continue; - } - // Build execution display - let execution_item_display = ExecutionItemDisplay { - command: { - let mut command = Str::from(&command_str[add_item_span.clone()]); - for arg in extra_args.iter() { - command.push(' '); - command.push_str(shell_escape::escape(arg.as_str().into()).as_ref()); - } - command - }, - and_item_index: if and_item_count > 1 { Some(index) } else { None }, - cwd: Arc::clone(&cwd), - task_display: task_node.task_display.clone(), - }; - - // Check for builtin commands like `echo ...` - if let Some(builtin_execution) = - InProcessExecution::get_builtin_execution(&and_item.program, args.iter(), &cwd) - { - items.push(ExecutionItem { - execution_item_display, - kind: ExecutionItemKind::Leaf(LeafExecutionKind::InProcess(builtin_execution)), - }); - continue; - } + // Check for builtin commands like `echo ...` + if let Some(builtin_execution) = + InProcessExecution::get_builtin_execution(&and_item.program, args.iter(), &cwd) + { + items.push(ExecutionItem { + execution_item_display, + kind: ExecutionItemKind::Leaf(LeafExecutionKind::InProcess( + builtin_execution, + )), + }); + continue; + } - // Create execution cache key for this and_item - let task_execution_cache_key = ExecutionCacheKey::UserTask { - task_name: task_node.task_display.task_name.clone(), - and_item_index: index, - extra_args: Arc::clone(&extra_args), - package_path: strip_prefix_for_cache(package_path, context.workspace_path()) - .map_err(|kind| PathFingerprintError { - kind, - path_type: PathType::PackagePath, - })?, - }; - - // Try to parse the args of an and_item to a plan request like `run -r build` - let envs: Arc, Arc>> = context.envs().clone().into(); - let mut script_command = ScriptCommand { - program: and_item.program.clone(), - args: args.into(), - envs, - cwd: Arc::clone(&cwd), - }; - let plan_request = - context.callbacks().get_plan_request(&mut script_command).await.map_err( - |error| Error::ParsePlanRequest { - program: script_command.program.clone(), - args: Arc::clone(&script_command.args), - cwd: Arc::clone(&script_command.cwd), - error, - }, - )?; + // Create execution cache key for this and_item + let task_execution_cache_key = ExecutionCacheKey::UserTask { + task_name: task_node.task_display.task_name.clone(), + and_item_index: index, + extra_args: Arc::clone(&extra_args), + package_path: strip_prefix_for_cache(package_path, context.workspace_path()) + .map_err(|kind| PathFingerprintError { + kind, + path_type: PathType::PackagePath, + })?, + }; - let execution_item_kind: ExecutionItemKind = match plan_request { - // Expand task query like `vp run -r build` - Some(PlanRequest::Query(query_plan_request)) => { - // Skip rule: skip if this nested query is the same as the parent expansion. - // This handles workspace root tasks like `"build": "vp run -r build"` — - // re-entering the same query would just re-expand the same tasks. - // - // The comparison is on TaskQuery only (package_query + task_name + - // include_explicit_deps). Extra args live in PlanOptions, so - // `vp run -r build extra_arg` still matches `vp run -r build`. - // Conversely, `cd packages/a && vp run build` does NOT match a - // parent `vp run build` from root because `cd` changes the cwd, - // producing a different ContainingPackage in the PackageQuery. - if query_plan_request.query == *context.parent_query() { - continue; - } + // Try to parse the args of an and_item to a plan request like `run -r build` + let envs: Arc, Arc>> = context.envs().clone().into(); + let mut script_command = ScriptCommand { + program: and_item.program.clone(), + args: args.into(), + envs, + cwd: Arc::clone(&cwd), + }; + let plan_request = + context.callbacks().get_plan_request(&mut script_command).await.map_err( + |error| Error::ParsePlanRequest { + program: script_command.program.clone(), + args: Arc::clone(&script_command.args), + cwd: Arc::clone(&script_command.cwd), + error, + }, + )?; + + let execution_item_kind: ExecutionItemKind = match plan_request { + // Expand task query like `vp run -r build` + Some(PlanRequest::Query(query_plan_request)) => { + // Skip rule: skip if this nested query is the same as the parent expansion. + if query_plan_request.query == *context.parent_query() { + continue; + } - // Save task name before consuming the request - let task_name = query_plan_request.query.task_name.clone(); - // Add prefix envs to the context - context.add_envs(and_item.envs.iter()); - let QueryPlanRequest { query, plan_options } = query_plan_request; - let query = Arc::new(query); - let execution_graph = - plan_query_request(Arc::clone(&query), plan_options, context) - .await - .map_err(|error| Error::NestPlan { + let task_name = query_plan_request.query.task_name.clone(); + context.add_envs(and_item.envs.iter()); + let QueryPlanRequest { query, plan_options } = query_plan_request; + let query = Arc::new(query); + let execution_graph = + plan_query_request(Arc::clone(&query), plan_options, context) + .await + .map_err(|error| Error::NestPlan { + task_display: task_node.task_display.clone(), + command: display.clone(), + error: Box::new(error), + })?; + if execution_graph.graph.node_count() == 0 { + return Err(Error::NestPlan { task_display: task_node.task_display.clone(), - command: Str::from(&command_str[add_item_span.clone()]), - error: Box::new(error), - })?; - // An empty execution graph means no tasks matched the query. - // At the top level the session shows the task selector UI, - // but in a nested context there is no UI — propagate as an error. - if execution_graph.graph.node_count() == 0 { - return Err(Error::NestPlan { - task_display: task_node.task_display.clone(), - command: Str::from(&command_str[add_item_span]), - error: Box::new(Error::NoTasksMatched(task_name)), - }); + command: display, + error: Box::new(Error::NoTasksMatched(task_name)), + }); + } + ExecutionItemKind::Expanded(execution_graph) } - ExecutionItemKind::Expanded(execution_graph) - } - // Synthetic task (from CommandHandler) - Some(PlanRequest::Synthetic(synthetic_plan_request)) => { - let task_effective_cache = effective_cache_config( - task_node.resolved_config.resolved_options.cache_config.as_ref(), - task_node.source, - *context.resolved_global_cache(), - ); - let parent_cache_config = task_effective_cache - .as_ref() - .map_or(ParentCacheConfig::Disabled, |config| { - ParentCacheConfig::Inherited(config.clone()) - }); - let spawn_execution = plan_synthetic_request( - context.workspace_path(), - &and_item.envs, - synthetic_plan_request, - Some(task_execution_cache_key), - &cwd, - package_path, - parent_cache_config, - )?; - ExecutionItemKind::Leaf(LeafExecutionKind::Spawn(spawn_execution)) - } - // Normal 3rd party tool command (like `tsc --noEmit`), using potentially mutated script_command - None => { - let program_path = which( - &OsStr::new(&script_command.program).into(), - &script_command.envs, - &script_command.cwd, - )?; - let (program_path, spawn_args) = crate::ps1_shim::rewrite_cmd_shim_with_args( - program_path, - script_command.args, - &task_node.resolved_config.resolved_options.cwd, - context.workspace_path(), - ); - let resolved_options = ResolvedTaskOptions { - cwd: Arc::clone(&task_node.resolved_config.resolved_options.cwd), - cache_config: effective_cache_config( + // Synthetic task (from CommandHandler) + Some(PlanRequest::Synthetic(synthetic_plan_request)) => { + let task_effective_cache = effective_cache_config( task_node.resolved_config.resolved_options.cache_config.as_ref(), task_node.source, *context.resolved_global_cache(), - ), - }; - let spawn_execution = plan_spawn_execution( - context.workspace_path(), - Some(task_execution_cache_key), - &and_item.envs, - &resolved_options, - &script_command.envs, - program_path, - spawn_args, - )?; - ExecutionItemKind::Leaf(LeafExecutionKind::Spawn(spawn_execution)) - } - }; - items.push(ExecutionItem { execution_item_display, kind: execution_item_kind }); - } - } else { - #[expect(clippy::disallowed_types, reason = "PathBuf needed for which fallback path")] - static SHELL_PROGRAM_PATH: LazyLock> = - LazyLock::new(|| { - if cfg!(target_os = "windows") { - AbsolutePathBuf::new(which::which("cmd.exe").unwrap_or_else(|_| { - std::path::PathBuf::from("C:\\Windows\\System32\\cmd.exe") - })) - .unwrap() - .into() - } else { - AbsolutePath::new("/bin/sh").unwrap().into() - } - }); + ); + let parent_cache_config = task_effective_cache + .as_ref() + .map_or(ParentCacheConfig::Disabled, |config| { + ParentCacheConfig::Inherited(config.clone()) + }); + let spawn_execution = plan_synthetic_request( + context.workspace_path(), + &and_item.envs, + synthetic_plan_request, + Some(task_execution_cache_key), + &cwd, + package_path, + parent_cache_config, + )?; + ExecutionItemKind::Leaf(LeafExecutionKind::Spawn(spawn_execution)) + } + // Normal 3rd party tool command (like `tsc --noEmit`), using potentially mutated script_command + None => { + let program_path = which( + &OsStr::new(&script_command.program).into(), + &script_command.envs, + &script_command.cwd, + )?; + let (program_path, spawn_args) = + crate::ps1_shim::rewrite_cmd_shim_with_args( + program_path, + script_command.args, + &task_node.resolved_config.resolved_options.cwd, + context.workspace_path(), + ); + let resolved_options = ResolvedTaskOptions { + cwd: Arc::clone(&task_node.resolved_config.resolved_options.cwd), + cache_config: effective_cache_config( + task_node.resolved_config.resolved_options.cache_config.as_ref(), + task_node.source, + *context.resolved_global_cache(), + ), + }; + let spawn_execution = plan_spawn_execution( + context.workspace_path(), + Some(task_execution_cache_key), + &and_item.envs, + &resolved_options, + &script_command.envs, + program_path, + spawn_args, + )?; + ExecutionItemKind::Leaf(LeafExecutionKind::Spawn(spawn_execution)) + } + }; + items.push(ExecutionItem { execution_item_display, kind: execution_item_kind }); + } + PlannedCommand::Shell(script) => { + #[expect( + clippy::disallowed_types, + reason = "PathBuf needed for which fallback path" + )] + static SHELL_PROGRAM_PATH: LazyLock> = LazyLock::new(|| { + if cfg!(target_os = "windows") { + AbsolutePathBuf::new(which::which("cmd.exe").unwrap_or_else(|_| { + std::path::PathBuf::from("C:\\Windows\\System32\\cmd.exe") + })) + .unwrap() + .into() + } else { + AbsolutePath::new("/bin/sh").unwrap().into() + } + }); - static SHELL_ARGS: &[&str] = - if cfg!(target_os = "windows") { &["/d", "/s", "/c"] } else { &["-c"] }; + static SHELL_ARGS: &[&str] = + if cfg!(target_os = "windows") { &["/d", "/s", "/c"] } else { &["-c"] }; - let mut context = context.duplicate(); - context.push_stack_frame(task_node_index, 0..command_str.len()); + let mut context = context.duplicate(); + context.push_stack_frame(task_node_index, 0..script.len()); - let execution_item_display = ExecutionItemDisplay { - command: command_str.into(), - and_item_index: None, - cwd, - task_display: task_node.task_display.clone(), - }; + let extra_args = if index == and_item_count - 1 { + Arc::clone(context.extra_args()) + } else { + Arc::new([]) + }; - let mut script = Str::from(command_str); - for arg in context.extra_args().iter() { - script.push(' '); - script.push_str(shell_escape::escape(arg.as_str().into()).as_ref()); - } + let execution_item_display = ExecutionItemDisplay { + command: script.clone(), + and_item_index: if and_item_count > 1 { Some(index) } else { None }, + cwd: Arc::clone(&cwd), + task_display: task_node.task_display.clone(), + }; - let resolved_options = ResolvedTaskOptions { - cwd: Arc::clone(&task_node.resolved_config.resolved_options.cwd), - cache_config: effective_cache_config( - task_node.resolved_config.resolved_options.cache_config.as_ref(), - task_node.source, - *context.resolved_global_cache(), - ), - }; - let spawn_execution = plan_spawn_execution( - context.workspace_path(), - Some(ExecutionCacheKey::UserTask { - task_name: task_node.task_display.task_name.clone(), - and_item_index: 0, - extra_args: Arc::clone(context.extra_args()), - package_path: strip_prefix_for_cache(package_path, context.workspace_path()) - .map_err(|kind| PathFingerprintError { - kind, - path_type: PathType::PackagePath, - })?, - }), - &BTreeMap::new(), - &resolved_options, - context.envs(), - Arc::clone(&*SHELL_PROGRAM_PATH), - SHELL_ARGS.iter().map(|s| Str::from(*s)).chain(std::iter::once(script)).collect(), - )?; - items.push(ExecutionItem { - execution_item_display, - kind: ExecutionItemKind::Leaf(LeafExecutionKind::Spawn(spawn_execution)), - }); + let mut script_with_args = script; + for arg in extra_args.iter() { + script_with_args.push(' '); + script_with_args.push_str(shell_escape::escape(arg.as_str().into()).as_ref()); + } + + let resolved_options = ResolvedTaskOptions { + cwd: Arc::clone(&task_node.resolved_config.resolved_options.cwd), + cache_config: effective_cache_config( + task_node.resolved_config.resolved_options.cache_config.as_ref(), + task_node.source, + *context.resolved_global_cache(), + ), + }; + let spawn_execution = plan_spawn_execution( + context.workspace_path(), + Some(ExecutionCacheKey::UserTask { + task_name: task_node.task_display.task_name.clone(), + and_item_index: index, + extra_args: Arc::clone(&extra_args), + package_path: strip_prefix_for_cache( + package_path, + context.workspace_path(), + ) + .map_err(|kind| PathFingerprintError { + kind, + path_type: PathType::PackagePath, + })?, + }), + &BTreeMap::new(), + &resolved_options, + context.envs(), + Arc::clone(&*SHELL_PROGRAM_PATH), + SHELL_ARGS + .iter() + .map(|s| Str::from(*s)) + .chain(std::iter::once(script_with_args)) + .collect(), + )?; + items.push(ExecutionItem { + execution_item_display, + kind: ExecutionItemKind::Leaf(LeafExecutionKind::Spawn(spawn_execution)), + }); + } + } } // Expand post-hook (`postX`) for package.json scripts. diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots.toml b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots.toml index f32eec089..bb9810c74 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots.toml +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots.toml @@ -6,6 +6,14 @@ args = ["run", "string_shorthand"] name = "array_shorthand" args = ["run", "array_shorthand"] +[[plan]] +name = "array_with_and" +args = ["run", "array_with_and"] + +[[plan]] +name = "nested_vt_array" +args = ["run", "nested_vt_array"] + [[plan]] name = "object_array_cache_false" args = ["run", "object_array_cache_false"] @@ -13,3 +21,11 @@ args = ["run", "object_array_cache_false"] [[plan]] name = "object_array_depends_on" args = ["run", "object_array_depends_on"] + +[[plan]] +name = "empty_array_error" +args = ["run", "empty_array"] + +[[plan]] +name = "empty_item_error" +args = ["run", "empty_item"] diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_with_and.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_with_and.jsonc new file mode 100644 index 000000000..dff16256d --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_with_and.jsonc @@ -0,0 +1,226 @@ +// run array_with_and +{ + "graph": [ + { + "key": [ + "/", + "array_with_and" + ], + "node": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "array_with_and", + "package_path": "/" + }, + "items": [ + { + "execution_item_display": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "array_with_and", + "package_path": "/" + }, + "command": "vtt print-file package.json", + "and_item_index": 0, + "cwd": "/" + }, + "kind": { + "Leaf": { + "Spawn": { + "cache_metadata": { + "spawn_fingerprint": { + "cwd": "", + "program_fingerprint": { + "OutsideWorkspace": { + "program_name": "vtt" + } + }, + "args": [ + "print-file", + "package.json" + ], + "env_fingerprints": { + "fingerprinted_envs": {}, + "untracked_env_config": [ + "" + ] + } + }, + "execution_cache_key": { + "UserTask": { + "task_name": "array_with_and", + "and_item_index": 0, + "extra_args": [], + "package_path": "" + } + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": false, + "positive_globs": [], + "negative_globs": [] + } + }, + "spawn_command": { + "program_path": "/vtt", + "args": [ + "print-file", + "package.json" + ], + "all_envs": { + "FORCE_COLOR": "1", + "PATH": "/node_modules/.bin:" + }, + "cwd": "/" + } + } + } + } + }, + { + "execution_item_display": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "array_with_and", + "package_path": "/" + }, + "command": "vtt print-file vite-task.json", + "and_item_index": 1, + "cwd": "/" + }, + "kind": { + "Leaf": { + "Spawn": { + "cache_metadata": { + "spawn_fingerprint": { + "cwd": "", + "program_fingerprint": { + "OutsideWorkspace": { + "program_name": "vtt" + } + }, + "args": [ + "print-file", + "vite-task.json" + ], + "env_fingerprints": { + "fingerprinted_envs": {}, + "untracked_env_config": [ + "" + ] + } + }, + "execution_cache_key": { + "UserTask": { + "task_name": "array_with_and", + "and_item_index": 1, + "extra_args": [], + "package_path": "" + } + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": false, + "positive_globs": [], + "negative_globs": [] + } + }, + "spawn_command": { + "program_path": "/vtt", + "args": [ + "print-file", + "vite-task.json" + ], + "all_envs": { + "FORCE_COLOR": "1", + "PATH": "/node_modules/.bin:" + }, + "cwd": "/" + } + } + } + } + }, + { + "execution_item_display": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "array_with_and", + "package_path": "/" + }, + "command": "vtt print-file package.json", + "and_item_index": 2, + "cwd": "/" + }, + "kind": { + "Leaf": { + "Spawn": { + "cache_metadata": { + "spawn_fingerprint": { + "cwd": "", + "program_fingerprint": { + "OutsideWorkspace": { + "program_name": "vtt" + } + }, + "args": [ + "print-file", + "package.json" + ], + "env_fingerprints": { + "fingerprinted_envs": {}, + "untracked_env_config": [ + "" + ] + } + }, + "execution_cache_key": { + "UserTask": { + "task_name": "array_with_and", + "and_item_index": 2, + "extra_args": [], + "package_path": "" + } + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": false, + "positive_globs": [], + "negative_globs": [] + } + }, + "spawn_command": { + "program_path": "/vtt", + "args": [ + "print-file", + "package.json" + ], + "all_envs": { + "FORCE_COLOR": "1", + "PATH": "/node_modules/.bin:" + }, + "cwd": "/" + } + } + } + } + } + ] + }, + "neighbors": [] + } + ], + "concurrency_limit": 4 +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_empty_array_error.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_empty_array_error.snap new file mode 100644 index 000000000..071fcf32d --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_empty_array_error.snap @@ -0,0 +1 @@ +Invalid task command: command array must not be empty \ No newline at end of file diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_empty_item_error.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_empty_item_error.snap new file mode 100644 index 000000000..db31e66b7 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_empty_item_error.snap @@ -0,0 +1 @@ +Invalid task command: command array entries must not be empty \ No newline at end of file diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_nested_vt_array.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_nested_vt_array.jsonc new file mode 100644 index 000000000..a8d1109c1 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_nested_vt_array.jsonc @@ -0,0 +1,193 @@ +// run nested_vt_array +{ + "graph": [ + { + "key": [ + "/", + "nested_vt_array" + ], + "node": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "nested_vt_array", + "package_path": "/" + }, + "items": [ + { + "execution_item_display": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "nested_vt_array", + "package_path": "/" + }, + "command": "vtt print-file package.json", + "and_item_index": 0, + "cwd": "/" + }, + "kind": { + "Leaf": { + "Spawn": { + "cache_metadata": { + "spawn_fingerprint": { + "cwd": "", + "program_fingerprint": { + "OutsideWorkspace": { + "program_name": "vtt" + } + }, + "args": [ + "print-file", + "package.json" + ], + "env_fingerprints": { + "fingerprinted_envs": {}, + "untracked_env_config": [ + "" + ] + } + }, + "execution_cache_key": { + "UserTask": { + "task_name": "nested_vt_array", + "and_item_index": 0, + "extra_args": [], + "package_path": "" + } + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": false, + "positive_globs": [], + "negative_globs": [] + } + }, + "spawn_command": { + "program_path": "/vtt", + "args": [ + "print-file", + "package.json" + ], + "all_envs": { + "FORCE_COLOR": "1", + "PATH": "/node_modules/.bin:" + }, + "cwd": "/" + } + } + } + } + }, + { + "execution_item_display": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "nested_vt_array", + "package_path": "/" + }, + "command": "vt run string_shorthand", + "and_item_index": 1, + "cwd": "/" + }, + "kind": { + "Expanded": { + "graph": [ + { + "key": [ + "/", + "string_shorthand" + ], + "node": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "string_shorthand", + "package_path": "/" + }, + "items": [ + { + "execution_item_display": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "string_shorthand", + "package_path": "/" + }, + "command": "vtt print-file package.json", + "and_item_index": null, + "cwd": "/" + }, + "kind": { + "Leaf": { + "Spawn": { + "cache_metadata": { + "spawn_fingerprint": { + "cwd": "", + "program_fingerprint": { + "OutsideWorkspace": { + "program_name": "vtt" + } + }, + "args": [ + "print-file", + "package.json" + ], + "env_fingerprints": { + "fingerprinted_envs": {}, + "untracked_env_config": [ + "" + ] + } + }, + "execution_cache_key": { + "UserTask": { + "task_name": "string_shorthand", + "and_item_index": 0, + "extra_args": [], + "package_path": "" + } + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": false, + "positive_globs": [], + "negative_globs": [] + } + }, + "spawn_command": { + "program_path": "/vtt", + "args": [ + "print-file", + "package.json" + ], + "all_envs": { + "FORCE_COLOR": "1", + "PATH": "/node_modules/.bin:" + }, + "cwd": "/" + } + } + } + } + } + ] + }, + "neighbors": [] + } + ], + "concurrency_limit": 4 + } + } + } + ] + }, + "neighbors": [] + } + ], + "concurrency_limit": 4 +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/task_graph.jsonc index 4c0dd63e5..ae835be90 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/task_graph.jsonc @@ -12,7 +12,176 @@ "package_path": "/" }, "resolved_config": { - "command": "vtt print-file package.json && vtt print-file vite-task.json && vtt print-file package.json", + "command": [ + "vtt print-file package.json", + "vtt print-file vite-task.json", + "vtt print-file package.json" + ], + "resolved_options": { + "cwd": "/", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": false, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "TaskConfig" + }, + "neighbors": [] + }, + { + "key": [ + "/", + "array_with_and" + ], + "node": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "array_with_and", + "package_path": "/" + }, + "resolved_config": { + "command": [ + "vtt print-file package.json", + "vtt print-file vite-task.json && vtt print-file package.json" + ], + "resolved_options": { + "cwd": "/", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": false, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "TaskConfig" + }, + "neighbors": [] + }, + { + "key": [ + "/", + "empty_array" + ], + "node": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "empty_array", + "package_path": "/" + }, + "resolved_config": { + "command": [], + "resolved_options": { + "cwd": "/", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": false, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "TaskConfig" + }, + "neighbors": [] + }, + { + "key": [ + "/", + "empty_item" + ], + "node": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "empty_item", + "package_path": "/" + }, + "resolved_config": { + "command": [ + "vtt print-file package.json", + "" + ], + "resolved_options": { + "cwd": "/", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": false, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "TaskConfig" + }, + "neighbors": [] + }, + { + "key": [ + "/", + "nested_vt_array" + ], + "node": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "nested_vt_array", + "package_path": "/" + }, + "resolved_config": { + "command": [ + "vtt print-file package.json", + "vt run string_shorthand" + ], "resolved_options": { "cwd": "/", "cache_config": { @@ -51,7 +220,11 @@ "package_path": "/" }, "resolved_config": { - "command": "vtt print-file package.json && vtt print-file vite-task.json && vtt print-file package.json", + "command": [ + "vtt print-file package.json", + "vtt print-file vite-task.json", + "vtt print-file package.json" + ], "resolved_options": { "cwd": "/", "cache_config": null @@ -73,7 +246,11 @@ "package_path": "/" }, "resolved_config": { - "command": "vtt print-file package.json && vtt print-file vite-task.json && vtt print-file package.json", + "command": [ + "vtt print-file package.json", + "vtt print-file vite-task.json", + "vtt print-file package.json" + ], "resolved_options": { "cwd": "/", "cache_config": { diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/vite-task.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/vite-task.json index f36439d39..aebb8684c 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/vite-task.json +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/vite-task.json @@ -2,6 +2,8 @@ "tasks": { "string_shorthand": "vtt print-file package.json", "array_shorthand": ["vtt print-file package.json", "vtt print-file vite-task.json", "vtt print-file package.json"], + "array_with_and": ["vtt print-file package.json", "vtt print-file vite-task.json && vtt print-file package.json"], + "nested_vt_array": ["vtt print-file package.json", "vt run string_shorthand"], "object_array_cache_false": { "command": ["vtt print-file package.json", "vtt print-file vite-task.json", "vtt print-file package.json"], "cache": false @@ -9,6 +11,8 @@ "object_array_depends_on": { "command": ["vtt print-file package.json", "vtt print-file vite-task.json", "vtt print-file package.json"], "dependsOn": ["string_shorthand"] - } + }, + "empty_array": [], + "empty_item": ["vtt print-file package.json", ""] } } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/package.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/package.json deleted file mode 100644 index c2b5e2749..000000000 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "@test/task-command-shorthands-cache-disabled", - "private": true -} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/snapshots.toml b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/snapshots.toml deleted file mode 100644 index 9bfaabf9d..000000000 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/snapshots.toml +++ /dev/null @@ -1,7 +0,0 @@ -[[plan]] -name = "string_shorthand_cache_disabled" -args = ["run", "string_shorthand"] - -[[plan]] -name = "array_shorthand_cache_disabled" -args = ["run", "array_shorthand"] diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/snapshots/query_array_shorthand_cache_disabled.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/snapshots/query_array_shorthand_cache_disabled.jsonc deleted file mode 100644 index 1f51a25f7..000000000 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/snapshots/query_array_shorthand_cache_disabled.jsonc +++ /dev/null @@ -1,84 +0,0 @@ -// run array_shorthand -{ - "graph": [ - { - "key": [ - "/", - "array_shorthand" - ], - "node": { - "task_display": { - "package_name": "@test/task-command-shorthands-cache-disabled", - "task_name": "array_shorthand", - "package_path": "/" - }, - "items": [ - { - "execution_item_display": { - "task_display": { - "package_name": "@test/task-command-shorthands-cache-disabled", - "task_name": "array_shorthand", - "package_path": "/" - }, - "command": "vtt print-file package.json", - "and_item_index": 0, - "cwd": "/" - }, - "kind": { - "Leaf": { - "Spawn": { - "cache_metadata": null, - "spawn_command": { - "program_path": "/vtt", - "args": [ - "print-file", - "package.json" - ], - "all_envs": { - "NO_COLOR": "1", - "PATH": "/node_modules/.bin:" - }, - "cwd": "/" - } - } - } - } - }, - { - "execution_item_display": { - "task_display": { - "package_name": "@test/task-command-shorthands-cache-disabled", - "task_name": "array_shorthand", - "package_path": "/" - }, - "command": "vtt print-file vite-task.json", - "and_item_index": 1, - "cwd": "/" - }, - "kind": { - "Leaf": { - "Spawn": { - "cache_metadata": null, - "spawn_command": { - "program_path": "/vtt", - "args": [ - "print-file", - "vite-task.json" - ], - "all_envs": { - "NO_COLOR": "1", - "PATH": "/node_modules/.bin:" - }, - "cwd": "/" - } - } - } - } - } - ] - }, - "neighbors": [] - } - ], - "concurrency_limit": 4 -} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/snapshots/query_string_shorthand_cache_disabled.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/snapshots/query_string_shorthand_cache_disabled.jsonc deleted file mode 100644 index c072a0f98..000000000 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/snapshots/query_string_shorthand_cache_disabled.jsonc +++ /dev/null @@ -1,53 +0,0 @@ -// run string_shorthand -{ - "graph": [ - { - "key": [ - "/", - "string_shorthand" - ], - "node": { - "task_display": { - "package_name": "@test/task-command-shorthands-cache-disabled", - "task_name": "string_shorthand", - "package_path": "/" - }, - "items": [ - { - "execution_item_display": { - "task_display": { - "package_name": "@test/task-command-shorthands-cache-disabled", - "task_name": "string_shorthand", - "package_path": "/" - }, - "command": "vtt print-file package.json", - "and_item_index": null, - "cwd": "/" - }, - "kind": { - "Leaf": { - "Spawn": { - "cache_metadata": null, - "spawn_command": { - "program_path": "/vtt", - "args": [ - "print-file", - "package.json" - ], - "all_envs": { - "NO_COLOR": "1", - "PATH": "/node_modules/.bin:" - }, - "cwd": "/" - } - } - } - } - } - ] - }, - "neighbors": [] - } - ], - "concurrency_limit": 4 -} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/snapshots/task_graph.jsonc deleted file mode 100644 index 237a5d824..000000000 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/snapshots/task_graph.jsonc +++ /dev/null @@ -1,81 +0,0 @@ -// task graph -[ - { - "key": [ - "/", - "array_shorthand" - ], - "node": { - "task_display": { - "package_name": "@test/task-command-shorthands-cache-disabled", - "task_name": "array_shorthand", - "package_path": "/" - }, - "resolved_config": { - "command": "vtt print-file package.json && vtt print-file vite-task.json", - "resolved_options": { - "cwd": "/", - "cache_config": { - "env_config": { - "fingerprinted_envs": [], - "untracked_env": [ - "" - ] - }, - "input_config": { - "includes_auto": true, - "positive_globs": [], - "negative_globs": [] - }, - "output_config": { - "includes_auto": false, - "positive_globs": [], - "negative_globs": [] - } - } - } - }, - "source": "TaskConfig" - }, - "neighbors": [] - }, - { - "key": [ - "/", - "string_shorthand" - ], - "node": { - "task_display": { - "package_name": "@test/task-command-shorthands-cache-disabled", - "task_name": "string_shorthand", - "package_path": "/" - }, - "resolved_config": { - "command": "vtt print-file package.json", - "resolved_options": { - "cwd": "/", - "cache_config": { - "env_config": { - "fingerprinted_envs": [], - "untracked_env": [ - "" - ] - }, - "input_config": { - "includes_auto": true, - "positive_globs": [], - "negative_globs": [] - }, - "output_config": { - "includes_auto": false, - "positive_globs": [], - "negative_globs": [] - } - } - } - }, - "source": "TaskConfig" - }, - "neighbors": [] - } -] diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/vite-task.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/vite-task.json deleted file mode 100644 index 9b75b53d4..000000000 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands_cache_disabled/vite-task.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "cache": { - "tasks": false - }, - "tasks": { - "string_shorthand": "vtt print-file package.json", - "array_shorthand": ["vtt print-file package.json", "vtt print-file vite-task.json"] - } -} From 32ded09d7256837089fb59b77a8110a8c360d66b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A2=85=EA=B2=BD?= Date: Tue, 19 May 2026 21:55:23 +0900 Subject: [PATCH 04/10] docs(changelog): add task command shorthand entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index add778222..0edabcde4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) From a73e602d73327647e3767bf276297382811276d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A2=85=EA=B2=BD?= Date: Tue, 19 May 2026 22:13:38 +0900 Subject: [PATCH 05/10] fix(plan): run post-cd commands from current cwd --- crates/vite_task_plan/src/plan.rs | 6 +- .../task_command_shorthands/snapshots.toml | 8 ++ .../snapshots/query_array_cd_shell.jsonc | 90 +++++++++++++++++++ .../snapshots/query_array_cd_spawn.jsonc | 90 +++++++++++++++++++ .../snapshots/task_graph.jsonc | 84 +++++++++++++++++ .../task_command_shorthands/vite-task.json | 2 + 6 files changed, 277 insertions(+), 3 deletions(-) create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_cd_shell.jsonc create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_cd_spawn.jsonc diff --git a/crates/vite_task_plan/src/plan.rs b/crates/vite_task_plan/src/plan.rs index dfff5673d..21fa882ce 100644 --- a/crates/vite_task_plan/src/plan.rs +++ b/crates/vite_task_plan/src/plan.rs @@ -326,11 +326,11 @@ async fn plan_task_as_execution_node( crate::ps1_shim::rewrite_cmd_shim_with_args( program_path, script_command.args, - &task_node.resolved_config.resolved_options.cwd, + &script_command.cwd, context.workspace_path(), ); let resolved_options = ResolvedTaskOptions { - cwd: Arc::clone(&task_node.resolved_config.resolved_options.cwd), + cwd: Arc::clone(&script_command.cwd), cache_config: effective_cache_config( task_node.resolved_config.resolved_options.cache_config.as_ref(), task_node.source, @@ -394,7 +394,7 @@ async fn plan_task_as_execution_node( } let resolved_options = ResolvedTaskOptions { - cwd: Arc::clone(&task_node.resolved_config.resolved_options.cwd), + cwd: Arc::clone(&cwd), cache_config: effective_cache_config( task_node.resolved_config.resolved_options.cache_config.as_ref(), task_node.source, diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots.toml b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots.toml index bb9810c74..de260daf1 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots.toml +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots.toml @@ -14,6 +14,14 @@ args = ["run", "array_with_and"] name = "nested_vt_array" args = ["run", "nested_vt_array"] +[[plan]] +name = "array_cd_spawn" +args = ["run", "array_cd_spawn"] + +[[plan]] +name = "array_cd_shell" +args = ["run", "array_cd_shell"] + [[plan]] name = "object_array_cache_false" args = ["run", "object_array_cache_false"] diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_cd_shell.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_cd_shell.jsonc new file mode 100644 index 000000000..eea951d41 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_cd_shell.jsonc @@ -0,0 +1,90 @@ +// run array_cd_shell +{ + "graph": [ + { + "key": [ + "/", + "array_cd_shell" + ], + "node": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "array_cd_shell", + "package_path": "/" + }, + "items": [ + { + "execution_item_display": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "array_cd_shell", + "package_path": "/" + }, + "command": "echo $PWD", + "and_item_index": 1, + "cwd": "/snapshots" + }, + "kind": { + "Leaf": { + "Spawn": { + "cache_metadata": { + "spawn_fingerprint": { + "cwd": "snapshots", + "program_fingerprint": { + "OutsideWorkspace": { + "program_name": "" + } + }, + "args": [ + "", + "echo $PWD" + ], + "env_fingerprints": { + "fingerprinted_envs": {}, + "untracked_env_config": [ + "" + ] + } + }, + "execution_cache_key": { + "UserTask": { + "task_name": "array_cd_shell", + "and_item_index": 1, + "extra_args": [], + "package_path": "" + } + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": false, + "positive_globs": [], + "negative_globs": [] + } + }, + "spawn_command": { + "program_path": "", + "args": [ + "", + "echo $PWD" + ], + "all_envs": { + "FORCE_COLOR": "1", + "PATH": "/node_modules/.bin:" + }, + "cwd": "/snapshots" + } + } + } + } + } + ] + }, + "neighbors": [] + } + ], + "concurrency_limit": 4 +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_cd_spawn.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_cd_spawn.jsonc new file mode 100644 index 000000000..570a21775 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_cd_spawn.jsonc @@ -0,0 +1,90 @@ +// run array_cd_spawn +{ + "graph": [ + { + "key": [ + "/", + "array_cd_spawn" + ], + "node": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "array_cd_spawn", + "package_path": "/" + }, + "items": [ + { + "execution_item_display": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "array_cd_spawn", + "package_path": "/" + }, + "command": "vtt print-file package.json", + "and_item_index": 1, + "cwd": "/snapshots" + }, + "kind": { + "Leaf": { + "Spawn": { + "cache_metadata": { + "spawn_fingerprint": { + "cwd": "snapshots", + "program_fingerprint": { + "OutsideWorkspace": { + "program_name": "vtt" + } + }, + "args": [ + "print-file", + "package.json" + ], + "env_fingerprints": { + "fingerprinted_envs": {}, + "untracked_env_config": [ + "" + ] + } + }, + "execution_cache_key": { + "UserTask": { + "task_name": "array_cd_spawn", + "and_item_index": 1, + "extra_args": [], + "package_path": "" + } + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": false, + "positive_globs": [], + "negative_globs": [] + } + }, + "spawn_command": { + "program_path": "/vtt", + "args": [ + "print-file", + "package.json" + ], + "all_envs": { + "FORCE_COLOR": "1", + "PATH": "/node_modules/.bin:" + }, + "cwd": "/snapshots" + } + } + } + } + } + ] + }, + "neighbors": [] + } + ], + "concurrency_limit": 4 +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/task_graph.jsonc index ae835be90..9dee80fdc 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/task_graph.jsonc @@ -1,5 +1,89 @@ // task graph [ + { + "key": [ + "/", + "array_cd_shell" + ], + "node": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "array_cd_shell", + "package_path": "/" + }, + "resolved_config": { + "command": [ + "cd snapshots", + "echo $PWD" + ], + "resolved_options": { + "cwd": "/", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": false, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "TaskConfig" + }, + "neighbors": [] + }, + { + "key": [ + "/", + "array_cd_spawn" + ], + "node": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "array_cd_spawn", + "package_path": "/" + }, + "resolved_config": { + "command": [ + "cd snapshots", + "vtt print-file package.json" + ], + "resolved_options": { + "cwd": "/", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": false, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "TaskConfig" + }, + "neighbors": [] + }, { "key": [ "/", diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/vite-task.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/vite-task.json index aebb8684c..cb08919b8 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/vite-task.json +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/vite-task.json @@ -4,6 +4,8 @@ "array_shorthand": ["vtt print-file package.json", "vtt print-file vite-task.json", "vtt print-file package.json"], "array_with_and": ["vtt print-file package.json", "vtt print-file vite-task.json && vtt print-file package.json"], "nested_vt_array": ["vtt print-file package.json", "vt run string_shorthand"], + "array_cd_spawn": ["cd snapshots", "vtt print-file package.json"], + "array_cd_shell": ["cd snapshots", "echo $PWD"], "object_array_cache_false": { "command": ["vtt print-file package.json", "vtt print-file vite-task.json", "vtt print-file package.json"], "cache": false From fc17f5041e3c430b320a2da8705e960a45397785 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A2=85=EA=B2=BD?= Date: Tue, 19 May 2026 22:42:58 +0900 Subject: [PATCH 06/10] fix(plan): reject shell cd before array continuations --- crates/vite_shell/src/lib.rs | 52 +++++++++++++++++++ crates/vite_task_plan/src/plan.rs | 14 ++++- .../task_command_shorthands/snapshots.toml | 4 ++ ...uery_array_shell_cd_before_next_error.snap | 1 + .../snapshots/task_graph.jsonc | 42 +++++++++++++++ .../task_command_shorthands/vite-task.json | 1 + 6 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_shell_cd_before_next_error.snap diff --git a/crates/vite_shell/src/lib.rs b/crates/vite_shell/src/lib.rs index 1bec3c9b2..f97120a8b 100644 --- a/crates/vite_shell/src/lib.rs +++ b/crates/vite_shell/src/lib.rs @@ -135,6 +135,45 @@ fn pipeline_to_command(pipeline: &Pipeline) -> Option<(TaskParsedCommand, Range< Some((TaskParsedCommand { envs, program: unquote(program)?, args }, range)) } +fn pipeline_program_is_cd(pipeline: &Pipeline) -> bool { + let Pipeline { timed: None, bang: false, seq } = pipeline else { + return false; + }; + let [Command::Simple(simple_command)] = seq.as_slice() else { + return false; + }; + let SimpleCommand { word_or_name: Some(program), .. } = simple_command else { + return false; + }; + unquote(program).is_some_and(|program| program.as_str() == "cd") +} + +#[must_use] +pub fn contains_cd_command(cmd: &str) -> bool { + let mut parser = Parser::new(cmd.as_bytes(), &PARSER_OPTIONS); + let Ok(Program { complete_commands }) = parser.parse_program() else { + return false; + }; + + for compound_list in &complete_commands { + for CompoundListItem(and_or_list, _) in &compound_list.0 { + if pipeline_program_is_cd(&and_or_list.first) { + return true; + } + for and_or in &and_or_list.additional { + let pipeline = match and_or { + AndOr::And(pipeline) | AndOr::Or(pipeline) => pipeline, + }; + if pipeline_program_is_cd(pipeline) { + return true; + } + } + } + } + + false +} + #[must_use] pub fn try_parse_as_and_list(cmd: &str) -> Option)>> { let mut parser = Parser::new(cmd.as_bytes(), &PARSER_OPTIONS); @@ -162,6 +201,19 @@ pub fn try_parse_as_and_list(cmd: &str) -> Option Result, Error> { + let mut array_len = None; let snippets: Box + '_> = match command { TaskCommand::String(command) => Box::new(std::iter::once(command)), TaskCommand::Array(commands) => { @@ -99,12 +100,13 @@ fn planned_commands(command: &TaskCommand) -> Result, Error> "command array entries must not be empty".into(), )); } + array_len = Some(commands.len()); Box::new(commands.iter()) } }; let mut planned = Vec::new(); - for snippet in snippets { + for (snippet_index, snippet) in snippets.enumerate() { if let Some(parsed) = try_parse_as_and_list(snippet.as_str()) { for (and_item, range) in parsed { planned.push(PlannedCommand::Parsed { @@ -114,6 +116,14 @@ fn planned_commands(command: &TaskCommand) -> Result, Error> }); } } else { + if array_len.is_some_and(|len| snippet_index + 1 < len) + && contains_cd_command(snippet.as_str()) + { + return Err(Error::InvalidTaskCommand( + "command array entries that change directory in a shell must be the final entry" + .into(), + )); + } planned.push(PlannedCommand::Shell(snippet.clone())); } } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots.toml b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots.toml index de260daf1..1a4115365 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots.toml +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots.toml @@ -22,6 +22,10 @@ args = ["run", "array_cd_spawn"] name = "array_cd_shell" args = ["run", "array_cd_shell"] +[[plan]] +name = "array_shell_cd_before_next_error" +args = ["run", "array_shell_cd_before_next"] + [[plan]] name = "object_array_cache_false" args = ["run", "object_array_cache_false"] diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_shell_cd_before_next_error.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_shell_cd_before_next_error.snap new file mode 100644 index 000000000..6552f5572 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_shell_cd_before_next_error.snap @@ -0,0 +1 @@ +Invalid task command: command array entries that change directory in a shell must be the final entry \ No newline at end of file diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/task_graph.jsonc index 9dee80fdc..806c895dc 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/task_graph.jsonc @@ -84,6 +84,48 @@ }, "neighbors": [] }, + { + "key": [ + "/", + "array_shell_cd_before_next" + ], + "node": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "array_shell_cd_before_next", + "package_path": "/" + }, + "resolved_config": { + "command": [ + "cd \"$APP_DIR\"", + "vtt print-file package.json" + ], + "resolved_options": { + "cwd": "/", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": false, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "TaskConfig" + }, + "neighbors": [] + }, { "key": [ "/", diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/vite-task.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/vite-task.json index cb08919b8..d2fc8ae54 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/vite-task.json +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/vite-task.json @@ -6,6 +6,7 @@ "nested_vt_array": ["vtt print-file package.json", "vt run string_shorthand"], "array_cd_spawn": ["cd snapshots", "vtt print-file package.json"], "array_cd_shell": ["cd snapshots", "echo $PWD"], + "array_shell_cd_before_next": ["cd \"$APP_DIR\"", "vtt print-file package.json"], "object_array_cache_false": { "command": ["vtt print-file package.json", "vtt print-file vite-task.json", "vtt print-file package.json"], "cache": false From 455b93b43474ac06c0b66b31b5c904e0a2eaba40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A2=85=EA=B2=BD?= Date: Tue, 19 May 2026 23:39:29 +0900 Subject: [PATCH 07/10] fix(plan): reject compound shell cd before array continuations --- crates/vite_shell/src/lib.rs | 150 ++++++++++++++---- crates/vite_task_plan/src/plan.rs | 8 +- .../task_command_shorthands/snapshots.toml | 4 + ...y_array_compound_cd_before_next_error.snap | 1 + ...uery_array_shell_cd_before_next_error.snap | 2 +- .../snapshots/task_graph.jsonc | 42 +++++ .../task_command_shorthands/vite-task.json | 1 + 7 files changed, 170 insertions(+), 38 deletions(-) create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_compound_cd_before_next_error.snap diff --git a/crates/vite_shell/src/lib.rs b/crates/vite_shell/src/lib.rs index f97120a8b..b7ad278b1 100644 --- a/crates/vite_shell/src/lib.rs +++ b/crates/vite_shell/src/lib.rs @@ -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}, }; @@ -135,43 +135,115 @@ fn pipeline_to_command(pipeline: &Pipeline) -> Option<(TaskParsedCommand, Range< Some((TaskParsedCommand { envs, program: unquote(program)?, args }, range)) } -fn pipeline_program_is_cd(pipeline: &Pipeline) -> bool { - let Pipeline { timed: None, bang: false, seq } = pipeline else { +fn command_name_may_change_cwd(command_name: &str) -> bool { + matches!(command_name, "cd" | "chdir" | "." | "source" | "eval") +} + +fn wrapper_command_may_change_cwd(suffix: Option<&CommandSuffix>) -> bool { + let Some(CommandSuffix(suffix_items)) = suffix else { return false; }; - let [Command::Simple(simple_command)] = seq.as_slice() else { + + 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 SimpleCommand { word_or_name: Some(program), .. } = simple_command else { - return false; + let Some(program) = unquote(program) else { + return true; }; - unquote(program).is_some_and(|program| program.as_str() == "cd") + + 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 contains_cd_command(cmd: &str) -> bool { +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 false; + return true; }; - for compound_list in &complete_commands { - for CompoundListItem(and_or_list, _) in &compound_list.0 { - if pipeline_program_is_cd(&and_or_list.first) { - return true; - } - for and_or in &and_or_list.additional { - let pipeline = match and_or { - AndOr::And(pipeline) | AndOr::Or(pipeline) => pipeline, - }; - if pipeline_program_is_cd(pipeline) { - return true; - } - } - } - } - - false + complete_commands.iter().any(compound_list_may_change_cwd) } #[must_use] @@ -202,16 +274,26 @@ mod tests { use super::*; #[test] - fn test_contains_cd_command_with_unresolved_arg() { - assert!(contains_cd_command(r#"cd "$APP_DIR""#)); - assert!(contains_cd_command(r#"echo ok && cd "$APP_DIR""#)); - assert!(contains_cd_command(r#"FOO=bar 'cd' "$APP_DIR""#)); + 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_contains_cd_command_ignores_cd_argument_text() { - assert!(!contains_cd_command(r#"echo "cd $APP_DIR""#)); - assert!(!contains_cd_command("cdtool $APP_DIR")); + 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] diff --git a/crates/vite_task_plan/src/plan.rs b/crates/vite_task_plan/src/plan.rs index 0f8a56909..5a737f815 100644 --- a/crates/vite_task_plan/src/plan.rs +++ b/crates/vite_task_plan/src/plan.rs @@ -16,7 +16,7 @@ use futures_util::FutureExt; use petgraph::Direction; use rustc_hash::FxHashMap; use vite_path::{AbsolutePath, AbsolutePathBuf, RelativePathBuf, relative::InvalidPathDataError}; -use vite_shell::{TaskParsedCommand, contains_cd_command, try_parse_as_and_list}; +use vite_shell::{TaskParsedCommand, shell_command_may_change_cwd, try_parse_as_and_list}; use vite_str::Str; use vite_task_graph::{ TaskNodeIndex, TaskSource, @@ -116,11 +116,13 @@ fn planned_commands(command: &TaskCommand) -> Result, Error> }); } } else { + // A shell fallback runs in a child shell, so any cwd change inside it cannot be + // reflected in the planner's cwd for following array entries. if array_len.is_some_and(|len| snippet_index + 1 < len) - && contains_cd_command(snippet.as_str()) + && shell_command_may_change_cwd(snippet.as_str()) { return Err(Error::InvalidTaskCommand( - "command array entries that change directory in a shell must be the final entry" + "command array entries that may change directory in a shell must be the final entry" .into(), )); } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots.toml b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots.toml index 1a4115365..a87bf5fd7 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots.toml +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots.toml @@ -26,6 +26,10 @@ args = ["run", "array_cd_shell"] name = "array_shell_cd_before_next_error" args = ["run", "array_shell_cd_before_next"] +[[plan]] +name = "array_compound_cd_before_next_error" +args = ["run", "array_compound_cd_before_next"] + [[plan]] name = "object_array_cache_false" args = ["run", "object_array_cache_false"] diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_compound_cd_before_next_error.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_compound_cd_before_next_error.snap new file mode 100644 index 000000000..a3c22784f --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_compound_cd_before_next_error.snap @@ -0,0 +1 @@ +Invalid task command: command array entries that may change directory in a shell must be the final entry \ No newline at end of file diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_shell_cd_before_next_error.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_shell_cd_before_next_error.snap index 6552f5572..a3c22784f 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_shell_cd_before_next_error.snap +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_shell_cd_before_next_error.snap @@ -1 +1 @@ -Invalid task command: command array entries that change directory in a shell must be the final entry \ No newline at end of file +Invalid task command: command array entries that may change directory in a shell must be the final entry \ No newline at end of file diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/task_graph.jsonc index 806c895dc..3e782d59d 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/task_graph.jsonc @@ -84,6 +84,48 @@ }, "neighbors": [] }, + { + "key": [ + "/", + "array_compound_cd_before_next" + ], + "node": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "array_compound_cd_before_next", + "package_path": "/" + }, + "resolved_config": { + "command": [ + "if true; then cd snapshots; fi", + "vtt print-file package.json" + ], + "resolved_options": { + "cwd": "/", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "untracked_env": [ + "" + ] + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": false, + "positive_globs": [], + "negative_globs": [] + } + } + } + }, + "source": "TaskConfig" + }, + "neighbors": [] + }, { "key": [ "/", diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/vite-task.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/vite-task.json index d2fc8ae54..a973cd3bd 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/vite-task.json +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/vite-task.json @@ -7,6 +7,7 @@ "array_cd_spawn": ["cd snapshots", "vtt print-file package.json"], "array_cd_shell": ["cd snapshots", "echo $PWD"], "array_shell_cd_before_next": ["cd \"$APP_DIR\"", "vtt print-file package.json"], + "array_compound_cd_before_next": ["if true; then cd snapshots; fi", "vtt print-file package.json"], "object_array_cache_false": { "command": ["vtt print-file package.json", "vtt print-file vite-task.json", "vtt print-file package.json"], "cache": false From 0958327ca46ce84f927cc8e5710fc3bf20aca162 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A2=85=EA=B2=BD?= Date: Wed, 20 May 2026 00:14:31 +0900 Subject: [PATCH 08/10] refactor(shell): model cwd-changing commands --- crates/vite_shell/src/lib.rs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/crates/vite_shell/src/lib.rs b/crates/vite_shell/src/lib.rs index b7ad278b1..301e7d19d 100644 --- a/crates/vite_shell/src/lib.rs +++ b/crates/vite_shell/src/lib.rs @@ -135,8 +135,30 @@ 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 { + 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 { - matches!(command_name, "cd" | "chdir" | "." | "source" | "eval") + CwdChangingCommand::from_name(command_name).is_some() } fn wrapper_command_may_change_cwd(suffix: Option<&CommandSuffix>) -> bool { From a5511356b817c488ac46b60455b7c119e04e494c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A2=85=EA=B2=BD?= Date: Thu, 21 May 2026 10:06:39 +0900 Subject: [PATCH 09/10] fix(plan): normalize command arrays through existing shell path Join command array entries with && before planning so string[] shorthands preserve existing shell semantics instead of relying on partial cwd-change detection. --- crates/vite_shell/src/lib.rs | 160 +----------------- crates/vite_task_plan/src/plan.rs | 59 +++---- .../task_command_shorthands/snapshots.toml | 4 +- .../snapshots/query_array_cd_shell.jsonc | 16 +- .../query_array_compound_cd_before_next.jsonc | 90 ++++++++++ ...y_array_compound_cd_before_next_error.snap | 1 - .../query_array_shell_cd_before_next.jsonc | 90 ++++++++++ ...uery_array_shell_cd_before_next_error.snap | 1 - 8 files changed, 220 insertions(+), 201 deletions(-) create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_compound_cd_before_next.jsonc delete mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_compound_cd_before_next_error.snap create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_shell_cd_before_next.jsonc delete mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_shell_cd_before_next_error.snap diff --git a/crates/vite_shell/src/lib.rs b/crates/vite_shell/src/lib.rs index 301e7d19d..1bec3c9b2 100644 --- a/crates/vite_shell/src/lib.rs +++ b/crates/vite_shell/src/lib.rs @@ -4,8 +4,8 @@ use brush_parser::{ Parser, ParserOptions, ast::{ AndOr, Assignment, AssignmentName, AssignmentValue, Command, CommandPrefix, - CommandPrefixOrSuffixItem, CommandSuffix, CompoundCommand, CompoundList, CompoundListItem, - DoGroupCommand, Pipeline, Program, SeparatorOperator, SimpleCommand, SourceLocation, Word, + CommandPrefixOrSuffixItem, CommandSuffix, CompoundListItem, Pipeline, Program, + SeparatorOperator, SimpleCommand, SourceLocation, Word, }, word::{WordPiece, WordPieceWithSource}, }; @@ -135,139 +135,6 @@ 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 { - 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)>> { let mut parser = Parser::new(cmd.as_bytes(), &PARSER_OPTIONS); @@ -295,29 +162,6 @@ pub fn try_parse_as_and_list(cmd: &str) -> Option Result, Error> { - let mut array_len = None; - let snippets: Box + '_> = match command { - TaskCommand::String(command) => Box::new(std::iter::once(command)), +fn command_source(command: &TaskCommand) -> Result { + match command { + TaskCommand::String(command) => Ok(command.clone()), TaskCommand::Array(commands) => { if commands.is_empty() { return Err(Error::InvalidTaskCommand("command array must not be empty".into())); @@ -100,36 +99,34 @@ fn planned_commands(command: &TaskCommand) -> Result, Error> "command array entries must not be empty".into(), )); } - array_len = Some(commands.len()); - Box::new(commands.iter()) - } - }; - let mut planned = Vec::new(); - for (snippet_index, snippet) in snippets.enumerate() { - if let Some(parsed) = try_parse_as_and_list(snippet.as_str()) { - for (and_item, range) in parsed { - planned.push(PlannedCommand::Parsed { - display: Str::from(&snippet.as_str()[range.clone()]), - and_item, - stack_frame: range, - }); - } - } else { - // A shell fallback runs in a child shell, so any cwd change inside it cannot be - // reflected in the planner's cwd for following array entries. - if array_len.is_some_and(|len| snippet_index + 1 < len) - && shell_command_may_change_cwd(snippet.as_str()) - { - return Err(Error::InvalidTaskCommand( - "command array entries that may change directory in a shell must be the final entry" - .into(), - )); + let mut source = Str::default(); + for (index, command) in commands.iter().enumerate() { + if index > 0 { + source.push_str(" && "); + } + source.push_str(command.as_str()); } - planned.push(PlannedCommand::Shell(snippet.clone())); + Ok(source) } } - Ok(planned) +} + +#[expect(clippy::result_large_err, reason = "Error is large for diagnostics")] +fn planned_commands(command: &TaskCommand) -> Result, Error> { + let source = command_source(command)?; + if let Some(parsed) = try_parse_as_and_list(source.as_str()) { + Ok(parsed + .into_iter() + .map(|(and_item, range)| PlannedCommand::Parsed { + display: Str::from(&source.as_str()[range.clone()]), + and_item, + stack_frame: range, + }) + .collect()) + } else { + Ok(vec![PlannedCommand::Shell(source)]) + } } /// - `with_hooks`: whether to look up `preX`/`postX` lifecycle hooks for this task. diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots.toml b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots.toml index a87bf5fd7..cae6aebd3 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots.toml +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots.toml @@ -23,11 +23,11 @@ name = "array_cd_shell" args = ["run", "array_cd_shell"] [[plan]] -name = "array_shell_cd_before_next_error" +name = "array_shell_cd_before_next" args = ["run", "array_shell_cd_before_next"] [[plan]] -name = "array_compound_cd_before_next_error" +name = "array_compound_cd_before_next" args = ["run", "array_compound_cd_before_next"] [[plan]] diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_cd_shell.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_cd_shell.jsonc index eea951d41..5beddf00f 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_cd_shell.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_cd_shell.jsonc @@ -20,16 +20,16 @@ "task_name": "array_cd_shell", "package_path": "/" }, - "command": "echo $PWD", - "and_item_index": 1, - "cwd": "/snapshots" + "command": "cd snapshots && echo $PWD", + "and_item_index": null, + "cwd": "/" }, "kind": { "Leaf": { "Spawn": { "cache_metadata": { "spawn_fingerprint": { - "cwd": "snapshots", + "cwd": "", "program_fingerprint": { "OutsideWorkspace": { "program_name": "" @@ -37,7 +37,7 @@ }, "args": [ "", - "echo $PWD" + "cd snapshots && echo $PWD" ], "env_fingerprints": { "fingerprinted_envs": {}, @@ -49,7 +49,7 @@ "execution_cache_key": { "UserTask": { "task_name": "array_cd_shell", - "and_item_index": 1, + "and_item_index": 0, "extra_args": [], "package_path": "" } @@ -69,13 +69,13 @@ "program_path": "", "args": [ "", - "echo $PWD" + "cd snapshots && echo $PWD" ], "all_envs": { "FORCE_COLOR": "1", "PATH": "/node_modules/.bin:" }, - "cwd": "/snapshots" + "cwd": "/" } } } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_compound_cd_before_next.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_compound_cd_before_next.jsonc new file mode 100644 index 000000000..eb34730e1 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_compound_cd_before_next.jsonc @@ -0,0 +1,90 @@ +// run array_compound_cd_before_next +{ + "graph": [ + { + "key": [ + "/", + "array_compound_cd_before_next" + ], + "node": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "array_compound_cd_before_next", + "package_path": "/" + }, + "items": [ + { + "execution_item_display": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "array_compound_cd_before_next", + "package_path": "/" + }, + "command": "if true; then cd snapshots; fi && vtt print-file package.json", + "and_item_index": null, + "cwd": "/" + }, + "kind": { + "Leaf": { + "Spawn": { + "cache_metadata": { + "spawn_fingerprint": { + "cwd": "", + "program_fingerprint": { + "OutsideWorkspace": { + "program_name": "" + } + }, + "args": [ + "", + "if true; then cd snapshots; fi && vtt print-file package.json" + ], + "env_fingerprints": { + "fingerprinted_envs": {}, + "untracked_env_config": [ + "" + ] + } + }, + "execution_cache_key": { + "UserTask": { + "task_name": "array_compound_cd_before_next", + "and_item_index": 0, + "extra_args": [], + "package_path": "" + } + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": false, + "positive_globs": [], + "negative_globs": [] + } + }, + "spawn_command": { + "program_path": "", + "args": [ + "", + "if true; then cd snapshots; fi && vtt print-file package.json" + ], + "all_envs": { + "FORCE_COLOR": "1", + "PATH": "/node_modules/.bin:" + }, + "cwd": "/" + } + } + } + } + } + ] + }, + "neighbors": [] + } + ], + "concurrency_limit": 4 +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_compound_cd_before_next_error.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_compound_cd_before_next_error.snap deleted file mode 100644 index a3c22784f..000000000 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_compound_cd_before_next_error.snap +++ /dev/null @@ -1 +0,0 @@ -Invalid task command: command array entries that may change directory in a shell must be the final entry \ No newline at end of file diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_shell_cd_before_next.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_shell_cd_before_next.jsonc new file mode 100644 index 000000000..4f3c36c18 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_shell_cd_before_next.jsonc @@ -0,0 +1,90 @@ +// run array_shell_cd_before_next +{ + "graph": [ + { + "key": [ + "/", + "array_shell_cd_before_next" + ], + "node": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "array_shell_cd_before_next", + "package_path": "/" + }, + "items": [ + { + "execution_item_display": { + "task_display": { + "package_name": "@test/task-command-shorthands", + "task_name": "array_shell_cd_before_next", + "package_path": "/" + }, + "command": "cd \"$APP_DIR\" && vtt print-file package.json", + "and_item_index": null, + "cwd": "/" + }, + "kind": { + "Leaf": { + "Spawn": { + "cache_metadata": { + "spawn_fingerprint": { + "cwd": "", + "program_fingerprint": { + "OutsideWorkspace": { + "program_name": "" + } + }, + "args": [ + "", + "cd \"$APP_DIR\" && vtt print-file package.json" + ], + "env_fingerprints": { + "fingerprinted_envs": {}, + "untracked_env_config": [ + "" + ] + } + }, + "execution_cache_key": { + "UserTask": { + "task_name": "array_shell_cd_before_next", + "and_item_index": 0, + "extra_args": [], + "package_path": "" + } + }, + "input_config": { + "includes_auto": true, + "positive_globs": [], + "negative_globs": [] + }, + "output_config": { + "includes_auto": false, + "positive_globs": [], + "negative_globs": [] + } + }, + "spawn_command": { + "program_path": "", + "args": [ + "", + "cd \"$APP_DIR\" && vtt print-file package.json" + ], + "all_envs": { + "FORCE_COLOR": "1", + "PATH": "/node_modules/.bin:" + }, + "cwd": "/" + } + } + } + } + } + ] + }, + "neighbors": [] + } + ], + "concurrency_limit": 4 +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_shell_cd_before_next_error.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_shell_cd_before_next_error.snap deleted file mode 100644 index a3c22784f..000000000 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_array_shell_cd_before_next_error.snap +++ /dev/null @@ -1 +0,0 @@ -Invalid task command: command array entries that may change directory in a shell must be the final entry \ No newline at end of file From 2f99de1423364376d95339640f12038fb0c2ae96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A2=85=EA=B2=BD?= Date: Thu, 21 May 2026 10:27:50 +0900 Subject: [PATCH 10/10] test(plan): remove redundant array cache snapshot Drop the explicit cache:false command-array plan snapshot now that cache behavior is covered elsewhere and this fixture can stay focused on shorthand planning semantics. --- .../task_command_shorthands/snapshots.toml | 4 - .../query_object_array_cache_false.jsonc | 115 ------------------ .../snapshots/task_graph.jsonc | 26 ---- .../task_command_shorthands/vite-task.json | 4 - 4 files changed, 149 deletions(-) delete mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_object_array_cache_false.jsonc diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots.toml b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots.toml index cae6aebd3..14243a2ab 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots.toml +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots.toml @@ -30,10 +30,6 @@ args = ["run", "array_shell_cd_before_next"] name = "array_compound_cd_before_next" args = ["run", "array_compound_cd_before_next"] -[[plan]] -name = "object_array_cache_false" -args = ["run", "object_array_cache_false"] - [[plan]] name = "object_array_depends_on" args = ["run", "object_array_depends_on"] diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_object_array_cache_false.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_object_array_cache_false.jsonc deleted file mode 100644 index 810dc4e25..000000000 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/query_object_array_cache_false.jsonc +++ /dev/null @@ -1,115 +0,0 @@ -// run object_array_cache_false -{ - "graph": [ - { - "key": [ - "/", - "object_array_cache_false" - ], - "node": { - "task_display": { - "package_name": "@test/task-command-shorthands", - "task_name": "object_array_cache_false", - "package_path": "/" - }, - "items": [ - { - "execution_item_display": { - "task_display": { - "package_name": "@test/task-command-shorthands", - "task_name": "object_array_cache_false", - "package_path": "/" - }, - "command": "vtt print-file package.json", - "and_item_index": 0, - "cwd": "/" - }, - "kind": { - "Leaf": { - "Spawn": { - "cache_metadata": null, - "spawn_command": { - "program_path": "/vtt", - "args": [ - "print-file", - "package.json" - ], - "all_envs": { - "NO_COLOR": "1", - "PATH": "/node_modules/.bin:" - }, - "cwd": "/" - } - } - } - } - }, - { - "execution_item_display": { - "task_display": { - "package_name": "@test/task-command-shorthands", - "task_name": "object_array_cache_false", - "package_path": "/" - }, - "command": "vtt print-file vite-task.json", - "and_item_index": 1, - "cwd": "/" - }, - "kind": { - "Leaf": { - "Spawn": { - "cache_metadata": null, - "spawn_command": { - "program_path": "/vtt", - "args": [ - "print-file", - "vite-task.json" - ], - "all_envs": { - "NO_COLOR": "1", - "PATH": "/node_modules/.bin:" - }, - "cwd": "/" - } - } - } - } - }, - { - "execution_item_display": { - "task_display": { - "package_name": "@test/task-command-shorthands", - "task_name": "object_array_cache_false", - "package_path": "/" - }, - "command": "vtt print-file package.json", - "and_item_index": 2, - "cwd": "/" - }, - "kind": { - "Leaf": { - "Spawn": { - "cache_metadata": null, - "spawn_command": { - "program_path": "/vtt", - "args": [ - "print-file", - "package.json" - ], - "all_envs": { - "NO_COLOR": "1", - "PATH": "/node_modules/.bin:" - }, - "cwd": "/" - } - } - } - } - } - ] - }, - "neighbors": [] - } - ], - "concurrency_limit": 4 -} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/task_graph.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/task_graph.jsonc index 3e782d59d..c7ad2eab1 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/task_graph.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/snapshots/task_graph.jsonc @@ -376,32 +376,6 @@ }, "neighbors": [] }, - { - "key": [ - "/", - "object_array_cache_false" - ], - "node": { - "task_display": { - "package_name": "@test/task-command-shorthands", - "task_name": "object_array_cache_false", - "package_path": "/" - }, - "resolved_config": { - "command": [ - "vtt print-file package.json", - "vtt print-file vite-task.json", - "vtt print-file package.json" - ], - "resolved_options": { - "cwd": "/", - "cache_config": null - } - }, - "source": "TaskConfig" - }, - "neighbors": [] - }, { "key": [ "/", diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/vite-task.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/vite-task.json index a973cd3bd..68e06f6a4 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/vite-task.json +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/task_command_shorthands/vite-task.json @@ -8,10 +8,6 @@ "array_cd_shell": ["cd snapshots", "echo $PWD"], "array_shell_cd_before_next": ["cd \"$APP_DIR\"", "vtt print-file package.json"], "array_compound_cd_before_next": ["if true; then cd snapshots; fi", "vtt print-file package.json"], - "object_array_cache_false": { - "command": ["vtt print-file package.json", "vtt print-file vite-task.json", "vtt print-file package.json"], - "cache": false - }, "object_array_depends_on": { "command": ["vtt print-file package.json", "vtt print-file vite-task.json", "vtt print-file package.json"], "dependsOn": ["string_shorthand"]