From 7f054d7e0d7eb646e8fb1dc746b89e8d3889eba2 Mon Sep 17 00:00:00 2001 From: branchseer Date: Wed, 4 Feb 2026 14:51:22 +0800 Subject: [PATCH 01/10] refactor: simplify CLI and Session API by removing generic type parameters Remove generic CustomSubcommand type parameter from Session, SessionCallbacks, PlanRequestParser, and TaskSynthesizer. The CLI only supports `run` as a top-level subcommand; synthetic tasks (lint, test, env-test) are now handled purely by TaskSynthesizer via string matching on program name and args. Key changes: - TaskSynthesizer trait takes (program, args) directly instead of a typed subcommand - Session/SessionCallbacks/PlanRequestParser no longer parameterized by subcommand type - plan_from_cli accepts BuiltInCommand directly (removed CLIArgs, TaskCLIArgs, ParsedTaskCLIArgs) - Binary crate owns its top-level Cli parser wrapping BuiltInCommand - E2E test fixtures converted from `vite lint`/`vite env-test` to `vite run ` - Removed same-name-as-builtin fixture (tested now-removed CLI lint vs run distinction) - Added diagnostic block sorting in E2E output redaction for deterministic snapshots Co-Authored-By: Claude Opus 4.5 --- Cargo.lock | 1 + crates/vite_task/docs/task-cache.md | 14 +- crates/vite_task/src/cli/mod.rs | 84 +---------- crates/vite_task/src/lib.rs | 2 +- crates/vite_task/src/session/execute/mod.rs | 2 +- crates/vite_task/src/session/mod.rs | 130 ++++++++---------- crates/vite_task/src/session/reporter.rs | 6 +- crates/vite_task_bin/src/lib.rs | 109 +++++++-------- crates/vite_task_bin/src/main.rs | 33 ++--- .../builtin-different-cwd/package.json | 6 +- .../builtin-different-cwd/snapshots.toml | 10 +- .../snapshots/builtin different cwd.snap | 109 ++++++++++++--- .../builtin-non-zero-exit/package.json | 5 +- .../builtin-non-zero-exit/snapshots.toml | 4 +- ... exit does not show cache not updated.snap | 33 ++++- .../fixtures/e2e-env-test/package.json | 5 +- .../fixtures/e2e-env-test/snapshots.toml | 6 +- ...est prints value from additional_envs.snap | 18 ++- .../env-test with different values.snap | 34 ++++- .../fixtures/e2e-lint-cache/package.json | 5 +- .../fixtures/e2e-lint-cache/snapshots.toml | 4 +- .../e2e-lint-cache/snapshots/direct lint.snap | 34 ++++- .../fixtures/lint-dot-git/package.json | 6 +- .../fixtures/lint-dot-git/snapshots.toml | 4 +- .../lint-dot-git/snapshots/lint dot git.snap | 35 ++++- .../fixtures/same-name-as-builtin/a.js | 1 - .../same-name-as-builtin/package.json | 5 - .../same-name-as-builtin/snapshots.toml | 11 -- .../snapshots/same name as builtin.snap | 61 -------- .../tests/e2e_snapshots/redact.rs | 48 +++++++ crates/vite_task_plan/Cargo.toml | 1 + crates/vite_task_plan/README.md | 6 +- crates/vite_task_plan/src/cache_metadata.rs | 7 +- crates/vite_task_plan/src/lib.rs | 2 +- crates/vite_task_plan/src/plan.rs | 2 +- crates/vite_task_plan/src/plan_request.rs | 4 +- .../fixtures/additional-envs/package.json | 3 +- .../fixtures/additional-envs/snapshots.toml | 17 +-- ...uery - direct env-test synthetic task.snap | 58 -------- ... env-test synthetic task in user task.snap | 88 ++++++++++++ .../additional-envs/snapshots/task graph.snap | 32 ++++- .../fixtures/cache-keys/snapshots.toml | 16 +-- ...uery - direct synthetic task with cwd.snap | 50 ------- ...direct synthetic task with extra args.snap | 55 -------- .../query - direct synthetic task.snap | 50 ------- ...query - echo and lint with extra args.snap | 4 +- ...query - lint and echo with extra args.snap | 4 +- .../query - normal task with extra args.snap | 4 +- ... synthetic task in user task with cwd.snap | 82 +++++++++++ .../query - synthetic task in user task.snap | 4 +- ...tic task with extra args in user task.snap | 4 +- .../cache-keys/snapshots/task graph.snap | 4 +- .../tests/plan_snapshots/main.rs | 26 ++-- 53 files changed, 696 insertions(+), 652 deletions(-) delete mode 100644 crates/vite_task_bin/tests/e2e_snapshots/fixtures/same-name-as-builtin/a.js delete mode 100644 crates/vite_task_bin/tests/e2e_snapshots/fixtures/same-name-as-builtin/package.json delete mode 100644 crates/vite_task_bin/tests/e2e_snapshots/fixtures/same-name-as-builtin/snapshots.toml delete mode 100644 crates/vite_task_bin/tests/e2e_snapshots/fixtures/same-name-as-builtin/snapshots/same name as builtin.snap delete mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/additional-envs/snapshots/query - direct env-test synthetic task.snap create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/additional-envs/snapshots/query - env-test synthetic task in user task.snap delete mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - direct synthetic task with cwd.snap delete mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - direct synthetic task with extra args.snap delete mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - direct synthetic task.snap create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - synthetic task in user task with cwd.snap diff --git a/Cargo.lock b/Cargo.lock index 3e48f87e..0585ead9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3371,6 +3371,7 @@ dependencies = [ "anyhow", "async-trait", "bincode", + "clap", "copy_dir", "cow-utils", "futures-util", diff --git a/crates/vite_task/docs/task-cache.md b/crates/vite_task/docs/task-cache.md index d21b7e42..db276b88 100644 --- a/crates/vite_task/docs/task-cache.md +++ b/crates/vite_task/docs/task-cache.md @@ -611,10 +611,10 @@ CommandFingerprint { } ``` -### Example: Built-in Task Cache Key +### Example: Synthetic Task Cache Key ```rust -// Built-in task: vite lint +// Synthetic task (e.g., "vite lint" in a task script) TaskRunKey { task_id: TaskId { task_group_id: TaskGroupId { @@ -830,20 +830,20 @@ Cache hit, replaying b ``` -### Example 3: Built-in Task Caching by Working Directory +### Example 3: Task Caching by Working Directory ```bash -# Different directories create separate caches for built-in tasks -> cd folder1 && vite lint +# Different directories create separate caches for tasks +> cd folder1 && vite run lint Cache not found Found 0 warnings and 0 errors. -> cd folder2 && vite lint +> cd folder2 && vite run lint Cache not found # Different cwd = different cache Found 0 warnings and 0 errors. # Each directory maintains its own cache -> cd folder1 && vite lint +> cd folder1 && vite run lint Cache hit, replaying Found 0 warnings and 0 errors. ``` diff --git a/crates/vite_task/src/cli/mod.rs b/crates/vite_task/src/cli/mod.rs index e31c7d9a..e6db9581 100644 --- a/crates/vite_task/src/cli/mod.rs +++ b/crates/vite_task/src/cli/mod.rs @@ -1,90 +1,14 @@ -use std::{ffi::OsStr, sync::Arc}; +use std::sync::Arc; -use clap::{Parser, Subcommand}; +use clap::Subcommand; use vite_path::AbsolutePath; use vite_str::Str; use vite_task_graph::{TaskSpecifier, query::TaskQueryKind}; use vite_task_plan::plan_request::{PlanOptions, PlanRequest, QueryPlanRequest}; -/// Represents the CLI arguments handled by vite-task, including both built-in (like run) and custom subcommands (like lint). -#[derive(Debug)] -pub struct TaskCLIArgs { - pub(crate) original: Arc<[Str]>, - pub(crate) parsed: ParsedTaskCLIArgs, -} - -impl TaskCLIArgs { - /// Inspect the custom subcommand (like lint/install). Returns `None` if it's built-in subcommand - /// The caller should not use this method to actually handle the custom subcommand. Instead, it should - /// private TaskSynthesizer to Session so that vite-task can handle custom subcommands consistently from - /// both direct CLI invocations and invocations in task scripts. - /// - /// This method is provided only to make it possible for the caller to behave differently BEFORE and AFTER the session. - /// For example, vite+ needs this method to skip auto-install when the custom subcommand is already `install`. - pub fn custom_subcommand(&self) -> Option<&CustomSubcommand> { - match &self.parsed { - ParsedTaskCLIArgs::BuiltIn(_) => None, - ParsedTaskCLIArgs::Custom(custom) => Some(custom), - } - } -} - -/// Represents the overall CLI arguments, containing three kinds of subcommands: -/// 1. Built-in subcommands handled by vite-task (like run) -/// 2. Custom subcommands handled by vite-task with the help of TaskSyntheizer (like lint) -/// 3. Custom subcommands not handled by vite-task (like vite+ commands without cache) -pub enum CLIArgs { - /// Subcommands handled by vite task, including built-in (like run) and custom (like lint) - Task(TaskCLIArgs), - /// Custom subcommands not handled by vite task (like vite+ commands without cache) - NonTask(NonTaskSubcommand), -} - -impl - CLIArgs -{ - /// Get the original CLI arguments - pub fn try_parse_from( - args: impl Iterator>, - ) -> Result { - #[derive(Debug, clap::Parser)] - enum ParsedCLIArgs { - /// subcommands handled by vite task - #[command(flatten)] - Task(ParsedTaskCLIArgs), - - /// subcommands that are not handled by vite task - #[command(flatten)] - NonTask(NonTaskSubcommand), - } - - let args = args.map(|arg| Str::from(arg.as_ref())).collect::>(); - let parsed_cli_args = ParsedCLIArgs::::try_parse_from( - args.iter().map(|s| OsStr::new(s.as_str())), - )?; - - Ok(match parsed_cli_args { - ParsedCLIArgs::Task(parsed_task_cli_args) => { - Self::Task(TaskCLIArgs { original: args, parsed: parsed_task_cli_args }) - } - ParsedCLIArgs::NonTask(non_task_subcommand) => Self::NonTask(non_task_subcommand), - }) - } -} - -#[derive(Debug, Parser)] -pub(crate) enum ParsedTaskCLIArgs { - /// subcommands provided by vite task, like `run` - #[clap(flatten)] - BuiltIn(BuiltInCommand), - /// custom subcommands provided by vite+, like `lint` - #[clap(flatten)] - Custom(CustomSubcommand), -} - /// vite task CLI subcommands #[derive(Debug, Subcommand)] -pub(crate) enum BuiltInCommand { +pub enum BuiltInCommand { /// Run tasks Run { /// `packageName#taskName` or `taskName`. @@ -118,7 +42,7 @@ pub enum CLITaskQueryError { } impl BuiltInCommand { - /// Convert to `TaskQuery`, or return an error if invalid. + /// Convert to `PlanRequest`, or return an error if invalid. pub fn into_plan_request( self, cwd: &Arc, diff --git a/crates/vite_task/src/lib.rs b/crates/vite_task/src/lib.rs index 458483ae..e1aeed36 100644 --- a/crates/vite_task/src/lib.rs +++ b/crates/vite_task/src/lib.rs @@ -4,7 +4,7 @@ mod maybe_str; pub mod session; // Public exports for vite_task_bin -pub use cli::CLIArgs; +pub use cli::BuiltInCommand; pub use session::{LabeledReporter, Reporter, Session, SessionCallbacks, TaskSynthesizer}; pub use vite_task_graph::{ config::{ diff --git a/crates/vite_task/src/session/execute/mod.rs b/crates/vite_task/src/session/execute/mod.rs index 7c9cae83..19fdcc61 100644 --- a/crates/vite_task/src/session/execute/mod.rs +++ b/crates/vite_task/src/session/execute/mod.rs @@ -348,7 +348,7 @@ impl ExecutionContext<'_> { } } -impl<'a, CustomSubcommand> Session<'a, CustomSubcommand> { +impl<'a> Session<'a> { /// Execute an execution plan, reporting events to the provided reporter. /// /// Returns Err(ExitStatus) to suggest the caller to abort and exit the process with the given exit status. diff --git a/crates/vite_task/src/session/mod.rs b/crates/vite_task/src/session/mod.rs index c06ce44e..76bb02ce 100644 --- a/crates/vite_task/src/session/mod.rs +++ b/crates/vite_task/src/session/mod.rs @@ -8,7 +8,7 @@ use std::{ffi::OsStr, fmt::Debug, sync::Arc}; use cache::ExecutionCache; pub use cache::{CacheMiss, FingerprintMismatch}; -use clap::{Parser, Subcommand}; +use clap::Parser; pub use event::ExecutionEvent; use once_cell::sync::OnceCell; pub use reporter::{LabeledReporter, Reporter}; @@ -22,10 +22,7 @@ use vite_task_plan::{ }; use vite_workspace::{WorkspaceRoot, find_workspace_root}; -use crate::{ - cli::{ParsedTaskCLIArgs, TaskCLIArgs}, - collections::HashMap, -}; +use crate::{cli::BuiltInCommand, collections::HashMap}; #[derive(Debug)] enum LazyTaskGraph<'a> { @@ -52,60 +49,42 @@ impl TaskGraphLoader for LazyTaskGraph<'_> { } } -pub struct SessionCallbacks<'a, CustomSubcommand> { - pub task_synthesizer: &'a mut (dyn TaskSynthesizer + 'a), +pub struct SessionCallbacks<'a> { + pub task_synthesizer: &'a mut (dyn TaskSynthesizer + 'a), pub user_config_loader: &'a mut (dyn UserConfigLoader + 'a), } +/// Handles synthesizing task plan requests from commands found in task scripts. +/// +/// When a task's command references a known program (e.g., `vite lint` in a script), +/// the synthesizer converts it into a `SyntheticPlanRequest` for execution. #[async_trait::async_trait(?Send)] -pub trait TaskSynthesizer: Debug { - fn should_synthesize_for_program(&self, program: &str) -> bool; - - /// Synthesize a synthetic task plan request for the given parsed custom subcommand. +pub trait TaskSynthesizer: Debug { + /// Called for every command in task scripts to determine if it should be synthesized. /// + /// - `program` is the program name (e.g., `"vite"`). + /// - `args` is all arguments after the program (e.g., `["lint", "--fix"]`). /// - `envs` is the current environment variables where the task is being planned. /// - `cwd` is the current working directory where the task is being planned. /// - /// The implementor can return a different `envs` in `SyntheticPlanRequest` to customize - /// environment variables for the synthetic task. + /// Returns `Ok(Some(request))` if the command is recognized and should be synthesized, + /// `Ok(None)` if the command should be executed as a normal process. async fn synthesize_task( &mut self, - subcommand: CustomSubcommand, + program: &str, + args: &[Str], envs: &Arc, Arc>>, cwd: &Arc, - ) -> anyhow::Result; + ) -> anyhow::Result>; } #[derive(derive_more::Debug)] -#[debug(bound())] // Avoid requiring CustomSubcommand: Debug -struct PlanRequestParser<'a, CustomSubcommand> { - task_synthesizer: &'a mut (dyn TaskSynthesizer + 'a), -} - -impl PlanRequestParser<'_, CustomSubcommand> { - async fn get_plan_request_from_cli_args( - &mut self, - cli_args: ParsedTaskCLIArgs, - envs: &Arc, Arc>>, - cwd: &Arc, - ) -> anyhow::Result { - match cli_args { - ParsedTaskCLIArgs::BuiltIn(vite_task_subcommand) => { - Ok(vite_task_subcommand.into_plan_request(cwd)?) - } - ParsedTaskCLIArgs::Custom(custom_subcommand) => { - let synthetic_plan_request = - self.task_synthesizer.synthesize_task(custom_subcommand, envs, cwd).await?; - Ok(PlanRequest::Synthetic(synthetic_plan_request)) - } - } - } +struct PlanRequestParser<'a> { + task_synthesizer: &'a mut (dyn TaskSynthesizer + 'a), } #[async_trait::async_trait(?Send)] -impl vite_task_plan::PlanRequestParser - for PlanRequestParser<'_, CustomSubcommand> -{ +impl vite_task_plan::PlanRequestParser for PlanRequestParser<'_> { async fn get_plan_request( &mut self, program: &str, @@ -113,26 +92,33 @@ impl vite_task_plan::PlanRequestParser envs: &Arc, Arc>>, cwd: &Arc, ) -> anyhow::Result> { - Ok( - if self.task_synthesizer.should_synthesize_for_program(program) - && let Some(subcommand) = args.first() - && ParsedTaskCLIArgs::::has_subcommand(subcommand) - { - let cli_args = ParsedTaskCLIArgs::::try_parse_from( - std::iter::once(program).chain(args.iter().map(Str::as_str)), - )?; - Some(self.get_plan_request_from_cli_args(cli_args, envs, cwd).await?) - } else { - None - }, - ) + // Try task synthesizer first (handles e.g. "vite lint" in scripts) + if let Some(synthetic) = + self.task_synthesizer.synthesize_task(program, args, envs, cwd).await? + { + return Ok(Some(PlanRequest::Synthetic(synthetic))); + } + + // Try built-in "run" command (handles "vite run build" in scripts) + #[derive(Parser)] + enum BuiltInParser { + #[clap(flatten)] + Command(BuiltInCommand), + } + if let Ok(BuiltInParser::Command(built_in)) = BuiltInParser::try_parse_from( + std::iter::once(program).chain(args.iter().map(Str::as_str)), + ) { + return Ok(Some(built_in.into_plan_request(cwd)?)); + } + + Ok(None) } } /// Represents a vite task session for planning and executing tasks. A process typically has one session. /// /// A session manages task graph loading internally and provides non-consuming methods to plan and/or execute tasks (allows multiple plans/executions per session). -pub struct Session<'a, CustomSubcommand> { +pub struct Session<'a> { workspace_path: Arc, /// A session doesn't necessarily load the task graph immediately. /// The task graph is loaded on-demand and cached for future use. @@ -141,7 +127,7 @@ pub struct Session<'a, CustomSubcommand> { envs: Arc, Arc>>, cwd: Arc, - plan_request_parser: PlanRequestParser<'a, CustomSubcommand>, + plan_request_parser: PlanRequestParser<'a>, /// Cache is lazily initialized to avoid SQLite race conditions when multiple /// processes (e.g., parallel `vite lib` commands) start simultaneously. @@ -157,9 +143,9 @@ fn get_cache_path_of_workspace(workspace_root: &AbsolutePath) -> AbsolutePathBuf } } -impl<'a, CustomSubcommand> Session<'a, CustomSubcommand> { +impl<'a> Session<'a> { /// Initialize a session with real environment variables and cwd - pub fn init(callbacks: SessionCallbacks<'a, CustomSubcommand>) -> anyhow::Result { + pub fn init(callbacks: SessionCallbacks<'a>) -> anyhow::Result { let envs = std::env::vars_os() .map(|(k, v)| (Arc::::from(k.as_os_str()), Arc::::from(v.as_os_str()))) .collect(); @@ -176,7 +162,7 @@ impl<'a, CustomSubcommand> Session<'a, CustomSubcommand> { pub fn init_with( mut envs: HashMap, Arc>, cwd: Arc, - callbacks: SessionCallbacks<'a, CustomSubcommand>, + callbacks: SessionCallbacks<'a>, ) -> anyhow::Result { let (workspace_root, _) = find_workspace_root(&cwd)?; let cache_path = get_cache_path_of_workspace(&workspace_root.path); @@ -217,9 +203,7 @@ impl<'a, CustomSubcommand> Session<'a, CustomSubcommand> { _ => None, } } -} -impl<'a, CustomSubcommand: clap::Subcommand> Session<'a, CustomSubcommand> { pub async fn plan_synthetic_task( &mut self, synthetic_plan_request: SyntheticPlanRequest, @@ -239,21 +223,17 @@ impl<'a, CustomSubcommand: clap::Subcommand> Session<'a, CustomSubcommand> { pub async fn plan_from_cli( &mut self, cwd: Arc, - cli_args: TaskCLIArgs, + command: BuiltInCommand, ) -> Result { - let plan_request = self - .plan_request_parser - .get_plan_request_from_cli_args(cli_args.parsed, &self.envs, &cwd) - .await - .map_err(|error| { - TaskPlanErrorKind::ParsePlanRequestError { - error, - program: cli_args.original[0].clone(), - args: cli_args.original.iter().skip(1).cloned().collect(), - cwd: Arc::clone(&cwd), - } - .with_empty_call_stack() - })?; + let plan_request = command.into_plan_request(&cwd).map_err(|error| { + TaskPlanErrorKind::ParsePlanRequestError { + error: error.into(), + program: Str::from("vite"), + args: Default::default(), + cwd: Arc::clone(&cwd), + } + .with_empty_call_stack() + })?; let plan = ExecutionPlan::plan( plan_request, &self.workspace_path, diff --git a/crates/vite_task/src/session/reporter.rs b/crates/vite_task/src/session/reporter.rs index 2d89fa3e..1c76ca8f 100644 --- a/crates/vite_task/src/session/reporter.rs +++ b/crates/vite_task/src/session/reporter.rs @@ -91,8 +91,8 @@ struct ExecutionStats { /// - Skips printing the execution summary entirely /// - Useful for programmatic usage or when summary is not needed /// -/// ## Simplified Summary for Built-in Commands -/// - When a single built-in command (e.g., `vite lint`) is executed: +/// ## Simplified Summary for Single Tasks +/// - When a single task is executed: /// - Skips full summary (no Statistics/Task Details sections) /// - Shows only cache status (except for "NotFound" which is hidden for clean first-run output) /// - Results in clean output showing just the command's stdout/stderr @@ -156,7 +156,7 @@ impl LabeledReporter { CacheStatus::Disabled(_) => self.stats.cache_disabled += 1, } - // Handle None display case - direct synthetic execution (e.g., `vite lint`) + // Handle None display case - direct synthetic execution (e.g., via plan_synthetic_task) // Don't print cache status here - will be printed at finish for cache hits only let Some(display) = display else { self.executions.push(ExecutionInfo { diff --git a/crates/vite_task_bin/src/lib.rs b/crates/vite_task_bin/src/lib.rs index 49fd6a06..f206be52 100644 --- a/crates/vite_task_bin/src/lib.rs +++ b/crates/vite_task_bin/src/lib.rs @@ -7,7 +7,6 @@ use std::{ sync::Arc, }; -use clap::Subcommand; use vite_path::AbsolutePath; use vite_str::Str; use vite_task::{ @@ -15,36 +14,6 @@ use vite_task::{ plan_request::SyntheticPlanRequest, }; -/// Theses are the custom subcommands that synthesize tasks for vite-task -#[derive(Debug, Subcommand)] -pub enum CustomTaskSubcommand { - /// oxlint - #[clap(disable_help_flag = true)] - Lint { - #[clap(allow_hyphen_values = true, trailing_var_arg = true)] - args: Vec, - }, - /// vitest - #[clap(disable_help_flag = true)] - Test { - #[clap(allow_hyphen_values = true, trailing_var_arg = true)] - args: Vec, - }, - /// Test command for testing additional_envs feature - EnvTest { - /// Environment variable name - name: Str, - /// Environment variable value - value: Str, - }, -} - -// These are the subcommands that is not handled by vite-task -#[derive(Debug, Subcommand)] -pub enum NonTaskSubcommand { - Version, -} - #[derive(Debug, Default)] pub struct TaskSynthesizer(()); @@ -69,52 +38,67 @@ fn find_executable( Ok(executable_path.into_os_string().into()) } -#[async_trait::async_trait(?Send)] -impl vite_task::TaskSynthesizer for TaskSynthesizer { - fn should_synthesize_for_program(&self, program: &str) -> bool { - program == "vite" - } +fn synthesize_node_modules_bin_task( + subcommand_name: &str, + executable_name: &str, + args: &[Str], + envs: &Arc, Arc>>, + cwd: &Arc, +) -> anyhow::Result { + let direct_execution_cache_key: Arc<[Str]> = + iter::once(Str::from(subcommand_name)).chain(args.iter().cloned()).collect(); + Ok(SyntheticPlanRequest { + program: find_executable(get_path_env(envs), &*cwd, executable_name)?, + args: args.into(), + task_options: Default::default(), + direct_execution_cache_key, + envs: Arc::clone(envs), + }) +} +#[async_trait::async_trait(?Send)] +impl vite_task::TaskSynthesizer for TaskSynthesizer { async fn synthesize_task( &mut self, - subcommand: CustomTaskSubcommand, + program: &str, + args: &[Str], envs: &Arc, Arc>>, cwd: &Arc, - ) -> anyhow::Result { - let synthesize_node_modules_bin_task = |subcommand_name: &str, - executable_name: &str, - args: Vec| - -> anyhow::Result { - let direct_execution_cache_key: Arc<[Str]> = - iter::once(Str::from(subcommand_name)).chain(args.iter().cloned()).collect(); - Ok(SyntheticPlanRequest { - program: find_executable(get_path_env(envs), &*cwd, executable_name)?, - args: args.into(), - task_options: Default::default(), - direct_execution_cache_key, - envs: Arc::clone(envs), - }) + ) -> anyhow::Result> { + if program != "vite" { + return Ok(None); + } + let Some(subcommand) = args.first() else { + return Ok(None); }; - - match subcommand { - CustomTaskSubcommand::Lint { args } => { - synthesize_node_modules_bin_task("lint", "oxlint", args) + let rest = &args[1..]; + match subcommand.as_str() { + "lint" => { + Ok(Some(synthesize_node_modules_bin_task("lint", "oxlint", rest, envs, cwd)?)) } - CustomTaskSubcommand::Test { args } => { - synthesize_node_modules_bin_task("test", "vitest", args) + "test" => { + Ok(Some(synthesize_node_modules_bin_task("test", "vitest", rest, envs, cwd)?)) } - CustomTaskSubcommand::EnvTest { name, value } => { + "env-test" => { + let name = rest + .first() + .ok_or_else(|| anyhow::anyhow!("env-test requires a name argument"))? + .clone(); + let value = rest + .get(1) + .ok_or_else(|| anyhow::anyhow!("env-test requires a value argument"))? + .clone(); + let direct_execution_cache_key: Arc<[Str]> = [Str::from("env-test"), name.clone(), value.clone()].into(); let mut envs = HashMap::clone(&envs); - // Update the env var for testing envs.insert( Arc::from(OsStr::new(name.as_str())), Arc::from(OsStr::new(value.as_str())), ); - Ok(SyntheticPlanRequest { + Ok(Some(SyntheticPlanRequest { program: find_executable(get_path_env(&envs), &*cwd, "print-env")?, args: [name.clone()].into(), task_options: UserTaskOptions { @@ -129,8 +113,9 @@ impl vite_task::TaskSynthesizer for TaskSynthesizer { }, direct_execution_cache_key, envs: Arc::new(envs), - }) + })) } + _ => Ok(None), } } } @@ -169,7 +154,7 @@ pub struct OwnedSessionCallbacks { } impl OwnedSessionCallbacks { - pub fn as_callbacks(&mut self) -> SessionCallbacks<'_, CustomTaskSubcommand> { + pub fn as_callbacks(&mut self) -> SessionCallbacks<'_> { SessionCallbacks { task_synthesizer: &mut self.task_synthesizer, user_config_loader: &mut self.user_config_loader, diff --git a/crates/vite_task_bin/src/main.rs b/crates/vite_task_bin/src/main.rs index 43dc2a78..7f2ce8c5 100644 --- a/crates/vite_task_bin/src/main.rs +++ b/crates/vite_task_bin/src/main.rs @@ -1,11 +1,19 @@ -use std::{env, process::ExitCode, sync::Arc}; +use std::{process::ExitCode, sync::Arc}; +use clap::Parser; use vite_path::{AbsolutePath, current_dir}; use vite_task::{ - CLIArgs, Session, + BuiltInCommand, Session, session::reporter::{ExitStatus, LabeledReporter}, }; -use vite_task_bin::{CustomTaskSubcommand, NonTaskSubcommand, OwnedSessionCallbacks}; +use vite_task_bin::OwnedSessionCallbacks; + +#[derive(Parser)] +#[command(name = "vite", version)] +struct Cli { + #[command(subcommand)] + command: BuiltInCommand, +} #[tokio::main] async fn main() -> anyhow::Result { @@ -15,26 +23,11 @@ async fn main() -> anyhow::Result { async fn run() -> anyhow::Result { let cwd: Arc = current_dir()?.into(); - // Parse the CLI arguments and see if they are for vite-task or not - let args = match CLIArgs::::try_parse_from(env::args()) - { - Ok(ok) => ok, - Err(err) => { - err.exit(); - } - }; - let task_cli_args = match args { - CLIArgs::Task(task_cli_args) => task_cli_args, - CLIArgs::NonTask(NonTaskSubcommand::Version) => { - // Non-task subcommands are not handled by vite-task's session. - println!("{}", env!("CARGO_PKG_VERSION")); - return Ok(ExitStatus::SUCCESS); - } - }; + let cli = Cli::parse(); let mut owned_callbacks = OwnedSessionCallbacks::default(); let mut session = Session::init(owned_callbacks.as_callbacks())?; - let plan = session.plan_from_cli(cwd, task_cli_args).await?; + let plan = session.plan_from_cli(cwd, cli.command).await?; // Create reporter and execute let reporter = LabeledReporter::new(std::io::stdout(), session.workspace_path()); diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/builtin-different-cwd/package.json b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/builtin-different-cwd/package.json index 0967ef42..3f45091c 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/builtin-different-cwd/package.json +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/builtin-different-cwd/package.json @@ -1 +1,5 @@ -{} +{ + "scripts": { + "lint": "vite lint" + } +} diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/builtin-different-cwd/snapshots.toml b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/builtin-different-cwd/snapshots.toml index b92b3f66..bb79544f 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/builtin-different-cwd/snapshots.toml +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/builtin-different-cwd/snapshots.toml @@ -1,11 +1,11 @@ -# Tests that builtin commands have separate cache per cwd +# Tests that tasks have separate cache per cwd [[e2e]] name = "builtin different cwd" steps = [ - "cd folder1 && vite lint # cache miss in folder1", - "cd folder2 && vite lint # cache miss in folder2", + "cd folder1 && vite run lint # cache miss in folder1", + "cd folder2 && vite run lint # cache miss in folder2", "echo 'console.log(1);' > folder2/a.js # modify folder2", - "cd folder1 && vite lint # cache hit", - "cd folder2 && vite lint # cache miss", + "cd folder1 && vite run lint # cache hit", + "cd folder2 && vite run lint # cache miss", ] diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/builtin-different-cwd/snapshots/builtin different cwd.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/builtin-different-cwd/snapshots/builtin different cwd.snap index b40f307a..e86b760d 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/builtin-different-cwd/snapshots/builtin different cwd.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/builtin-different-cwd/snapshots/builtin different cwd.snap @@ -1,51 +1,128 @@ --- source: crates/vite_task_bin/tests/e2e_snapshots/main.rs -assertion_line: 203 expression: e2e_outputs input_file: crates/vite_task_bin/tests/e2e_snapshots/fixtures/builtin-different-cwd --- -> cd folder1 && vite lint # cache miss in folder1 +> cd folder1 && vite run lint # cache miss in folder1 +$ vite lint ! eslint-plugin-unicorn(no-empty-file): Empty files are not allowed. - ,-[a.js:1:1] + ,-[folder1/a.js:1:1] 1 | // Empty JS file for oxlint : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `---- help: Delete this file or add some code to it. -Found 1 warning and 0 errors. -Finished in on 1 file with 90 rules using threads. + ! eslint-plugin-unicorn(no-empty-file): Empty files are not allowed. + ,-[folder2/a.js:1:1] + 1 | // Empty JS file for oxlint + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + `---- + help: Delete this file or add some code to it. + +Found 2 warnings and 0 errors. +Finished in on 2 files with 90 rules using threads. + + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Vite+ Task Runner • Execution Summary +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Statistics: 1 tasks • 0 cache hits • 1 cache misses +Performance: 0% cache hit rate -> cd folder2 && vite lint # cache miss in folder2 +Task Details: +──────────────────────────────────────────────── + [1] lint: $ vite lint ✓ + → Cache miss: no previous cache entry found +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +> cd folder2 && vite run lint # cache miss in folder2 +$ vite lint ✓ cache hit, replaying ! eslint-plugin-unicorn(no-empty-file): Empty files are not allowed. - ,-[a.js:1:1] + ,-[folder1/a.js:1:1] 1 | // Empty JS file for oxlint : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `---- help: Delete this file or add some code to it. -Found 1 warning and 0 errors. -Finished in on 1 file with 90 rules using threads. + ! eslint-plugin-unicorn(no-empty-file): Empty files are not allowed. + ,-[folder2/a.js:1:1] + 1 | // Empty JS file for oxlint + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + `---- + help: Delete this file or add some code to it. + +Found 2 warnings and 0 errors. +Finished in on 2 files with 90 rules using threads. + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Vite+ Task Runner • Execution Summary +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Statistics: 1 tasks • 1 cache hits • 0 cache misses +Performance: 100% cache hit rate, saved in total + +Task Details: +──────────────────────────────────────────────── + [1] lint: $ vite lint ✓ + → Cache hit - output replayed - saved +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ > echo 'console.log(1);' > folder2/a.js # modify folder2 -> cd folder1 && vite lint # cache hit +> cd folder1 && vite run lint # cache hit +$ vite lint ✗ cache miss: content of input 'folder2/a.js' changed, executing ! eslint-plugin-unicorn(no-empty-file): Empty files are not allowed. - ,-[a.js:1:1] + ,-[folder1/a.js:1:1] 1 | // Empty JS file for oxlint : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `---- help: Delete this file or add some code to it. Found 1 warning and 0 errors. -Finished in on 1 file with 90 rules using threads. -✓ cache hit, logs replayed +Finished in on 2 files with 90 rules using threads. + + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Vite+ Task Runner • Execution Summary +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Statistics: 1 tasks • 0 cache hits • 1 cache misses +Performance: 0% cache hit rate + +Task Details: +──────────────────────────────────────────────── + [1] lint: $ vite lint ✓ + → Cache miss: content of input 'folder2/a.js' changed +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +> cd folder2 && vite run lint # cache miss +$ vite lint ✓ cache hit, replaying + + ! eslint-plugin-unicorn(no-empty-file): Empty files are not allowed. + ,-[folder1/a.js:1:1] + 1 | // Empty JS file for oxlint + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + `---- + help: Delete this file or add some code to it. + +Found 1 warning and 0 errors. +Finished in on 2 files with 90 rules using threads. + + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Vite+ Task Runner • Execution Summary +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Statistics: 1 tasks • 1 cache hits • 0 cache misses +Performance: 100% cache hit rate, saved in total -> cd folder2 && vite lint # cache miss -Found 0 warnings and 0 errors. -Finished in on 1 file with 90 rules using threads. +Task Details: +──────────────────────────────────────────────── + [1] lint: $ vite lint ✓ + → Cache hit - output replayed - saved +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/builtin-non-zero-exit/package.json b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/builtin-non-zero-exit/package.json index cd8fc56e..484cf605 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/builtin-non-zero-exit/package.json +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/builtin-non-zero-exit/package.json @@ -1,3 +1,6 @@ { - "name": "builtin-non-zero-exit-test" + "name": "builtin-non-zero-exit-test", + "scripts": { + "lint": "vite lint" + } } diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/builtin-non-zero-exit/snapshots.toml b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/builtin-non-zero-exit/snapshots.toml index 28b3fb46..f50465ef 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/builtin-non-zero-exit/snapshots.toml +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/builtin-non-zero-exit/snapshots.toml @@ -1,6 +1,6 @@ [[e2e]] name = "builtin command with non-zero exit does not show cache not updated" steps = [ - "vite lint -D no-debugger", - "vite lint -D no-debugger", + "vite run lint -- -D no-debugger", + "vite run lint -- -D no-debugger", ] diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/builtin-non-zero-exit/snapshots/builtin command with non-zero exit does not show cache not updated.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/builtin-non-zero-exit/snapshots/builtin command with non-zero exit does not show cache not updated.snap index 605f153a..06b73644 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/builtin-non-zero-exit/snapshots/builtin command with non-zero exit does not show cache not updated.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/builtin-non-zero-exit/snapshots/builtin command with non-zero exit does not show cache not updated.snap @@ -3,7 +3,8 @@ source: crates/vite_task_bin/tests/e2e_snapshots/main.rs expression: e2e_outputs input_file: crates/vite_task_bin/tests/e2e_snapshots/fixtures/builtin-non-zero-exit --- -[1]> vite lint -D no-debugger +[1]> vite run lint -- -D no-debugger +$ vite lint -D no-debugger x eslint(no-debugger): `debugger` statement is not allowed ,-[bad.js:1:1] @@ -16,7 +17,21 @@ Found 0 warnings and 1 error. Finished in on 1 file with 90 rules using threads. -[1]> vite lint -D no-debugger +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Vite+ Task Runner • Execution Summary +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Statistics: 1 tasks • 0 cache hits • 1 cache misses • 1 failed +Performance: 0% cache hit rate + +Task Details: +──────────────────────────────────────────────── + [1] builtin-non-zero-exit-test#lint: $ vite lint -D no-debugger ✗ (exit code: 1) + → Cache miss: no previous cache entry found +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +[1]> vite run lint -- -D no-debugger +$ vite lint -D no-debugger x eslint(no-debugger): `debugger` statement is not allowed ,-[bad.js:1:1] @@ -27,3 +42,17 @@ Finished in on 1 file with 90 rules using threads. Found 0 warnings and 1 error. Finished in on 1 file with 90 rules using threads. + + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Vite+ Task Runner • Execution Summary +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Statistics: 1 tasks • 0 cache hits • 1 cache misses • 1 failed +Performance: 0% cache hit rate + +Task Details: +──────────────────────────────────────────────── + [1] builtin-non-zero-exit-test#lint: $ vite lint -D no-debugger ✗ (exit code: 1) + → Cache miss: no previous cache entry found +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/e2e-env-test/package.json b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/e2e-env-test/package.json index 9340d35d..ed6793f0 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/e2e-env-test/package.json +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/e2e-env-test/package.json @@ -1,4 +1,7 @@ { "name": "e2e-env-test", - "private": true + "private": true, + "scripts": { + "env-test": "vite env-test" + } } diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/e2e-env-test/snapshots.toml b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/e2e-env-test/snapshots.toml index 94a6dfdf..069aa8e5 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/e2e-env-test/snapshots.toml +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/e2e-env-test/snapshots.toml @@ -3,12 +3,12 @@ [[e2e]] name = "env-test prints value from additional_envs" steps = [ - "vite env-test SYNTHETIC_ENV_VAR test_value_from_synthesizer # prints env value", + "vite run env-test -- SYNTHETIC_ENV_VAR test_value_from_synthesizer # prints env value", ] [[e2e]] name = "env-test with different values" steps = [ - "vite env-test FOO bar # sets FOO=bar", - "vite env-test BAZ qux # sets BAZ=qux", + "vite run env-test -- FOO bar # sets FOO=bar", + "vite run env-test -- BAZ qux # sets BAZ=qux", ] diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/e2e-env-test/snapshots/env-test prints value from additional_envs.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/e2e-env-test/snapshots/env-test prints value from additional_envs.snap index caf589c6..eb60e270 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/e2e-env-test/snapshots/env-test prints value from additional_envs.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/e2e-env-test/snapshots/env-test prints value from additional_envs.snap @@ -1,8 +1,22 @@ --- source: crates/vite_task_bin/tests/e2e_snapshots/main.rs -assertion_line: 203 expression: e2e_outputs input_file: crates/vite_task_bin/tests/e2e_snapshots/fixtures/e2e-env-test --- -> vite env-test SYNTHETIC_ENV_VAR test_value_from_synthesizer # prints env value +> vite run env-test -- SYNTHETIC_ENV_VAR test_value_from_synthesizer # prints env value +$ vite env-test SYNTHETIC_ENV_VAR test_value_from_synthesizer test_value_from_synthesizer + + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Vite+ Task Runner • Execution Summary +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Statistics: 1 tasks • 0 cache hits • 1 cache misses +Performance: 0% cache hit rate + +Task Details: +──────────────────────────────────────────────── + [1] e2e-env-test#env-test: $ vite env-test SYNTHETIC_ENV_VAR test_value_from_synthesizer ✓ + → Cache miss: no previous cache entry found +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/e2e-env-test/snapshots/env-test with different values.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/e2e-env-test/snapshots/env-test with different values.snap index 375b1f1f..43b582c2 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/e2e-env-test/snapshots/env-test with different values.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/e2e-env-test/snapshots/env-test with different values.snap @@ -1,12 +1,40 @@ --- source: crates/vite_task_bin/tests/e2e_snapshots/main.rs -assertion_line: 203 expression: e2e_outputs input_file: crates/vite_task_bin/tests/e2e_snapshots/fixtures/e2e-env-test --- -> vite env-test FOO bar # sets FOO=bar +> vite run env-test -- FOO bar # sets FOO=bar +$ vite env-test FOO bar bar -> vite env-test BAZ qux # sets BAZ=qux +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Vite+ Task Runner • Execution Summary +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Statistics: 1 tasks • 0 cache hits • 1 cache misses +Performance: 0% cache hit rate + +Task Details: +──────────────────────────────────────────────── + [1] e2e-env-test#env-test: $ vite env-test FOO bar ✓ + → Cache miss: no previous cache entry found +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +> vite run env-test -- BAZ qux # sets BAZ=qux +$ vite env-test BAZ qux qux + + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Vite+ Task Runner • Execution Summary +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Statistics: 1 tasks • 0 cache hits • 1 cache misses +Performance: 0% cache hit rate + +Task Details: +──────────────────────────────────────────────── + [1] e2e-env-test#env-test: $ vite env-test BAZ qux ✓ + → Cache miss: no previous cache entry found +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/e2e-lint-cache/package.json b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/e2e-lint-cache/package.json index c9faaf6d..8aeca1bc 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/e2e-lint-cache/package.json +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/e2e-lint-cache/package.json @@ -1,4 +1,7 @@ { "name": "e2e-lint-cache", - "private": true + "private": true, + "scripts": { + "lint": "vite lint" + } } diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/e2e-lint-cache/snapshots.toml b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/e2e-lint-cache/snapshots.toml index 1ad17a2d..ed0f46fb 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/e2e-lint-cache/snapshots.toml +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/e2e-lint-cache/snapshots.toml @@ -3,7 +3,7 @@ [[e2e]] name = "direct lint" steps = [ - "vite lint # cache miss", + "vite run lint # cache miss", "echo debugger > main.js # add lint error", - "vite lint # cache miss, lint fails", + "vite run lint # cache miss, lint fails", ] diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/e2e-lint-cache/snapshots/direct lint.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/e2e-lint-cache/snapshots/direct lint.snap index 5fafaefe..66a27871 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/e2e-lint-cache/snapshots/direct lint.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/e2e-lint-cache/snapshots/direct lint.snap @@ -1,17 +1,31 @@ --- source: crates/vite_task_bin/tests/e2e_snapshots/main.rs -assertion_line: 203 expression: e2e_outputs input_file: crates/vite_task_bin/tests/e2e_snapshots/fixtures/e2e-lint-cache --- -> vite lint # cache miss +> vite run lint # cache miss +$ vite lint Found 0 warnings and 0 errors. Finished in on 0 files with 90 rules using threads. +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Vite+ Task Runner • Execution Summary +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Statistics: 1 tasks • 0 cache hits • 1 cache misses +Performance: 0% cache hit rate + +Task Details: +──────────────────────────────────────────────── + [1] e2e-lint-cache#lint: $ vite lint ✓ + → Cache miss: no previous cache entry found +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + > echo debugger > main.js # add lint error -> vite lint # cache miss, lint fails +> vite run lint # cache miss, lint fails +$ vite lint ✗ cache miss: content of input '' changed, executing ! eslint(no-debugger): `debugger` statement is not allowed ,-[main.js:1:1] @@ -22,3 +36,17 @@ Finished in on 0 files with 90 rules using threads. Found 1 warning and 0 errors. Finished in on 1 file with 90 rules using threads. + + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Vite+ Task Runner • Execution Summary +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Statistics: 1 tasks • 0 cache hits • 1 cache misses +Performance: 0% cache hit rate + +Task Details: +──────────────────────────────────────────────── + [1] e2e-lint-cache#lint: $ vite lint ✓ + → Cache miss: content of input '' changed +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/lint-dot-git/package.json b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/lint-dot-git/package.json index 0967ef42..3f45091c 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/lint-dot-git/package.json +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/lint-dot-git/package.json @@ -1 +1,5 @@ -{} +{ + "scripts": { + "lint": "vite lint" + } +} diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/lint-dot-git/snapshots.toml b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/lint-dot-git/snapshots.toml index 134dc2e5..6d0d9103 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/lint-dot-git/snapshots.toml +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/lint-dot-git/snapshots.toml @@ -4,7 +4,7 @@ name = "lint dot git" steps = [ "mkdir .git", - "vite lint # cache miss", + "vite run lint # cache miss", "echo hello > .git/foo.txt # add file inside .git", - "vite lint # cache hit, .git is ignored", + "vite run lint # cache hit, .git is ignored", ] diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/lint-dot-git/snapshots/lint dot git.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/lint-dot-git/snapshots/lint dot git.snap index 5ced85d4..353eea7a 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/lint-dot-git/snapshots/lint dot git.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/lint-dot-git/snapshots/lint dot git.snap @@ -1,12 +1,12 @@ --- source: crates/vite_task_bin/tests/e2e_snapshots/main.rs -assertion_line: 203 expression: e2e_outputs input_file: crates/vite_task_bin/tests/e2e_snapshots/fixtures/lint-dot-git --- > mkdir .git -> vite lint # cache miss +> vite run lint # cache miss +$ vite lint ! eslint-plugin-unicorn(no-empty-file): Empty files are not allowed. ,-[a.js:1:1] @@ -19,9 +19,23 @@ Found 1 warning and 0 errors. Finished in on 1 file with 90 rules using threads. +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Vite+ Task Runner • Execution Summary +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Statistics: 1 tasks • 0 cache hits • 1 cache misses +Performance: 0% cache hit rate + +Task Details: +──────────────────────────────────────────────── + [1] lint: $ vite lint ✓ + → Cache miss: no previous cache entry found +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + > echo hello > .git/foo.txt # add file inside .git -> vite lint # cache hit, .git is ignored +> vite run lint # cache hit, .git is ignored +$ vite lint ✓ cache hit, replaying ! eslint-plugin-unicorn(no-empty-file): Empty files are not allowed. ,-[a.js:1:1] @@ -32,4 +46,17 @@ Finished in on 1 file with 90 rules using threads. Found 1 warning and 0 errors. Finished in on 1 file with 90 rules using threads. -✓ cache hit, logs replayed + + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Vite+ Task Runner • Execution Summary +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Statistics: 1 tasks • 1 cache hits • 0 cache misses +Performance: 100% cache hit rate, saved in total + +Task Details: +──────────────────────────────────────────────── + [1] lint: $ vite lint ✓ + → Cache hit - output replayed - saved +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/same-name-as-builtin/a.js b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/same-name-as-builtin/a.js deleted file mode 100644 index dddcb9f2..00000000 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/same-name-as-builtin/a.js +++ /dev/null @@ -1 +0,0 @@ -// Empty JS file for oxlint diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/same-name-as-builtin/package.json b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/same-name-as-builtin/package.json deleted file mode 100644 index 008774dd..00000000 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/same-name-as-builtin/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "scripts": { - "lint": "echo custom lint script" - } -} diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/same-name-as-builtin/snapshots.toml b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/same-name-as-builtin/snapshots.toml deleted file mode 100644 index dc6a58f1..00000000 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/same-name-as-builtin/snapshots.toml +++ /dev/null @@ -1,11 +0,0 @@ -# Tests that user task named "lint" doesn't conflict with builtin lint - -[[e2e]] -name = "same name as builtin" -steps = [ - "vite lint # runs builtin oxlint", - "vite run lint # runs user-defined lint script", - "echo 'console.log(1);' > a.js # add valid JS file", - "vite lint # builtin lint: cache miss", - "vite run lint # user lint: cache miss", -] diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/same-name-as-builtin/snapshots/same name as builtin.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/same-name-as-builtin/snapshots/same name as builtin.snap deleted file mode 100644 index 08376497..00000000 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/same-name-as-builtin/snapshots/same name as builtin.snap +++ /dev/null @@ -1,61 +0,0 @@ ---- -source: crates/vite_task_bin/tests/e2e_snapshots/main.rs -assertion_line: 203 -expression: e2e_outputs -input_file: crates/vite_task_bin/tests/e2e_snapshots/fixtures/same-name-as-builtin ---- -> vite lint # runs builtin oxlint - - ! eslint-plugin-unicorn(no-empty-file): Empty files are not allowed. - ,-[a.js:1:1] - 1 | // Empty JS file for oxlint - : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - `---- - help: Delete this file or add some code to it. - -Found 1 warning and 0 errors. -Finished in on 1 file with 90 rules using threads. - - -> vite run lint # runs user-defined lint script -$ echo custom lint script ⊘ cache disabled: built-in command -custom lint script - - -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - Vite+ Task Runner • Execution Summary -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -Statistics: 1 tasks • 0 cache hits • 0 cache misses • 1 cache disabled -Performance: 0% cache hit rate - -Task Details: -──────────────────────────────────────────────── - [1] lint: $ echo custom lint script ✓ - → Cache disabled for built-in command -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -> echo 'console.log(1);' > a.js # add valid JS file - -> vite lint # builtin lint: cache miss -Found 0 warnings and 0 errors. -Finished in on 1 file with 90 rules using threads. - - -> vite run lint # user lint: cache miss -$ echo custom lint script ⊘ cache disabled: built-in command -custom lint script - - -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - Vite+ Task Runner • Execution Summary -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -Statistics: 1 tasks • 0 cache hits • 0 cache misses • 1 cache disabled -Performance: 0% cache hit rate - -Task Details: -──────────────────────────────────────────────── - [1] lint: $ echo custom lint script ✓ - → Cache disabled for built-in command -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ diff --git a/crates/vite_task_bin/tests/e2e_snapshots/redact.rs b/crates/vite_task_bin/tests/e2e_snapshots/redact.rs index 4877b243..0e6d1de5 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/redact.rs +++ b/crates/vite_task_bin/tests/e2e_snapshots/redact.rs @@ -52,5 +52,53 @@ pub fn redact_e2e_output(mut output: String, workspace_root: &str) -> String { .unwrap(); output = node_trace_warning_regex.replace_all(&output, "").into_owned(); + // Sort consecutive diagnostic blocks to handle non-deterministic tool output + // (e.g., oxlint reports warnings in arbitrary order due to multi-threading). + // Each block starts with " ! " and ends at the next empty line. + output = sort_diagnostic_blocks(output); + output } + +fn sort_diagnostic_blocks(output: String) -> String { + let parts: Vec<&str> = output.split('\n').collect(); + let mut result: Vec<&str> = Vec::new(); + let mut i = 0; + + while i < parts.len() { + if parts[i].starts_with(" ! ") { + let mut blocks: Vec> = Vec::new(); + + loop { + if i >= parts.len() || !parts[i].starts_with(" ! ") { + break; + } + let mut block: Vec<&str> = Vec::new(); + while i < parts.len() && !parts[i].is_empty() { + block.push(parts[i]); + i += 1; + } + blocks.push(block); + // Skip the empty line separator between blocks + if i < parts.len() && parts[i].is_empty() { + i += 1; + } + } + + blocks.sort(); + + for (j, block) in blocks.iter().enumerate() { + result.extend_from_slice(block); + // Restore empty line separators (between blocks + trailing) + if j < blocks.len() - 1 || i <= parts.len() { + result.push(""); + } + } + } else { + result.push(parts[i]); + i += 1; + } + } + + result.join("\n") +} diff --git a/crates/vite_task_plan/Cargo.toml b/crates/vite_task_plan/Cargo.toml index 2cdd741f..b3585966 100644 --- a/crates/vite_task_plan/Cargo.toml +++ b/crates/vite_task_plan/Cargo.toml @@ -31,6 +31,7 @@ vite_task_graph = { workspace = true } which = { workspace = true } [dev-dependencies] +clap = { workspace = true, features = ["derive"] } copy_dir = { workspace = true } cow-utils = { workspace = true } insta = { workspace = true, features = ["glob", "json", "redactions", "filters", "ron"] } diff --git a/crates/vite_task_plan/README.md b/crates/vite_task_plan/README.md index 7a66daa7..8416b174 100644 --- a/crates/vite_task_plan/README.md +++ b/crates/vite_task_plan/README.md @@ -31,9 +31,9 @@ There are two types of execution requests: - Queries the task graph based on task patterns - Builds execution graph with dependency ordering -2. **Synthetic Request** - Execute on-the-fly tasks not in the graph (e.g., `vite lint`) - - Generated dynamically with provided configuration - - Used for built-in commands +2. **Synthetic Request** - Execute on-the-fly tasks not in the graph (e.g., `vite lint` in a task script) + - Generated dynamically by the TaskSynthesizer + - Used for synthesized commands within task scripts ### Execution Items diff --git a/crates/vite_task_plan/src/cache_metadata.rs b/crates/vite_task_plan/src/cache_metadata.rs index 83136f6b..cd51915f 100644 --- a/crates/vite_task_plan/src/cache_metadata.rs +++ b/crates/vite_task_plan/src/cache_metadata.rs @@ -10,10 +10,11 @@ use crate::envs::EnvFingerprints; /// The kind of a key to identify an execution. #[derive(Debug, Encode, bincode::Decode, Serialize)] pub(crate) enum ExecutionCacheKeyKind { - /// This execution is directly from a custom syntactic vite-task subcommand (like `vite lint`). + /// This execution is from a synthetic task generated by the task synthesizer + /// (e.g., `"lint": "vite lint"` expanding to an `oxlint` invocation). /// - /// Note that this is only for the case where the subcommand is directly typed in the cli, - /// not from a task script (like `"lint-task": "vite lint"`), which is covered by the `UserTask` variant. + /// Note that this variant is used when the synthetic task is not wrapped in a user task, + /// unlike the `UserTask` variant which covers `"lint-task": "vite lint"` with its own cache key. DirectSyntactic { /// Provided in `SyntheticPlanRequest.direct_execution_cache_key` by task synthezier direct_execution_cache_key: Arc<[Str]>, diff --git a/crates/vite_task_plan/src/lib.rs b/crates/vite_task_plan/src/lib.rs index 346b73ba..3bde78ef 100644 --- a/crates/vite_task_plan/src/lib.rs +++ b/crates/vite_task_plan/src/lib.rs @@ -141,7 +141,7 @@ pub enum LeafExecutionKind { /// An execution item, from a split subcommand in a task's command (`item1 && item2 && ...`). #[derive(Debug, Serialize)] pub enum ExecutionItemKind { - /// Expanded from a known vite subcommand, like `vite run ...` or `vite lint`. + /// Expanded from a known vite subcommand, like `vite run ...` or a synthesized task. Expanded(#[serde(serialize_with = "serialize_by_key")] ExecutionGraph), /// A normal execution that spawns a child process, like `tsc --noEmit`. Leaf(LeafExecutionKind), diff --git a/crates/vite_task_plan/src/plan.rs b/crates/vite_task_plan/src/plan.rs index 30e2dcaa..80e776de 100644 --- a/crates/vite_task_plan/src/plan.rs +++ b/crates/vite_task_plan/src/plan.rs @@ -182,7 +182,7 @@ async fn plan_task_as_execution_node( let execution_graph = plan_query_request(query_plan_request, context).await?; ExecutionItemKind::Expanded(execution_graph) } - // Synthetic task, like `vite lint` + // Synthetic task (from TaskSynthesizer) Some(PlanRequest::Synthetic(synthetic_plan_request)) => { let spawn_execution = plan_synthetic_request( context.workspace_path(), diff --git a/crates/vite_task_plan/src/plan_request.rs b/crates/vite_task_plan/src/plan_request.rs index 6e4b9b08..9a105694 100644 --- a/crates/vite_task_plan/src/plan_request.rs +++ b/crates/vite_task_plan/src/plan_request.rs @@ -19,7 +19,7 @@ pub struct QueryPlanRequest { pub plan_options: PlanOptions, } -/// The request to run a synthetic task, like `vite lint` or `vite exec ...` +/// The request to run a synthetic task (e.g., one generated by TaskSynthesizer from `vite lint` in a script). /// Synthetic tasks are not defined in the task graph, but are generated on-the-fly. #[derive(Debug)] pub struct SyntheticPlanRequest { @@ -51,6 +51,6 @@ pub struct SyntheticPlanRequest { pub enum PlanRequest { /// The request to run tasks queried from the task graph, like `vite run ...` or `vite run-many ...`. Query(QueryPlanRequest), - /// The request to run a synthetic task (not defined in the task graph), like `vite lint` or `vite exec ...`. + /// The request to run a synthetic task (not defined in the task graph), e.g., from TaskSynthesizer. Synthetic(SyntheticPlanRequest), } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/additional-envs/package.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/additional-envs/package.json index a3f69d92..670b1e46 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/additional-envs/package.json +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/additional-envs/package.json @@ -2,6 +2,7 @@ "name": "additional-envs", "private": true, "scripts": { - "hello": "echo hello" + "hello": "echo hello", + "env-test": "vite env-test TEST_VAR hello_world" } } diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/additional-envs/snapshots.toml b/crates/vite_task_plan/tests/plan_snapshots/fixtures/additional-envs/snapshots.toml index ac268dc8..326567a0 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/additional-envs/snapshots.toml +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/additional-envs/snapshots.toml @@ -1,18 +1,5 @@ # Tests env-test synthetic command with additional_envs [[plan]] -name = "direct env-test synthetic task" -args = ["env-test", "TEST_VAR", "hello_world"] - -[[e2e]] -name = "env-test prints value from additional_envs" -steps = [ - "vite env-test SYNTHETIC_ENV_VAR test_value_from_synthesizer # prints env value", -] - -[[e2e]] -name = "env-test with different values" -steps = [ - "vite env-test FOO bar # sets FOO=bar", - "vite env-test BAZ qux # sets BAZ=qux", -] +name = "env-test synthetic task in user task" +args = ["run", "env-test"] diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/additional-envs/snapshots/query - direct env-test synthetic task.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/additional-envs/snapshots/query - direct env-test synthetic task.snap deleted file mode 100644 index 9f844c66..00000000 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/additional-envs/snapshots/query - direct env-test synthetic task.snap +++ /dev/null @@ -1,58 +0,0 @@ ---- -source: crates/vite_task_bin/tests/test_snapshots/main.rs -expression: "&plan_json" -input_file: crates/vite_task_bin/tests/test_snapshots/fixtures/additional-envs ---- -{ - "root_node": { - "Leaf": { - "Spawn": { - "cache_metadata": { - "spawn_fingerprint": { - "cwd": "", - "program_fingerprint": { - "OutsideWorkspace": { - "program_name": "print-env" - } - }, - "args": [ - "TEST_VAR" - ], - "env_fingerprints": { - "fingerprinted_envs": {}, - "pass_through_env_config": [ - "TEST_VAR", - "" - ] - }, - "fingerprint_ignores": null - }, - "execution_cache_key": { - "kind": { - "DirectSyntactic": { - "direct_execution_cache_key": [ - "env-test", - "TEST_VAR", - "hello_world" - ] - } - }, - "origin_path": "" - } - }, - "spawn_command": { - "program_path": "/node_modules/.bin/print-env", - "args": [ - "TEST_VAR" - ], - "all_envs": { - "NO_COLOR": "1", - "PATH": "/node_modules/.bin:/node_modules/.bin", - "TEST_VAR": "hello_world" - }, - "cwd": "/" - } - } - } - } -} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/additional-envs/snapshots/query - env-test synthetic task in user task.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/additional-envs/snapshots/query - env-test synthetic task in user task.snap new file mode 100644 index 00000000..ee219d60 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/additional-envs/snapshots/query - env-test synthetic task in user task.snap @@ -0,0 +1,88 @@ +--- +source: crates/vite_task_plan/tests/plan_snapshots/main.rs +expression: "&plan_json" +input_file: crates/vite_task_plan/tests/plan_snapshots/fixtures/additional-envs +--- +{ + "root_node": { + "Expanded": [ + { + "key": [ + "/", + "env-test" + ], + "node": { + "task_display": { + "package_name": "additional-envs", + "task_name": "env-test", + "package_path": "/" + }, + "items": [ + { + "execution_item_display": { + "task_display": { + "package_name": "additional-envs", + "task_name": "env-test", + "package_path": "/" + }, + "command": "vite env-test TEST_VAR hello_world", + "and_item_index": null, + "cwd": "/" + }, + "kind": { + "Leaf": { + "Spawn": { + "cache_metadata": { + "spawn_fingerprint": { + "cwd": "", + "program_fingerprint": { + "OutsideWorkspace": { + "program_name": "print-env" + } + }, + "args": [ + "TEST_VAR" + ], + "env_fingerprints": { + "fingerprinted_envs": {}, + "pass_through_env_config": [ + "TEST_VAR", + "" + ] + }, + "fingerprint_ignores": null + }, + "execution_cache_key": { + "kind": { + "UserTask": { + "task_name": "env-test", + "and_item_index": 0, + "extra_args": [] + } + }, + "origin_path": "" + } + }, + "spawn_command": { + "program_path": "/node_modules/.bin/print-env", + "args": [ + "TEST_VAR" + ], + "all_envs": { + "NO_COLOR": "1", + "PATH": "/node_modules/.bin:/node_modules/.bin", + "TEST_VAR": "hello_world" + }, + "cwd": "/" + } + } + } + } + } + ] + }, + "neighbors": [] + } + ] + } +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/additional-envs/snapshots/task graph.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/additional-envs/snapshots/task graph.snap index ad0b5839..72be1051 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/additional-envs/snapshots/task graph.snap +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/additional-envs/snapshots/task graph.snap @@ -1,9 +1,37 @@ --- -source: crates/vite_task_bin/tests/test_snapshots/main.rs +source: crates/vite_task_plan/tests/plan_snapshots/main.rs expression: task_graph_json -input_file: crates/vite_task_bin/tests/test_snapshots/fixtures/additional-envs +input_file: crates/vite_task_plan/tests/plan_snapshots/fixtures/additional-envs --- [ + { + "key": [ + "/", + "env-test" + ], + "node": { + "task_display": { + "package_name": "additional-envs", + "task_name": "env-test", + "package_path": "/" + }, + "resolved_config": { + "command": "vite env-test TEST_VAR hello_world", + "resolved_options": { + "cwd": "/", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "pass_through_envs": [ + "" + ] + } + } + } + } + }, + "neighbors": [] + }, { "key": [ "/", diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots.toml b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots.toml index c4e8c96d..610b3d59 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots.toml +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots.toml @@ -13,17 +13,9 @@ name = "normal task with extra args" args = ["run", "hello", "a.txt"] [[plan]] -name = "direct synthetic task" -args = ["lint"] - -[[plan]] -name = "direct synthetic task with cwd" +name = "synthetic task in user task with cwd" cwd = "subdir" -args = ["lint"] - -[[plan]] -name = "direct synthetic task with extra args" -args = ["lint", "--fix"] +args = ["run", "lint"] [[plan]] name = "lint and echo with extra args" @@ -36,7 +28,7 @@ args = ["run", "echo-and-lint", "--fix"] [[e2e]] name = "direct lint" steps = [ - "vite lint # cache miss", + "vite run lint # cache miss", "echo debugger > main.js # add lint error", - "vite lint # cache miss, lint fails", + "vite run lint # cache miss, lint fails", ] diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - direct synthetic task with cwd.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - direct synthetic task with cwd.snap deleted file mode 100644 index 9c47d777..00000000 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - direct synthetic task with cwd.snap +++ /dev/null @@ -1,50 +0,0 @@ ---- -source: crates/vite_task_bin/tests/test_snapshots/main.rs -expression: "&plan_json" -input_file: crates/vite_task_bin/tests/test_snapshots/fixtures/cache-keys ---- -{ - "root_node": { - "Leaf": { - "Spawn": { - "cache_metadata": { - "spawn_fingerprint": { - "cwd": "subdir", - "program_fingerprint": { - "OutsideWorkspace": { - "program_name": "oxlint" - } - }, - "args": [], - "env_fingerprints": { - "fingerprinted_envs": {}, - "pass_through_env_config": [ - "" - ] - }, - "fingerprint_ignores": null - }, - "execution_cache_key": { - "kind": { - "DirectSyntactic": { - "direct_execution_cache_key": [ - "lint" - ] - } - }, - "origin_path": "subdir" - } - }, - "spawn_command": { - "program_path": "/node_modules/.bin/oxlint", - "args": [], - "all_envs": { - "NO_COLOR": "1", - "PATH": "/node_modules/.bin:/node_modules/.bin" - }, - "cwd": "/subdir" - } - } - } - } -} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - direct synthetic task with extra args.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - direct synthetic task with extra args.snap deleted file mode 100644 index 5beaeec6..00000000 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - direct synthetic task with extra args.snap +++ /dev/null @@ -1,55 +0,0 @@ ---- -source: crates/vite_task_bin/tests/test_snapshots/main.rs -expression: "&plan_json" -input_file: crates/vite_task_bin/tests/test_snapshots/fixtures/cache-keys ---- -{ - "root_node": { - "Leaf": { - "Spawn": { - "cache_metadata": { - "spawn_fingerprint": { - "cwd": "", - "program_fingerprint": { - "OutsideWorkspace": { - "program_name": "oxlint" - } - }, - "args": [ - "--fix" - ], - "env_fingerprints": { - "fingerprinted_envs": {}, - "pass_through_env_config": [ - "" - ] - }, - "fingerprint_ignores": null - }, - "execution_cache_key": { - "kind": { - "DirectSyntactic": { - "direct_execution_cache_key": [ - "lint", - "--fix" - ] - } - }, - "origin_path": "" - } - }, - "spawn_command": { - "program_path": "/node_modules/.bin/oxlint", - "args": [ - "--fix" - ], - "all_envs": { - "NO_COLOR": "1", - "PATH": "/node_modules/.bin:/node_modules/.bin" - }, - "cwd": "/" - } - } - } - } -} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - direct synthetic task.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - direct synthetic task.snap deleted file mode 100644 index b13900b4..00000000 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - direct synthetic task.snap +++ /dev/null @@ -1,50 +0,0 @@ ---- -source: crates/vite_task_bin/tests/test_snapshots/main.rs -expression: "&plan_json" -input_file: crates/vite_task_bin/tests/test_snapshots/fixtures/cache-keys ---- -{ - "root_node": { - "Leaf": { - "Spawn": { - "cache_metadata": { - "spawn_fingerprint": { - "cwd": "", - "program_fingerprint": { - "OutsideWorkspace": { - "program_name": "oxlint" - } - }, - "args": [], - "env_fingerprints": { - "fingerprinted_envs": {}, - "pass_through_env_config": [ - "" - ] - }, - "fingerprint_ignores": null - }, - "execution_cache_key": { - "kind": { - "DirectSyntactic": { - "direct_execution_cache_key": [ - "lint" - ] - } - }, - "origin_path": "" - } - }, - "spawn_command": { - "program_path": "/node_modules/.bin/oxlint", - "args": [], - "all_envs": { - "NO_COLOR": "1", - "PATH": "/node_modules/.bin:/node_modules/.bin" - }, - "cwd": "/" - } - } - } - } -} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - echo and lint with extra args.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - echo and lint with extra args.snap index 1643cb47..79957604 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - echo and lint with extra args.snap +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - echo and lint with extra args.snap @@ -1,7 +1,7 @@ --- -source: crates/vite_task_bin/tests/test_snapshots/main.rs +source: crates/vite_task_plan/tests/plan_snapshots/main.rs expression: "&plan_json" -input_file: crates/vite_task_bin/tests/test_snapshots/fixtures/cache-keys +input_file: crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys --- { "root_node": { diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - lint and echo with extra args.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - lint and echo with extra args.snap index c95a028d..8d463b85 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - lint and echo with extra args.snap +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - lint and echo with extra args.snap @@ -1,7 +1,7 @@ --- -source: crates/vite_task_bin/tests/test_snapshots/main.rs +source: crates/vite_task_plan/tests/plan_snapshots/main.rs expression: "&plan_json" -input_file: crates/vite_task_bin/tests/test_snapshots/fixtures/cache-keys +input_file: crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys --- { "root_node": { diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - normal task with extra args.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - normal task with extra args.snap index 8b4a858c..ab892bb3 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - normal task with extra args.snap +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - normal task with extra args.snap @@ -1,7 +1,7 @@ --- -source: crates/vite_task_bin/tests/test_snapshots/main.rs +source: crates/vite_task_plan/tests/plan_snapshots/main.rs expression: "&plan_json" -input_file: crates/vite_task_bin/tests/test_snapshots/fixtures/cache-keys +input_file: crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys --- { "root_node": { diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - synthetic task in user task with cwd.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - synthetic task in user task with cwd.snap new file mode 100644 index 00000000..cd423306 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - synthetic task in user task with cwd.snap @@ -0,0 +1,82 @@ +--- +source: crates/vite_task_plan/tests/plan_snapshots/main.rs +expression: "&plan_json" +input_file: crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys +--- +{ + "root_node": { + "Expanded": [ + { + "key": [ + "/", + "lint" + ], + "node": { + "task_display": { + "package_name": "", + "task_name": "lint", + "package_path": "/" + }, + "items": [ + { + "execution_item_display": { + "task_display": { + "package_name": "", + "task_name": "lint", + "package_path": "/" + }, + "command": "vite lint", + "and_item_index": null, + "cwd": "/" + }, + "kind": { + "Leaf": { + "Spawn": { + "cache_metadata": { + "spawn_fingerprint": { + "cwd": "", + "program_fingerprint": { + "OutsideWorkspace": { + "program_name": "oxlint" + } + }, + "args": [], + "env_fingerprints": { + "fingerprinted_envs": {}, + "pass_through_env_config": [ + "" + ] + }, + "fingerprint_ignores": null + }, + "execution_cache_key": { + "kind": { + "UserTask": { + "task_name": "lint", + "and_item_index": 0, + "extra_args": [] + } + }, + "origin_path": "" + } + }, + "spawn_command": { + "program_path": "/node_modules/.bin/oxlint", + "args": [], + "all_envs": { + "NO_COLOR": "1", + "PATH": "/node_modules/.bin:/node_modules/.bin" + }, + "cwd": "/" + } + } + } + } + } + ] + }, + "neighbors": [] + } + ] + } +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - synthetic task in user task.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - synthetic task in user task.snap index 5d46256e..cd423306 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - synthetic task in user task.snap +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - synthetic task in user task.snap @@ -1,7 +1,7 @@ --- -source: crates/vite_task_bin/tests/test_snapshots/main.rs +source: crates/vite_task_plan/tests/plan_snapshots/main.rs expression: "&plan_json" -input_file: crates/vite_task_bin/tests/test_snapshots/fixtures/cache-keys +input_file: crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys --- { "root_node": { diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - synthetic task with extra args in user task.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - synthetic task with extra args in user task.snap index bbea6ce6..1da71fa3 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - synthetic task with extra args in user task.snap +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - synthetic task with extra args in user task.snap @@ -1,7 +1,7 @@ --- -source: crates/vite_task_bin/tests/test_snapshots/main.rs +source: crates/vite_task_plan/tests/plan_snapshots/main.rs expression: "&plan_json" -input_file: crates/vite_task_bin/tests/test_snapshots/fixtures/cache-keys +input_file: crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys --- { "root_node": { diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/task graph.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/task graph.snap index e62a6bce..146b2bfd 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/task graph.snap +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/task graph.snap @@ -1,7 +1,7 @@ --- -source: crates/vite_task_bin/tests/test_snapshots/main.rs +source: crates/vite_task_plan/tests/plan_snapshots/main.rs expression: task_graph_json -input_file: crates/vite_task_bin/tests/test_snapshots/fixtures/cache-keys +input_file: crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys --- [ { diff --git a/crates/vite_task_plan/tests/plan_snapshots/main.rs b/crates/vite_task_plan/tests/plan_snapshots/main.rs index 0677eeac..a39f4dde 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/main.rs +++ b/crates/vite_task_plan/tests/plan_snapshots/main.rs @@ -1,16 +1,24 @@ mod redact; -use std::{collections::HashMap, convert::Infallible, ffi::OsStr, path::Path, sync::Arc}; +use std::{collections::HashMap, ffi::OsStr, path::Path, sync::Arc}; +use clap::Parser; use copy_dir::copy_dir; use redact::redact_snapshot; use tokio::runtime::Runtime; use vite_path::{AbsolutePath, AbsolutePathBuf, RelativePathBuf}; use vite_str::Str; -use vite_task::{CLIArgs, Session}; -use vite_task_bin::CustomTaskSubcommand; +use vite_task::{BuiltInCommand, Session}; use vite_workspace::find_workspace_root; +/// Local parser wrapper for BuiltInCommand +#[derive(Parser)] +#[command(name = "vite")] +enum Cli { + #[clap(flatten)] + Command(BuiltInCommand), +} + #[derive(serde::Deserialize, Debug)] struct Plan { pub name: Str, @@ -118,7 +126,7 @@ fn run_case_inner( for plan in cases_file.plan_cases { let snapshot_name = format!("query - {}", plan.name); - let cli_args = match CLIArgs::::try_parse_from( + let cli = match Cli::try_parse_from( std::iter::once("vite") // dummy program name .chain(plan.args.iter().map(|s| s.as_str())), ) { @@ -128,14 +136,10 @@ fn run_case_inner( continue; } }; - let task_cli_args = match cli_args { - CLIArgs::Task(task_cli_args) => task_cli_args, - CLIArgs::NonTask(never) => match never {}, - }; + let Cli::Command(command) = cli; - let plan_result = session - .plan_from_cli(workspace_root.path.join(plan.cwd).into(), task_cli_args) - .await; + let plan_result = + session.plan_from_cli(workspace_root.path.join(plan.cwd).into(), command).await; let plan = match plan_result { Ok(plan) => plan, From 5207ad4ea7ea0f3e89b7088eba03478d7099f94d Mon Sep 17 00:00:00 2001 From: branchseer Date: Wed, 4 Feb 2026 16:36:52 +0800 Subject: [PATCH 02/10] refactor: rename BuiltInCommand to Command and update references --- crates/vite_task/src/cli/mod.rs | 8 ++++---- crates/vite_task/src/lib.rs | 2 +- crates/vite_task/src/session/mod.rs | 15 +++++---------- crates/vite_task_bin/src/main.rs | 4 ++-- .../vite_task_plan/tests/plan_snapshots/main.rs | 4 ++-- 5 files changed, 14 insertions(+), 19 deletions(-) diff --git a/crates/vite_task/src/cli/mod.rs b/crates/vite_task/src/cli/mod.rs index e6db9581..d1ec7257 100644 --- a/crates/vite_task/src/cli/mod.rs +++ b/crates/vite_task/src/cli/mod.rs @@ -1,14 +1,14 @@ use std::sync::Arc; -use clap::Subcommand; +use clap::Parser; use vite_path::AbsolutePath; use vite_str::Str; use vite_task_graph::{TaskSpecifier, query::TaskQueryKind}; use vite_task_plan::plan_request::{PlanOptions, PlanRequest, QueryPlanRequest}; /// vite task CLI subcommands -#[derive(Debug, Subcommand)] -pub enum BuiltInCommand { +#[derive(Debug, Parser)] +pub enum Command { /// Run tasks Run { /// `packageName#taskName` or `taskName`. @@ -41,7 +41,7 @@ pub enum CLITaskQueryError { PackageNameSpecifiedWithRecursive { package_name: Str, task_name: Str }, } -impl BuiltInCommand { +impl Command { /// Convert to `PlanRequest`, or return an error if invalid. pub fn into_plan_request( self, diff --git a/crates/vite_task/src/lib.rs b/crates/vite_task/src/lib.rs index e1aeed36..0d45d081 100644 --- a/crates/vite_task/src/lib.rs +++ b/crates/vite_task/src/lib.rs @@ -4,7 +4,7 @@ mod maybe_str; pub mod session; // Public exports for vite_task_bin -pub use cli::BuiltInCommand; +pub use cli::Command; pub use session::{LabeledReporter, Reporter, Session, SessionCallbacks, TaskSynthesizer}; pub use vite_task_graph::{ config::{ diff --git a/crates/vite_task/src/session/mod.rs b/crates/vite_task/src/session/mod.rs index 76bb02ce..a9d86555 100644 --- a/crates/vite_task/src/session/mod.rs +++ b/crates/vite_task/src/session/mod.rs @@ -22,7 +22,7 @@ use vite_task_plan::{ }; use vite_workspace::{WorkspaceRoot, find_workspace_root}; -use crate::{cli::BuiltInCommand, collections::HashMap}; +use crate::{cli::Command, collections::HashMap}; #[derive(Debug)] enum LazyTaskGraph<'a> { @@ -100,14 +100,9 @@ impl vite_task_plan::PlanRequestParser for PlanRequestParser<'_> { } // Try built-in "run" command (handles "vite run build" in scripts) - #[derive(Parser)] - enum BuiltInParser { - #[clap(flatten)] - Command(BuiltInCommand), - } - if let Ok(BuiltInParser::Command(built_in)) = BuiltInParser::try_parse_from( - std::iter::once(program).chain(args.iter().map(Str::as_str)), - ) { + if let Ok(built_in) = + Command::try_parse_from(std::iter::once(program).chain(args.iter().map(Str::as_str))) + { return Ok(Some(built_in.into_plan_request(cwd)?)); } @@ -223,7 +218,7 @@ impl<'a> Session<'a> { pub async fn plan_from_cli( &mut self, cwd: Arc, - command: BuiltInCommand, + command: Command, ) -> Result { let plan_request = command.into_plan_request(&cwd).map_err(|error| { TaskPlanErrorKind::ParsePlanRequestError { diff --git a/crates/vite_task_bin/src/main.rs b/crates/vite_task_bin/src/main.rs index 7f2ce8c5..f896ca93 100644 --- a/crates/vite_task_bin/src/main.rs +++ b/crates/vite_task_bin/src/main.rs @@ -3,7 +3,7 @@ use std::{process::ExitCode, sync::Arc}; use clap::Parser; use vite_path::{AbsolutePath, current_dir}; use vite_task::{ - BuiltInCommand, Session, + Command, Session, session::reporter::{ExitStatus, LabeledReporter}, }; use vite_task_bin::OwnedSessionCallbacks; @@ -12,7 +12,7 @@ use vite_task_bin::OwnedSessionCallbacks; #[command(name = "vite", version)] struct Cli { #[command(subcommand)] - command: BuiltInCommand, + command: Command, } #[tokio::main] diff --git a/crates/vite_task_plan/tests/plan_snapshots/main.rs b/crates/vite_task_plan/tests/plan_snapshots/main.rs index a39f4dde..3b5364b9 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/main.rs +++ b/crates/vite_task_plan/tests/plan_snapshots/main.rs @@ -8,7 +8,7 @@ use redact::redact_snapshot; use tokio::runtime::Runtime; use vite_path::{AbsolutePath, AbsolutePathBuf, RelativePathBuf}; use vite_str::Str; -use vite_task::{BuiltInCommand, Session}; +use vite_task::{Command, Session}; use vite_workspace::find_workspace_root; /// Local parser wrapper for BuiltInCommand @@ -16,7 +16,7 @@ use vite_workspace::find_workspace_root; #[command(name = "vite")] enum Cli { #[clap(flatten)] - Command(BuiltInCommand), + Command(Command), } #[derive(serde::Deserialize, Debug)] From 297e65408a0b6b9dd644c10e693184ce038e55e9 Mon Sep 17 00:00:00 2001 From: branchseer Date: Wed, 4 Feb 2026 14:51:22 +0800 Subject: [PATCH 03/10] refactor: simplify CLI and Session API by removing generic type parameters Remove generic CustomSubcommand type parameter from Session, SessionCallbacks, PlanRequestParser, and TaskSynthesizer. The CLI only supports `run` as a top-level subcommand; synthetic tasks (lint, test, env-test) are now handled purely by TaskSynthesizer via string matching on program name and args. Key changes: - TaskSynthesizer trait takes (program, args) directly instead of a typed subcommand - Session/SessionCallbacks/PlanRequestParser no longer parameterized by subcommand type - plan_from_cli accepts BuiltInCommand directly (removed CLIArgs, TaskCLIArgs, ParsedTaskCLIArgs) - Binary crate owns its top-level Cli parser wrapping BuiltInCommand - E2E test fixtures converted from `vite lint`/`vite env-test` to `vite run ` - Removed same-name-as-builtin fixture (tested now-removed CLI lint vs run distinction) - Added diagnostic block sorting in E2E output redaction for deterministic snapshots Co-Authored-By: Claude Opus 4.5 --- .../e2e_snapshots/fixtures/same-name-as-builtin/vite-task.json | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 crates/vite_task_bin/tests/e2e_snapshots/fixtures/same-name-as-builtin/vite-task.json diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/same-name-as-builtin/vite-task.json b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/same-name-as-builtin/vite-task.json deleted file mode 100644 index 1d0fe9f2..00000000 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/same-name-as-builtin/vite-task.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "cacheScripts": true -} From 5c62346895c45e76ddb998dbfa71b6eb40596d20 Mon Sep 17 00:00:00 2001 From: branchseer Date: Fri, 6 Feb 2026 16:28:41 +0800 Subject: [PATCH 04/10] refactor: introduce ScriptCommand struct and refine CommandHandler API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Group program/args/envs/cwd into a ScriptCommand struct passed by &mut to both CommandHandler::handle_command and PlanRequestParser::get_plan_request. Handlers can now mutate command fields in-place (e.g., rewriting vpr → vite run) or return a Synthesized plan request. Replace old ViteTaskCommand variant with NotSynthesized { is_vite_task_entry } and use clap's has_subcommand() for subcommand detection. Co-Authored-By: Claude Opus 4.6 --- crates/vite_task/src/lib.rs | 8 +- crates/vite_task/src/session/mod.rs | 97 +++++++++++-------- crates/vite_task_bin/src/lib.rs | 66 +++++++------ crates/vite_task_plan/src/lib.rs | 16 +-- crates/vite_task_plan/src/plan.rs | 35 ++++--- crates/vite_task_plan/src/plan_request.rs | 14 +++ .../fixtures/vpr-shorthand/package.json | 6 ++ .../fixtures/vpr-shorthand/snapshots.toml | 3 + .../query - vpr expands to vite run.snap | 85 ++++++++++++++++ .../vpr-shorthand/snapshots/task graph.snap | 63 ++++++++++++ .../fixtures/vpr-shorthand/vite-task.json | 3 + .../tests/plan_snapshots/main.rs | 2 +- 12 files changed, 306 insertions(+), 92 deletions(-) create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/vpr-shorthand/package.json create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/vpr-shorthand/snapshots.toml create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/vpr-shorthand/snapshots/query - vpr expands to vite run.snap create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/vpr-shorthand/snapshots/task graph.snap create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/vpr-shorthand/vite-task.json diff --git a/crates/vite_task/src/lib.rs b/crates/vite_task/src/lib.rs index 0d45d081..61311f25 100644 --- a/crates/vite_task/src/lib.rs +++ b/crates/vite_task/src/lib.rs @@ -5,7 +5,9 @@ pub mod session; // Public exports for vite_task_bin pub use cli::Command; -pub use session::{LabeledReporter, Reporter, Session, SessionCallbacks, TaskSynthesizer}; +pub use session::{ + CommandHandler, HandledCommand, LabeledReporter, Reporter, Session, SessionCallbacks, +}; pub use vite_task_graph::{ config::{ self, @@ -13,6 +15,6 @@ pub use vite_task_graph::{ }, loader, }; -/// get_path_env is useful for TaskSynthesizer implementations. Re-export it here. +/// Re-exports useful for CommandHandler implementations. pub use vite_task_plan::get_path_env; -pub use vite_task_plan::plan_request; +pub use vite_task_plan::{plan_request, plan_request::ScriptCommand}; diff --git a/crates/vite_task/src/session/mod.rs b/crates/vite_task/src/session/mod.rs index a9d86555..7eabb4e3 100644 --- a/crates/vite_task/src/session/mod.rs +++ b/crates/vite_task/src/session/mod.rs @@ -6,9 +6,10 @@ pub mod reporter; // Re-export types that are part of the public API use std::{ffi::OsStr, fmt::Debug, sync::Arc}; +use anyhow::Context; use cache::ExecutionCache; pub use cache::{CacheMiss, FingerprintMismatch}; -use clap::Parser; +use clap::{Parser, Subcommand as _}; pub use event::ExecutionEvent; use once_cell::sync::OnceCell; pub use reporter::{LabeledReporter, Reporter}; @@ -17,7 +18,7 @@ use vite_str::Str; use vite_task_graph::{IndexedTaskGraph, TaskGraph, TaskGraphLoadError, loader::UserConfigLoader}; use vite_task_plan::{ ExecutionPlan, TaskGraphLoader, TaskPlanErrorKind, - plan_request::{PlanRequest, SyntheticPlanRequest}, + plan_request::{PlanRequest, ScriptCommand, SyntheticPlanRequest}, prepend_path_env, }; use vite_workspace::{WorkspaceRoot, find_workspace_root}; @@ -50,63 +51,79 @@ impl TaskGraphLoader for LazyTaskGraph<'_> { } pub struct SessionCallbacks<'a> { - pub task_synthesizer: &'a mut (dyn TaskSynthesizer + 'a), + pub command_handler: &'a mut (dyn CommandHandler + 'a), pub user_config_loader: &'a mut (dyn UserConfigLoader + 'a), } -/// Handles synthesizing task plan requests from commands found in task scripts. +/// The result of a [`CommandHandler::handle_command`] call. +#[derive(Debug)] +pub enum HandledCommand { + /// The command was synthesized into a task (e.g., `vite lint` → `oxlint`). + Synthesized(SyntheticPlanRequest), + /// The command was not synthesized. + NotSynthesized { + /// Whether this program is the task runner's own entry point. + /// If `true`, `get_plan_request` will check if the first arg is a known + /// subcommand (via [`Command::has_subcommand`]) and parse it as a CLI command. + is_vite_task_entry: bool, + }, +} + +/// Handles commands found in task scripts to determine how they should be executed. /// -/// When a task's command references a known program (e.g., `vite lint` in a script), -/// the synthesizer converts it into a `SyntheticPlanRequest` for execution. +/// Since `vite_task` doesn't know the name of its own binary, it relies on the caller +/// to identify which commands are vite-task commands. Return [`HandledCommand::NotSynthesized`] +/// with `is_vite_task_entry: true` to let vite-task check if the args match a CLI subcommand, +/// or [`HandledCommand::Synthesized`] to provide a synthetic task directly. #[async_trait::async_trait(?Send)] -pub trait TaskSynthesizer: Debug { - /// Called for every command in task scripts to determine if it should be synthesized. +pub trait CommandHandler: Debug { + /// Called for every command in task scripts to determine how it should be handled. /// - /// - `program` is the program name (e.g., `"vite"`). - /// - `args` is all arguments after the program (e.g., `["lint", "--fix"]`). - /// - `envs` is the current environment variables where the task is being planned. - /// - `cwd` is the current working directory where the task is being planned. + /// The implementation can either: + /// - Return `Synthesized(...)` to replace the command with a synthetic task. + /// - Return `NotSynthesized { is_vite_task_entry }` and optionally mutate `command` + /// to modify how the command is executed as a normal process. /// - /// Returns `Ok(Some(request))` if the command is recognized and should be synthesized, - /// `Ok(None)` if the command should be executed as a normal process. - async fn synthesize_task( + /// If `Synthesized` is returned, any mutations to `command` are discarded. + async fn handle_command( &mut self, - program: &str, - args: &[Str], - envs: &Arc, Arc>>, - cwd: &Arc, - ) -> anyhow::Result>; + command: &mut ScriptCommand, + ) -> anyhow::Result; } #[derive(derive_more::Debug)] struct PlanRequestParser<'a> { - task_synthesizer: &'a mut (dyn TaskSynthesizer + 'a), + command_handler: &'a mut (dyn CommandHandler + 'a), } #[async_trait::async_trait(?Send)] impl vite_task_plan::PlanRequestParser for PlanRequestParser<'_> { async fn get_plan_request( &mut self, - program: &str, - args: &[Str], - envs: &Arc, Arc>>, - cwd: &Arc, + command: &mut ScriptCommand, ) -> anyhow::Result> { - // Try task synthesizer first (handles e.g. "vite lint" in scripts) - if let Some(synthetic) = - self.task_synthesizer.synthesize_task(program, args, envs, cwd).await? - { - return Ok(Some(PlanRequest::Synthetic(synthetic))); - } - - // Try built-in "run" command (handles "vite run build" in scripts) - if let Ok(built_in) = - Command::try_parse_from(std::iter::once(program).chain(args.iter().map(Str::as_str))) - { - return Ok(Some(built_in.into_plan_request(cwd)?)); + match self.command_handler.handle_command(command).await? { + HandledCommand::Synthesized(synthetic) => Ok(Some(PlanRequest::Synthetic(synthetic))), + HandledCommand::NotSynthesized { is_vite_task_entry: true } + if command + .args + .first() + .is_some_and(|arg| Command::has_subcommand(arg.as_str())) => + { + let cli_command = Command::try_parse_from( + std::iter::once(command.program.as_str()) + .chain(command.args.iter().map(Str::as_str)), + ) + .with_context(|| { + vite_str::format!( + "Failed to parse vite-task command from args: {:?}", + command.args + ) + })?; + Ok(Some(cli_command.into_plan_request(&command.cwd)?)) + } + HandledCommand::NotSynthesized { .. } => Ok(None), } - - Ok(None) } } @@ -175,7 +192,7 @@ impl<'a> Session<'a> { }, envs: Arc::new(envs), cwd, - plan_request_parser: PlanRequestParser { task_synthesizer: callbacks.task_synthesizer }, + plan_request_parser: PlanRequestParser { command_handler: callbacks.command_handler }, cache: OnceCell::new(), cache_path, }) diff --git a/crates/vite_task_bin/src/lib.rs b/crates/vite_task_bin/src/lib.rs index f206be52..7221207a 100644 --- a/crates/vite_task_bin/src/lib.rs +++ b/crates/vite_task_bin/src/lib.rs @@ -10,12 +10,12 @@ use std::{ use vite_path::AbsolutePath; use vite_str::Str; use vite_task::{ - EnabledCacheConfig, SessionCallbacks, UserCacheConfig, UserTaskOptions, get_path_env, - plan_request::SyntheticPlanRequest, + EnabledCacheConfig, HandledCommand, ScriptCommand, SessionCallbacks, UserCacheConfig, + UserTaskOptions, get_path_env, plan_request::SyntheticPlanRequest, }; #[derive(Debug, Default)] -pub struct TaskSynthesizer(()); +pub struct CommandHandler(()); fn find_executable( path_env: Option<&Arc>, @@ -57,28 +57,40 @@ fn synthesize_node_modules_bin_task( } #[async_trait::async_trait(?Send)] -impl vite_task::TaskSynthesizer for TaskSynthesizer { - async fn synthesize_task( +impl vite_task::CommandHandler for CommandHandler { + async fn handle_command( &mut self, - program: &str, - args: &[Str], - envs: &Arc, Arc>>, - cwd: &Arc, - ) -> anyhow::Result> { - if program != "vite" { - return Ok(None); + command: &mut ScriptCommand, + ) -> anyhow::Result { + match command.program.as_str() { + "vite" => {} + // `vpr ` is shorthand for `vite run ` + "vpr" => { + command.program = Str::from("vite"); + command.args = + iter::once(Str::from("run")).chain(command.args.iter().cloned()).collect(); + } + _ => return Ok(HandledCommand::NotSynthesized { is_vite_task_entry: false }), } - let Some(subcommand) = args.first() else { - return Ok(None); + let Some(subcommand) = command.args.first().cloned() else { + return Ok(HandledCommand::NotSynthesized { is_vite_task_entry: true }); }; - let rest = &args[1..]; + let rest = &command.args[1..]; match subcommand.as_str() { - "lint" => { - Ok(Some(synthesize_node_modules_bin_task("lint", "oxlint", rest, envs, cwd)?)) - } - "test" => { - Ok(Some(synthesize_node_modules_bin_task("test", "vitest", rest, envs, cwd)?)) - } + "lint" => Ok(HandledCommand::Synthesized(synthesize_node_modules_bin_task( + "lint", + "oxlint", + rest, + &command.envs, + &command.cwd, + )?)), + "test" => Ok(HandledCommand::Synthesized(synthesize_node_modules_bin_task( + "test", + "vitest", + rest, + &command.envs, + &command.cwd, + )?)), "env-test" => { let name = rest .first() @@ -92,14 +104,14 @@ impl vite_task::TaskSynthesizer for TaskSynthesizer { let direct_execution_cache_key: Arc<[Str]> = [Str::from("env-test"), name.clone(), value.clone()].into(); - let mut envs = HashMap::clone(&envs); + let mut envs = HashMap::clone(&command.envs); envs.insert( Arc::from(OsStr::new(name.as_str())), Arc::from(OsStr::new(value.as_str())), ); - Ok(Some(SyntheticPlanRequest { - program: find_executable(get_path_env(&envs), &*cwd, "print-env")?, + Ok(HandledCommand::Synthesized(SyntheticPlanRequest { + program: find_executable(get_path_env(&envs), &*command.cwd, "print-env")?, args: [name.clone()].into(), task_options: UserTaskOptions { cache_config: UserCacheConfig::Enabled { @@ -115,7 +127,7 @@ impl vite_task::TaskSynthesizer for TaskSynthesizer { envs: Arc::new(envs), })) } - _ => Ok(None), + _ => Ok(HandledCommand::NotSynthesized { is_vite_task_entry: true }), } } } @@ -149,14 +161,14 @@ impl vite_task::loader::UserConfigLoader for JsonUserConfigLoader { #[derive(Default)] pub struct OwnedSessionCallbacks { - task_synthesizer: TaskSynthesizer, + command_handler: CommandHandler, user_config_loader: JsonUserConfigLoader, } impl OwnedSessionCallbacks { pub fn as_callbacks(&mut self) -> SessionCallbacks<'_> { SessionCallbacks { - task_synthesizer: &mut self.task_synthesizer, + command_handler: &mut self.command_handler, user_config_loader: &mut self.user_config_loader, } } diff --git a/crates/vite_task_plan/src/lib.rs b/crates/vite_task_plan/src/lib.rs index 3bde78ef..90a1804e 100644 --- a/crates/vite_task_plan/src/lib.rs +++ b/crates/vite_task_plan/src/lib.rs @@ -147,7 +147,7 @@ pub enum ExecutionItemKind { Leaf(LeafExecutionKind), } -/// The callback trait for parsing plan requests from cli args. +/// The callback trait for parsing plan requests from script commands. /// See the method for details. #[async_trait::async_trait(?Send)] pub trait PlanRequestParser: Debug { @@ -155,16 +155,16 @@ pub trait PlanRequestParser: Debug { /// /// `vite_task_plan` doesn't have the knowledge of how cli args should be parsed. It relies on this callback. /// + /// The implementation can either mutate `command` or return a `PlanRequest`: /// - If it returns `Err`, the planning will abort with the returned error. - /// - If it returns `Ok(None)`, the command will be spawned as a normal process. - /// - If it returns `Ok(Some(PlanRequest::Query)`, the command will be expanded as a `ExpandedExecution` with a task graph queried from the returned `TaskQuery`. - /// - If it returns `Ok(Some(PlanRequest::Synthetic)`, the command will become a `SpawnExecution` with the synthetic task. + /// - If it returns `Ok(None)`, the (potentially mutated) `command` will be spawned as a normal process. + /// - If it returns `Ok(Some(PlanRequest::Query))`, the command will be expanded as a `ExpandedExecution` with a task graph queried from the returned `TaskQuery`. + /// - If it returns `Ok(Some(PlanRequest::Synthetic))`, the command will become a `SpawnExecution` with the synthetic task. + /// + /// When a `PlanRequest` is returned, any mutations to `command` are discarded. async fn get_plan_request( &mut self, - program: &str, - args: &[Str], - envs: &Arc, Arc>>, - cwd: &Arc, + command: &mut plan_request::ScriptCommand, ) -> anyhow::Result>; } diff --git a/crates/vite_task_plan/src/plan.rs b/crates/vite_task_plan/src/plan.rs index 80e776de..96ffef6c 100644 --- a/crates/vite_task_plan/src/plan.rs +++ b/crates/vite_task_plan/src/plan.rs @@ -28,7 +28,7 @@ use crate::{ execution_graph::{ExecutionGraph, ExecutionNodeIndex}, in_process::InProcessExecution, path_env::get_path_env, - plan_request::{PlanRequest, QueryPlanRequest, SyntheticPlanRequest}, + plan_request::{PlanRequest, QueryPlanRequest, ScriptCommand, SyntheticPlanRequest}, }; /// Locate the executable path for a given program name in the provided envs and cwd. @@ -162,14 +162,20 @@ async fn plan_task_as_execution_node( // 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(&and_item.program, &args, &envs, &cwd) + .get_plan_request(&mut script_command) .await .map_err(|error| TaskPlanErrorKind::ParsePlanRequestError { - program: and_item.program.clone(), - args: args.clone().into(), - cwd: Arc::clone(&cwd), + program: script_command.program.clone(), + args: Arc::clone(&script_command.args), + cwd: Arc::clone(&script_command.cwd), error, }) .with_plan_context(&context)?; @@ -182,7 +188,7 @@ async fn plan_task_as_execution_node( let execution_graph = plan_query_request(query_plan_request, context).await?; ExecutionItemKind::Expanded(execution_graph) } - // Synthetic task (from TaskSynthesizer) + // Synthetic task (from CommandHandler) Some(PlanRequest::Synthetic(synthetic_plan_request)) => { let spawn_execution = plan_synthetic_request( context.workspace_path(), @@ -194,20 +200,23 @@ async fn plan_task_as_execution_node( .with_plan_context(&context)?; ExecutionItemKind::Leaf(LeafExecutionKind::Spawn(spawn_execution)) } - // Normal 3rd party tool command (like `tsc --noEmit`) + // Normal 3rd party tool command (like `tsc --noEmit`), using potentially mutated script_command None => { - let program_path = - which(&OsStr::new(&and_item.program).into(), context.envs(), &cwd) - .map_err(TaskPlanErrorKind::ProgramNotFound) - .with_plan_context(&context)?; + let program_path = which( + &OsStr::new(&script_command.program).into(), + &script_command.envs, + &script_command.cwd, + ) + .map_err(TaskPlanErrorKind::ProgramNotFound) + .with_plan_context(&context)?; let spawn_execution = plan_spawn_execution( context.workspace_path(), task_execution_cache_key, &and_item.envs, &task_node.resolved_config.resolved_options, - context.envs(), + &script_command.envs, program_path, - args.into(), + script_command.args, ) .with_plan_context(&context)?; ExecutionItemKind::Leaf(LeafExecutionKind::Spawn(spawn_execution)) diff --git a/crates/vite_task_plan/src/plan_request.rs b/crates/vite_task_plan/src/plan_request.rs index 9a105694..ac1f86a4 100644 --- a/crates/vite_task_plan/src/plan_request.rs +++ b/crates/vite_task_plan/src/plan_request.rs @@ -1,8 +1,22 @@ use std::{collections::HashMap, ffi::OsStr, sync::Arc}; +use vite_path::AbsolutePath; use vite_str::Str; use vite_task_graph::{config::user::UserTaskOptions, query::TaskQuery}; +/// A parsed command from a task script, passed to [`super::PlanRequestParser::get_plan_request`]. +/// +/// All fields use `Arc` for cheap reassignment. The implementation can mutate +/// these fields to modify how the command is executed when it falls through as a +/// normal process (i.e., when `get_plan_request` returns `None`). +#[derive(Debug)] +pub struct ScriptCommand { + pub program: Str, + pub args: Arc<[Str]>, + pub envs: Arc, Arc>>, + pub cwd: Arc, +} + #[derive(Debug)] pub struct PlanOptions { pub extra_args: Arc<[Str]>, diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/vpr-shorthand/package.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/vpr-shorthand/package.json new file mode 100644 index 00000000..314d6388 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/vpr-shorthand/package.json @@ -0,0 +1,6 @@ +{ + "scripts": { + "build": "echo building", + "all": "vpr build" + } +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/vpr-shorthand/snapshots.toml b/crates/vite_task_plan/tests/plan_snapshots/fixtures/vpr-shorthand/snapshots.toml new file mode 100644 index 00000000..7cb91d86 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/vpr-shorthand/snapshots.toml @@ -0,0 +1,3 @@ +[[plan]] +name = "vpr expands to vite run" +args = ["run", "all"] diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/vpr-shorthand/snapshots/query - vpr expands to vite run.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/vpr-shorthand/snapshots/query - vpr expands to vite run.snap new file mode 100644 index 00000000..5d31eed5 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/vpr-shorthand/snapshots/query - vpr expands to vite run.snap @@ -0,0 +1,85 @@ +--- +source: crates/vite_task_plan/tests/plan_snapshots/main.rs +expression: "&plan_json" +input_file: crates/vite_task_plan/tests/plan_snapshots/fixtures/vpr-shorthand +--- +{ + "root_node": { + "Expanded": [ + { + "key": [ + "/", + "all" + ], + "node": { + "task_display": { + "package_name": "", + "task_name": "all", + "package_path": "/" + }, + "items": [ + { + "execution_item_display": { + "task_display": { + "package_name": "", + "task_name": "all", + "package_path": "/" + }, + "command": "vpr build", + "and_item_index": null, + "cwd": "/" + }, + "kind": { + "Expanded": [ + { + "key": [ + "/", + "build" + ], + "node": { + "task_display": { + "package_name": "", + "task_name": "build", + "package_path": "/" + }, + "items": [ + { + "execution_item_display": { + "task_display": { + "package_name": "", + "task_name": "build", + "package_path": "/" + }, + "command": "echo building", + "and_item_index": null, + "cwd": "/" + }, + "kind": { + "Leaf": { + "InProcess": { + "kind": { + "Echo": { + "strings": [ + "building" + ], + "trailing_newline": true + } + } + } + } + } + } + ] + }, + "neighbors": [] + } + ] + } + } + ] + }, + "neighbors": [] + } + ] + } +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/vpr-shorthand/snapshots/task graph.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/vpr-shorthand/snapshots/task graph.snap new file mode 100644 index 00000000..1dcf808c --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/vpr-shorthand/snapshots/task graph.snap @@ -0,0 +1,63 @@ +--- +source: crates/vite_task_plan/tests/plan_snapshots/main.rs +expression: task_graph_json +input_file: crates/vite_task_plan/tests/plan_snapshots/fixtures/vpr-shorthand +--- +[ + { + "key": [ + "/", + "all" + ], + "node": { + "task_display": { + "package_name": "", + "task_name": "all", + "package_path": "/" + }, + "resolved_config": { + "command": "vpr build", + "resolved_options": { + "cwd": "/", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "pass_through_envs": [ + "" + ] + } + } + } + } + }, + "neighbors": [] + }, + { + "key": [ + "/", + "build" + ], + "node": { + "task_display": { + "package_name": "", + "task_name": "build", + "package_path": "/" + }, + "resolved_config": { + "command": "echo building", + "resolved_options": { + "cwd": "/", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "pass_through_envs": [ + "" + ] + } + } + } + } + }, + "neighbors": [] + } +] diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/vpr-shorthand/vite-task.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/vpr-shorthand/vite-task.json new file mode 100644 index 00000000..1d0fe9f2 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/vpr-shorthand/vite-task.json @@ -0,0 +1,3 @@ +{ + "cacheScripts": true +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/main.rs b/crates/vite_task_plan/tests/plan_snapshots/main.rs index 3b5364b9..99b4d641 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/main.rs +++ b/crates/vite_task_plan/tests/plan_snapshots/main.rs @@ -127,7 +127,7 @@ fn run_case_inner( let snapshot_name = format!("query - {}", plan.name); let cli = match Cli::try_parse_from( - std::iter::once("vite") // dummy program name + std::iter::once("vite-task") // dummy program name .chain(plan.args.iter().map(|s| s.as_str())), ) { Ok(ok) => ok, From c94855e794bd2d8377cedfa2de96d8a428732ca2 Mon Sep 17 00:00:00 2001 From: branchseer Date: Fri, 6 Feb 2026 18:46:49 +0800 Subject: [PATCH 05/10] refactor: replace ExecutionCacheKeyKind with flat ExecutionCacheKey enum Remove ExecutionCacheKeyKind and make ExecutionCacheKey a flat enum with UserTask and Exec variants. The Exec variant derives its cache key from actual execution content (program name, args, cwd) instead of requiring callers to provide an opaque direct_execution_cache_key. This also removes the direct_execution_cache_key field from SyntheticPlanRequest and renames Session::plan_synthetic_task to Session::plan_exec. Co-Authored-By: Claude Opus 4.6 --- crates/vite_task/src/session/cache/mod.rs | 8 +-- crates/vite_task/src/session/mod.rs | 2 +- crates/vite_task/src/session/reporter.rs | 2 +- crates/vite_task_bin/src/lib.rs | 10 ---- crates/vite_task_plan/src/cache_metadata.rs | 41 ++++++-------- crates/vite_task_plan/src/plan.rs | 54 ++++++++++--------- crates/vite_task_plan/src/plan_request.rs | 4 -- ... env-test synthetic task in user task.snap | 14 +++-- ...query - echo and lint with extra args.snap | 18 +++---- ...query - lint and echo with extra args.snap | 14 +++-- .../query - normal task with extra args.snap | 18 +++---- ... synthetic task in user task with cwd.snap | 14 +++-- .../query - synthetic task in user task.snap | 14 +++-- ...tic task with extra args in user task.snap | 18 +++---- ...ery - shell fallback for pipe command.snap | 14 +++-- .../query - synthetic-in-subpackage.snap | 18 +++---- .../tests/plan_snapshots/main.rs | 2 +- 17 files changed, 113 insertions(+), 152 deletions(-) diff --git a/crates/vite_task/src/session/cache/mod.rs b/crates/vite_task/src/session/cache/mod.rs index d43a01db..18f4ae19 100644 --- a/crates/vite_task/src/session/cache/mod.rs +++ b/crates/vite_task/src/session/cache/mod.rs @@ -92,16 +92,16 @@ impl ExecutionCache { "CREATE TABLE execution_key_to_fingerprint (key BLOB PRIMARY KEY, value BLOB);", (), )?; - conn.execute("PRAGMA user_version = 4", ())?; + conn.execute("PRAGMA user_version = 5", ())?; } - 1..=3 => { + 1..=4 => { // old internal db version. reset conn.set_db_config(DbConfig::SQLITE_DBCONFIG_RESET_DATABASE, true)?; conn.execute("VACUUM", ())?; conn.set_db_config(DbConfig::SQLITE_DBCONFIG_RESET_DATABASE, false)?; } - 4 => break, // current version - 5.. => { + 5 => break, // current version + 6.. => { return Err(anyhow::anyhow!("Unrecognized database version: {}", user_version)); } } diff --git a/crates/vite_task/src/session/mod.rs b/crates/vite_task/src/session/mod.rs index 7eabb4e3..f29af726 100644 --- a/crates/vite_task/src/session/mod.rs +++ b/crates/vite_task/src/session/mod.rs @@ -216,7 +216,7 @@ impl<'a> Session<'a> { } } - pub async fn plan_synthetic_task( + pub async fn plan_exec( &mut self, synthetic_plan_request: SyntheticPlanRequest, ) -> Result { diff --git a/crates/vite_task/src/session/reporter.rs b/crates/vite_task/src/session/reporter.rs index 1c76ca8f..bc74a8bc 100644 --- a/crates/vite_task/src/session/reporter.rs +++ b/crates/vite_task/src/session/reporter.rs @@ -156,7 +156,7 @@ impl LabeledReporter { CacheStatus::Disabled(_) => self.stats.cache_disabled += 1, } - // Handle None display case - direct synthetic execution (e.g., via plan_synthetic_task) + // Handle None display case - direct synthetic execution (e.g., via plan_exec) // Don't print cache status here - will be printed at finish for cache hits only let Some(display) = display else { self.executions.push(ExecutionInfo { diff --git a/crates/vite_task_bin/src/lib.rs b/crates/vite_task_bin/src/lib.rs index 7221207a..f1503eac 100644 --- a/crates/vite_task_bin/src/lib.rs +++ b/crates/vite_task_bin/src/lib.rs @@ -39,19 +39,15 @@ fn find_executable( } fn synthesize_node_modules_bin_task( - subcommand_name: &str, executable_name: &str, args: &[Str], envs: &Arc, Arc>>, cwd: &Arc, ) -> anyhow::Result { - let direct_execution_cache_key: Arc<[Str]> = - iter::once(Str::from(subcommand_name)).chain(args.iter().cloned()).collect(); Ok(SyntheticPlanRequest { program: find_executable(get_path_env(envs), &*cwd, executable_name)?, args: args.into(), task_options: Default::default(), - direct_execution_cache_key, envs: Arc::clone(envs), }) } @@ -78,14 +74,12 @@ impl vite_task::CommandHandler for CommandHandler { let rest = &command.args[1..]; match subcommand.as_str() { "lint" => Ok(HandledCommand::Synthesized(synthesize_node_modules_bin_task( - "lint", "oxlint", rest, &command.envs, &command.cwd, )?)), "test" => Ok(HandledCommand::Synthesized(synthesize_node_modules_bin_task( - "test", "vitest", rest, &command.envs, @@ -101,9 +95,6 @@ impl vite_task::CommandHandler for CommandHandler { .ok_or_else(|| anyhow::anyhow!("env-test requires a value argument"))? .clone(); - let direct_execution_cache_key: Arc<[Str]> = - [Str::from("env-test"), name.clone(), value.clone()].into(); - let mut envs = HashMap::clone(&command.envs); envs.insert( Arc::from(OsStr::new(name.as_str())), @@ -123,7 +114,6 @@ impl vite_task::CommandHandler for CommandHandler { }, ..Default::default() }, - direct_execution_cache_key, envs: Arc::new(envs), })) } diff --git a/crates/vite_task_plan/src/cache_metadata.rs b/crates/vite_task_plan/src/cache_metadata.rs index cd51915f..9dc97f13 100644 --- a/crates/vite_task_plan/src/cache_metadata.rs +++ b/crates/vite_task_plan/src/cache_metadata.rs @@ -7,18 +7,9 @@ use vite_str::Str; use crate::envs::EnvFingerprints; -/// The kind of a key to identify an execution. -#[derive(Debug, Encode, bincode::Decode, Serialize)] -pub(crate) enum ExecutionCacheKeyKind { - /// This execution is from a synthetic task generated by the task synthesizer - /// (e.g., `"lint": "vite lint"` expanding to an `oxlint` invocation). - /// - /// Note that this variant is used when the synthetic task is not wrapped in a user task, - /// unlike the `UserTask` variant which covers `"lint-task": "vite lint"` with its own cache key. - DirectSyntactic { - /// Provided in `SyntheticPlanRequest.direct_execution_cache_key` by task synthezier - direct_execution_cache_key: Arc<[Str]>, - }, +/// Key to identify an execution across sessions. +#[derive(Debug, Encode, Decode, Serialize)] +pub enum ExecutionCacheKey { /// This execution is from a script of a user-defined task. UserTask { /// The name of the user-defined task. @@ -29,21 +20,21 @@ pub(crate) enum ExecutionCacheKeyKind { /// Extra args provided when invoking the user-defined task (`vite [task_name] [extra_args...]`). /// These args are appended to the last and_item. Non-last and_items don't get extra args. extra_args: Arc<[Str]>, + /// The package path where the user-defined task is defined, relative to the workspace root. + package_path: RelativePathBuf, }, -} - -/// Key to identify an execution -#[derive(Debug, Encode, Decode, Serialize)] -pub struct ExecutionCacheKey { - /// The kind of the execution cache key (DirectSyntactic or UserTask) - pub(crate) kind: ExecutionCacheKeyKind, - /// The origin path where this execution is planned from. - /// It's relative to the workspace root. + /// This execution is from a synthetic task directly invoked from the command line + /// (e.g., `vite lint --fix` expanding to an `oxlint --fix` invocation). /// - /// - For DirectSyntactic, it's the cwd where the command `vite [custom subcommand] ...` is run. - /// It's not necessarily the actual cwd that the synthesized task runs in. - /// - For UserTask, it's the package path where the user-defined task is defined. - pub(crate) origin_path: RelativePathBuf, + /// The cache key is derived from the actual execution content. + Exec { + /// The program name (basename of the resolved program path). + program_name: Str, + /// The arguments to pass to the program. + args: Arc<[Str]>, + /// The working directory, relative to the workspace root. + cwd: RelativePathBuf, + }, } /// Cache information for a spawn execution. diff --git a/crates/vite_task_plan/src/plan.rs b/crates/vite_task_plan/src/plan.rs index 96ffef6c..3dc43d9e 100644 --- a/crates/vite_task_plan/src/plan.rs +++ b/crates/vite_task_plan/src/plan.rs @@ -16,10 +16,7 @@ use vite_task_graph::{TaskNodeIndex, config::ResolvedTaskOptions}; use crate::{ ExecutionItem, ExecutionItemDisplay, ExecutionItemKind, LeafExecutionKind, PlanContext, SpawnCommand, SpawnExecution, TaskExecution, - cache_metadata::{ - CacheMetadata, ExecutionCacheKey, ExecutionCacheKeyKind, ProgramFingerprint, - SpawnFingerprint, - }, + cache_metadata::{CacheMetadata, ExecutionCacheKey, ProgramFingerprint, SpawnFingerprint}, envs::EnvFingerprints, error::{ CdCommandError, Error, PathFingerprintError, PathFingerprintErrorKind, PathType, @@ -144,13 +141,11 @@ async fn plan_task_as_execution_node( } // Create execution cache key for this and_item - let task_execution_cache_key = ExecutionCacheKey { - kind: ExecutionCacheKeyKind::UserTask { - task_name: task_node.task_display.task_name.clone(), - and_item_index: index, - extra_args: Arc::clone(&extra_args), - }, - origin_path: strip_prefix_for_cache(package_path, context.workspace_path()) + 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| { TaskPlanErrorKind::PathFingerprintError(PathFingerprintError { kind, @@ -259,13 +254,11 @@ async fn plan_task_as_execution_node( let spawn_execution = plan_spawn_execution( context.workspace_path(), - ExecutionCacheKey { - kind: ExecutionCacheKeyKind::UserTask { - task_name: task_node.task_display.task_name.clone(), - and_item_index: 0, - extra_args: Arc::clone(context.extra_args()), - }, - origin_path: strip_prefix_for_cache(package_path, context.workspace_path()) + 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| { TaskPlanErrorKind::PathFingerprintError(PathFingerprintError { kind, @@ -294,12 +287,11 @@ pub fn plan_synthetic_request( workspace_path: &Arc, prefix_envs: &BTreeMap, synthetic_plan_request: SyntheticPlanRequest, - // generated from the task, overrides `synthetic_plan_request.direct_execution_cache_key` + // generated from the task, overrides the derived Exec cache key task_execution_cache_key: Option, cwd: &Arc, ) -> Result { - let SyntheticPlanRequest { program, args, task_options, direct_execution_cache_key, envs } = - synthetic_plan_request; + let SyntheticPlanRequest { program, args, task_options, envs } = synthetic_plan_request; let program_path = which(&program, &envs, cwd).map_err(TaskPlanErrorKind::ProgramNotFound)?; let resolved_options = ResolvedTaskOptions::resolve(task_options, &cwd); @@ -308,11 +300,21 @@ pub fn plan_synthetic_request( // Use task generated cache key if any task_execution_cache_key } else { - // Otherwise, use direct execution cache key - ExecutionCacheKey { - kind: ExecutionCacheKeyKind::DirectSyntactic { direct_execution_cache_key }, - origin_path: strip_prefix_for_cache(cwd, workspace_path) - .map_err(|kind| PathFingerprintError { kind, path_type: PathType::Cwd })?, + // Derive Exec cache key from the execution content + let program_name_os = program_path.as_path().file_stem().unwrap_or_default(); + let program_name = program_name_os.to_str().ok_or_else(|| PathFingerprintError { + kind: PathFingerprintErrorKind::NonPortableRelativePath { + path: Path::new(program_name_os).into(), + error: InvalidPathDataError::NonUtf8, + }, + path_type: PathType::Program, + })?; + let exec_cwd = strip_prefix_for_cache(&resolved_options.cwd, workspace_path) + .map_err(|kind| PathFingerprintError { kind, path_type: PathType::Cwd })?; + ExecutionCacheKey::Exec { + program_name: Str::from(program_name), + args: Arc::clone(&args), + cwd: exec_cwd, } }; diff --git a/crates/vite_task_plan/src/plan_request.rs b/crates/vite_task_plan/src/plan_request.rs index ac1f86a4..d0c406a0 100644 --- a/crates/vite_task_plan/src/plan_request.rs +++ b/crates/vite_task_plan/src/plan_request.rs @@ -46,10 +46,6 @@ pub struct SyntheticPlanRequest { /// The task options as if it's defined in `vite.config.*` pub task_options: UserTaskOptions, - /// The cache key for execution directly issued from user command line. - /// It typically includes the subcommand name and all args after it. (e.g. `["lint", "--fix"]` for `vite lint --fix`) - pub direct_execution_cache_key: Arc<[Str]>, - /// All environment variables to set for the synthetic task. /// /// This is set in the plan stage before resolving envs for caching. diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/additional-envs/snapshots/query - env-test synthetic task in user task.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/additional-envs/snapshots/query - env-test synthetic task in user task.snap index ee219d60..2087308b 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/additional-envs/snapshots/query - env-test synthetic task in user task.snap +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/additional-envs/snapshots/query - env-test synthetic task in user task.snap @@ -53,14 +53,12 @@ input_file: crates/vite_task_plan/tests/plan_snapshots/fixtures/additional-envs "fingerprint_ignores": null }, "execution_cache_key": { - "kind": { - "UserTask": { - "task_name": "env-test", - "and_item_index": 0, - "extra_args": [] - } - }, - "origin_path": "" + "UserTask": { + "task_name": "env-test", + "and_item_index": 0, + "extra_args": [], + "package_path": "" + } } }, "spawn_command": { diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - echo and lint with extra args.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - echo and lint with extra args.snap index 79957604..10f6851e 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - echo and lint with extra args.snap +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - echo and lint with extra args.snap @@ -78,16 +78,14 @@ input_file: crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys "fingerprint_ignores": null }, "execution_cache_key": { - "kind": { - "UserTask": { - "task_name": "echo-and-lint", - "and_item_index": 1, - "extra_args": [ - "--fix" - ] - } - }, - "origin_path": "" + "UserTask": { + "task_name": "echo-and-lint", + "and_item_index": 1, + "extra_args": [ + "--fix" + ], + "package_path": "" + } } }, "spawn_command": { diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - lint and echo with extra args.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - lint and echo with extra args.snap index 8d463b85..e5116b63 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - lint and echo with extra args.snap +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - lint and echo with extra args.snap @@ -50,14 +50,12 @@ input_file: crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys "fingerprint_ignores": null }, "execution_cache_key": { - "kind": { - "UserTask": { - "task_name": "lint-and-echo", - "and_item_index": 0, - "extra_args": [] - } - }, - "origin_path": "" + "UserTask": { + "task_name": "lint-and-echo", + "and_item_index": 0, + "extra_args": [], + "package_path": "" + } } }, "spawn_command": { diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - normal task with extra args.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - normal task with extra args.snap index ab892bb3..440abfa4 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - normal task with extra args.snap +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - normal task with extra args.snap @@ -52,16 +52,14 @@ input_file: crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys "fingerprint_ignores": null }, "execution_cache_key": { - "kind": { - "UserTask": { - "task_name": "hello", - "and_item_index": 0, - "extra_args": [ - "a.txt" - ] - } - }, - "origin_path": "" + "UserTask": { + "task_name": "hello", + "and_item_index": 0, + "extra_args": [ + "a.txt" + ], + "package_path": "" + } } }, "spawn_command": { diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - synthetic task in user task with cwd.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - synthetic task in user task with cwd.snap index cd423306..a71b87e3 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - synthetic task in user task with cwd.snap +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - synthetic task in user task with cwd.snap @@ -50,14 +50,12 @@ input_file: crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys "fingerprint_ignores": null }, "execution_cache_key": { - "kind": { - "UserTask": { - "task_name": "lint", - "and_item_index": 0, - "extra_args": [] - } - }, - "origin_path": "" + "UserTask": { + "task_name": "lint", + "and_item_index": 0, + "extra_args": [], + "package_path": "" + } } }, "spawn_command": { diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - synthetic task in user task.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - synthetic task in user task.snap index cd423306..a71b87e3 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - synthetic task in user task.snap +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - synthetic task in user task.snap @@ -50,14 +50,12 @@ input_file: crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys "fingerprint_ignores": null }, "execution_cache_key": { - "kind": { - "UserTask": { - "task_name": "lint", - "and_item_index": 0, - "extra_args": [] - } - }, - "origin_path": "" + "UserTask": { + "task_name": "lint", + "and_item_index": 0, + "extra_args": [], + "package_path": "" + } } }, "spawn_command": { diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - synthetic task with extra args in user task.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - synthetic task with extra args in user task.snap index 1da71fa3..91b9a980 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - synthetic task with extra args in user task.snap +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys/snapshots/query - synthetic task with extra args in user task.snap @@ -52,16 +52,14 @@ input_file: crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-keys "fingerprint_ignores": null }, "execution_cache_key": { - "kind": { - "UserTask": { - "task_name": "lint", - "and_item_index": 0, - "extra_args": [ - "--fix" - ] - } - }, - "origin_path": "" + "UserTask": { + "task_name": "lint", + "and_item_index": 0, + "extra_args": [ + "--fix" + ], + "package_path": "" + } } }, "spawn_command": { diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/shell-fallback/snapshots/query - shell fallback for pipe command.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/shell-fallback/snapshots/query - shell fallback for pipe command.snap index 159fb7d5..0e230877 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/shell-fallback/snapshots/query - shell fallback for pipe command.snap +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/shell-fallback/snapshots/query - shell fallback for pipe command.snap @@ -53,14 +53,12 @@ input_file: crates/vite_task_plan/tests/plan_snapshots/fixtures/shell-fallback "fingerprint_ignores": null }, "execution_cache_key": { - "kind": { - "UserTask": { - "task_name": "pipe-test", - "and_item_index": 0, - "extra_args": [] - } - }, - "origin_path": "" + "UserTask": { + "task_name": "pipe-test", + "and_item_index": 0, + "extra_args": [], + "package_path": "" + } } }, "spawn_command": { diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic-in-subpackage/snapshots/query - synthetic-in-subpackage.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic-in-subpackage/snapshots/query - synthetic-in-subpackage.snap index c207584d..83b85689 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic-in-subpackage/snapshots/query - synthetic-in-subpackage.snap +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic-in-subpackage/snapshots/query - synthetic-in-subpackage.snap @@ -1,7 +1,7 @@ --- -source: crates/vite_task_bin/tests/test_snapshots/main.rs +source: crates/vite_task_plan/tests/plan_snapshots/main.rs expression: "&plan_json" -input_file: crates/vite_task_bin/tests/test_snapshots/fixtures/synthetic-in-subpackage +input_file: crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic-in-subpackage --- { "root_node": { @@ -75,14 +75,12 @@ input_file: crates/vite_task_bin/tests/test_snapshots/fixtures/synthetic-in-subp "fingerprint_ignores": null }, "execution_cache_key": { - "kind": { - "UserTask": { - "task_name": "lint", - "and_item_index": 0, - "extra_args": [] - } - }, - "origin_path": "packages/a" + "UserTask": { + "task_name": "lint", + "and_item_index": 0, + "extra_args": [], + "package_path": "packages/a" + } } }, "spawn_command": { diff --git a/crates/vite_task_plan/tests/plan_snapshots/main.rs b/crates/vite_task_plan/tests/plan_snapshots/main.rs index 99b4d641..c6714efd 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/main.rs +++ b/crates/vite_task_plan/tests/plan_snapshots/main.rs @@ -127,7 +127,7 @@ fn run_case_inner( let snapshot_name = format!("query - {}", plan.name); let cli = match Cli::try_parse_from( - std::iter::once("vite-task") // dummy program name + std::iter::once("vp") // dummy program name .chain(plan.args.iter().map(|s| s.as_str())), ) { Ok(ok) => ok, From b53bf341d1d710984aad1d563508922694db2e94 Mon Sep 17 00:00:00 2001 From: branchseer Date: Fri, 6 Feb 2026 21:40:18 +0800 Subject: [PATCH 06/10] refactor: replace Exec cache key with opaque ExecAPI variant Replace `ExecutionCacheKey::Exec { program_name, args, cwd }` with `ExecAPI(Arc<[Str]>)`, an opaque caller-provided cache key. Add `cache_key` parameter to `Session::plan_exec` and new `ExecutionPlan::plan_exec` entry point. Bump cache DB version to 6. Co-Authored-By: Claude Opus 4.6 --- crates/vite_task/src/session/cache/mod.rs | 6 ++-- crates/vite_task/src/session/mod.rs | 16 +++------ crates/vite_task_plan/src/cache_metadata.rs | 14 ++------ crates/vite_task_plan/src/lib.rs | 20 ++++++++++- crates/vite_task_plan/src/plan.rs | 38 +++++---------------- 5 files changed, 38 insertions(+), 56 deletions(-) diff --git a/crates/vite_task/src/session/cache/mod.rs b/crates/vite_task/src/session/cache/mod.rs index 18f4ae19..a44e8d53 100644 --- a/crates/vite_task/src/session/cache/mod.rs +++ b/crates/vite_task/src/session/cache/mod.rs @@ -92,15 +92,15 @@ impl ExecutionCache { "CREATE TABLE execution_key_to_fingerprint (key BLOB PRIMARY KEY, value BLOB);", (), )?; - conn.execute("PRAGMA user_version = 5", ())?; + conn.execute("PRAGMA user_version = 6", ())?; } - 1..=4 => { + 1..=5 => { // old internal db version. reset conn.set_db_config(DbConfig::SQLITE_DBCONFIG_RESET_DATABASE, true)?; conn.execute("VACUUM", ())?; conn.set_db_config(DbConfig::SQLITE_DBCONFIG_RESET_DATABASE, false)?; } - 5 => break, // current version + 6 => break, // current version 6.. => { return Err(anyhow::anyhow!("Unrecognized database version: {}", user_version)); } diff --git a/crates/vite_task/src/session/mod.rs b/crates/vite_task/src/session/mod.rs index f29af726..9ec3d114 100644 --- a/crates/vite_task/src/session/mod.rs +++ b/crates/vite_task/src/session/mod.rs @@ -216,20 +216,12 @@ impl<'a> Session<'a> { } } - pub async fn plan_exec( - &mut self, + pub fn plan_exec( + &self, synthetic_plan_request: SyntheticPlanRequest, + cache_key: Arc<[Str]>, ) -> Result { - let plan = ExecutionPlan::plan( - PlanRequest::Synthetic(synthetic_plan_request), - &self.workspace_path, - &self.cwd, - &self.envs, - &mut self.plan_request_parser, - &mut self.lazy_task_graph, - ) - .await?; - Ok(plan) + ExecutionPlan::plan_exec(&self.workspace_path, &self.cwd, synthetic_plan_request, cache_key) } pub async fn plan_from_cli( diff --git a/crates/vite_task_plan/src/cache_metadata.rs b/crates/vite_task_plan/src/cache_metadata.rs index 9dc97f13..46690f21 100644 --- a/crates/vite_task_plan/src/cache_metadata.rs +++ b/crates/vite_task_plan/src/cache_metadata.rs @@ -23,18 +23,10 @@ pub enum ExecutionCacheKey { /// The package path where the user-defined task is defined, relative to the workspace root. package_path: RelativePathBuf, }, - /// This execution is from a synthetic task directly invoked from the command line - /// (e.g., `vite lint --fix` expanding to an `oxlint --fix` invocation). + /// This execution is from a synthetic task directly invoked from `Session::plan_exec` API. /// - /// The cache key is derived from the actual execution content. - Exec { - /// The program name (basename of the resolved program path). - program_name: Str, - /// The arguments to pass to the program. - args: Arc<[Str]>, - /// The working directory, relative to the workspace root. - cwd: RelativePathBuf, - }, + /// The cache key is an opaque value provided by the caller. + ExecAPI(Arc<[Str]>), } /// Cache information for a spawn execution. diff --git a/crates/vite_task_plan/src/lib.rs b/crates/vite_task_plan/src/lib.rs index 90a1804e..698972a3 100644 --- a/crates/vite_task_plan/src/lib.rs +++ b/crates/vite_task_plan/src/lib.rs @@ -22,7 +22,7 @@ use execution_graph::ExecutionGraph; use in_process::InProcessExecution; pub use path_env::{get_path_env, prepend_path_env}; use plan::{plan_query_request, plan_synthetic_request}; -use plan_request::PlanRequest; +use plan_request::{PlanRequest, SyntheticPlanRequest}; use serde::{Serialize, ser::SerializeMap as _}; use vite_graph_ser::serialize_by_key; use vite_path::AbsolutePath; @@ -225,4 +225,22 @@ impl ExecutionPlan { }; Ok(Self { root_node }) } + + pub fn plan_exec( + workspace_path: &Arc, + cwd: &Arc, + synthetic_plan_request: SyntheticPlanRequest, + cache_key: Arc<[Str]>, + ) -> Result { + let execution_cache_key = cache_metadata::ExecutionCacheKey::ExecAPI(cache_key); + let execution = plan_synthetic_request( + workspace_path, + &Default::default(), + synthetic_plan_request, + Some(execution_cache_key), + cwd, + ) + .with_empty_call_stack()?; + Ok(Self { root_node: ExecutionItemKind::Leaf(LeafExecutionKind::Spawn(execution)) }) + } } diff --git a/crates/vite_task_plan/src/plan.rs b/crates/vite_task_plan/src/plan.rs index 3dc43d9e..bf5a2547 100644 --- a/crates/vite_task_plan/src/plan.rs +++ b/crates/vite_task_plan/src/plan.rs @@ -206,7 +206,7 @@ async fn plan_task_as_execution_node( .with_plan_context(&context)?; let spawn_execution = plan_spawn_execution( context.workspace_path(), - task_execution_cache_key, + Some(task_execution_cache_key), &and_item.envs, &task_node.resolved_config.resolved_options, &script_command.envs, @@ -254,7 +254,7 @@ async fn plan_task_as_execution_node( let spawn_execution = plan_spawn_execution( context.workspace_path(), - ExecutionCacheKey::UserTask { + Some(ExecutionCacheKey::UserTask { task_name: task_node.task_display.task_name.clone(), and_item_index: 0, extra_args: Arc::clone(context.extra_args()), @@ -266,7 +266,7 @@ async fn plan_task_as_execution_node( }) }) .with_plan_context(&context)?, - }, + }), &BTreeMap::new(), &task_node.resolved_config.resolved_options, context.envs(), @@ -287,8 +287,7 @@ pub fn plan_synthetic_request( workspace_path: &Arc, prefix_envs: &BTreeMap, synthetic_plan_request: SyntheticPlanRequest, - // generated from the task, overrides the derived Exec cache key - task_execution_cache_key: Option, + execution_cache_key: Option, cwd: &Arc, ) -> Result { let SyntheticPlanRequest { program, args, task_options, envs } = synthetic_plan_request; @@ -296,28 +295,6 @@ pub fn plan_synthetic_request( let program_path = which(&program, &envs, cwd).map_err(TaskPlanErrorKind::ProgramNotFound)?; let resolved_options = ResolvedTaskOptions::resolve(task_options, &cwd); - let execution_cache_key = if let Some(task_execution_cache_key) = task_execution_cache_key { - // Use task generated cache key if any - task_execution_cache_key - } else { - // Derive Exec cache key from the execution content - let program_name_os = program_path.as_path().file_stem().unwrap_or_default(); - let program_name = program_name_os.to_str().ok_or_else(|| PathFingerprintError { - kind: PathFingerprintErrorKind::NonPortableRelativePath { - path: Path::new(program_name_os).into(), - error: InvalidPathDataError::NonUtf8, - }, - path_type: PathType::Program, - })?; - let exec_cwd = strip_prefix_for_cache(&resolved_options.cwd, workspace_path) - .map_err(|kind| PathFingerprintError { kind, path_type: PathType::Cwd })?; - ExecutionCacheKey::Exec { - program_name: Str::from(program_name), - args: Arc::clone(&args), - cwd: exec_cwd, - } - }; - plan_spawn_execution( workspace_path, execution_cache_key, @@ -348,7 +325,7 @@ fn strip_prefix_for_cache( fn plan_spawn_execution( workspace_path: &Arc, - execution_cache_key: ExecutionCacheKey, + execution_cache_key: Option, prefix_envs: &BTreeMap, resolved_task_options: &ResolvedTaskOptions, envs: &HashMap, Arc>, @@ -402,7 +379,10 @@ fn plan_spawn_execution( env_fingerprints, fingerprint_ignores: None, }; - resolved_cache_metadata = Some(CacheMetadata { execution_cache_key, spawn_fingerprint }); + if let Some(execution_cache_key) = execution_cache_key { + resolved_cache_metadata = + Some(CacheMetadata { execution_cache_key, spawn_fingerprint }); + } } // Add prefix envs to all envs From 97eb97c2d3d42cf657ee1bf8be0640047bf9ffae Mon Sep 17 00:00:00 2001 From: branchseer Date: Sat, 7 Feb 2026 00:44:50 +0800 Subject: [PATCH 07/10] refactor: move CLI subcommand parsing into clap-derived Args enum Replace HandledCommand::NotSynthesized { is_vite_task_entry } with explicit Verbatim and ViteTaskCommand(Command) variants. All vite subcommand parsing (lint, test, env-test, run) now happens via a clap-derived Args enum in vite_task_bin, simplifying PlanRequestParser to a trivial dispatcher. Co-Authored-By: Claude Opus 4.6 --- crates/vite_task/src/session/mod.rs | 47 +++++---------------- crates/vite_task_bin/src/lib.rs | 63 +++++++++++++++-------------- 2 files changed, 43 insertions(+), 67 deletions(-) diff --git a/crates/vite_task/src/session/mod.rs b/crates/vite_task/src/session/mod.rs index 9ec3d114..f551e94d 100644 --- a/crates/vite_task/src/session/mod.rs +++ b/crates/vite_task/src/session/mod.rs @@ -6,10 +6,8 @@ pub mod reporter; // Re-export types that are part of the public API use std::{ffi::OsStr, fmt::Debug, sync::Arc}; -use anyhow::Context; use cache::ExecutionCache; pub use cache::{CacheMiss, FingerprintMismatch}; -use clap::{Parser, Subcommand as _}; pub use event::ExecutionEvent; use once_cell::sync::OnceCell; pub use reporter::{LabeledReporter, Reporter}; @@ -60,31 +58,21 @@ pub struct SessionCallbacks<'a> { pub enum HandledCommand { /// The command was synthesized into a task (e.g., `vite lint` → `oxlint`). Synthesized(SyntheticPlanRequest), - /// The command was not synthesized. - NotSynthesized { - /// Whether this program is the task runner's own entry point. - /// If `true`, `get_plan_request` will check if the first arg is a known - /// subcommand (via [`Command::has_subcommand`]) and parse it as a CLI command. - is_vite_task_entry: bool, - }, + /// The command is a vite-task CLI command (e.g., `vite run build`). + ViteTaskCommand(Command), + /// The command should be executed verbatim as an external process. + Verbatim, } /// Handles commands found in task scripts to determine how they should be executed. /// -/// Since `vite_task` doesn't know the name of its own binary, it relies on the caller -/// to identify which commands are vite-task commands. Return [`HandledCommand::NotSynthesized`] -/// with `is_vite_task_entry: true` to let vite-task check if the args match a CLI subcommand, -/// or [`HandledCommand::Synthesized`] to provide a synthetic task directly. +/// The implementation should return: +/// - [`HandledCommand::Synthesized`] to replace the command with a synthetic task. +/// - [`HandledCommand::ViteTaskCommand`] when the command is a vite-task CLI invocation. +/// - [`HandledCommand::Verbatim`] to execute the command as-is as an external process. #[async_trait::async_trait(?Send)] pub trait CommandHandler: Debug { /// Called for every command in task scripts to determine how it should be handled. - /// - /// The implementation can either: - /// - Return `Synthesized(...)` to replace the command with a synthetic task. - /// - Return `NotSynthesized { is_vite_task_entry }` and optionally mutate `command` - /// to modify how the command is executed as a normal process. - /// - /// If `Synthesized` is returned, any mutations to `command` are discarded. async fn handle_command( &mut self, command: &mut ScriptCommand, @@ -104,25 +92,10 @@ impl vite_task_plan::PlanRequestParser for PlanRequestParser<'_> { ) -> anyhow::Result> { match self.command_handler.handle_command(command).await? { HandledCommand::Synthesized(synthetic) => Ok(Some(PlanRequest::Synthetic(synthetic))), - HandledCommand::NotSynthesized { is_vite_task_entry: true } - if command - .args - .first() - .is_some_and(|arg| Command::has_subcommand(arg.as_str())) => - { - let cli_command = Command::try_parse_from( - std::iter::once(command.program.as_str()) - .chain(command.args.iter().map(Str::as_str)), - ) - .with_context(|| { - vite_str::format!( - "Failed to parse vite-task command from args: {:?}", - command.args - ) - })?; + HandledCommand::ViteTaskCommand(cli_command) => { Ok(Some(cli_command.into_plan_request(&command.cwd)?)) } - HandledCommand::NotSynthesized { .. } => Ok(None), + HandledCommand::Verbatim => Ok(None), } } } diff --git a/crates/vite_task_bin/src/lib.rs b/crates/vite_task_bin/src/lib.rs index f1503eac..45177e0e 100644 --- a/crates/vite_task_bin/src/lib.rs +++ b/crates/vite_task_bin/src/lib.rs @@ -7,10 +7,11 @@ use std::{ sync::Arc, }; +use clap::Parser as _; use vite_path::AbsolutePath; use vite_str::Str; use vite_task::{ - EnabledCacheConfig, HandledCommand, ScriptCommand, SessionCallbacks, UserCacheConfig, + Command, EnabledCacheConfig, HandledCommand, ScriptCommand, SessionCallbacks, UserCacheConfig, UserTaskOptions, get_path_env, plan_request::SyntheticPlanRequest, }; @@ -52,6 +53,24 @@ fn synthesize_node_modules_bin_task( }) } +#[derive(clap::Parser)] +enum Args { + Lint { + #[clap(trailing_var_arg = true, allow_hyphen_values = true)] + args: Vec, + }, + Test { + #[clap(trailing_var_arg = true, allow_hyphen_values = true)] + args: Vec, + }, + EnvTest { + name: Str, + value: Str, + }, + #[command(flatten)] + Task(Command), +} + #[async_trait::async_trait(?Send)] impl vite_task::CommandHandler for CommandHandler { async fn handle_command( @@ -66,35 +85,19 @@ impl vite_task::CommandHandler for CommandHandler { command.args = iter::once(Str::from("run")).chain(command.args.iter().cloned()).collect(); } - _ => return Ok(HandledCommand::NotSynthesized { is_vite_task_entry: false }), + _ => return Ok(HandledCommand::Verbatim), } - let Some(subcommand) = command.args.first().cloned() else { - return Ok(HandledCommand::NotSynthesized { is_vite_task_entry: true }); - }; - let rest = &command.args[1..]; - match subcommand.as_str() { - "lint" => Ok(HandledCommand::Synthesized(synthesize_node_modules_bin_task( - "oxlint", - rest, - &command.envs, - &command.cwd, - )?)), - "test" => Ok(HandledCommand::Synthesized(synthesize_node_modules_bin_task( - "vitest", - rest, - &command.envs, - &command.cwd, - )?)), - "env-test" => { - let name = rest - .first() - .ok_or_else(|| anyhow::anyhow!("env-test requires a name argument"))? - .clone(); - let value = rest - .get(1) - .ok_or_else(|| anyhow::anyhow!("env-test requires a value argument"))? - .clone(); - + let args = Args::try_parse_from( + std::iter::once(command.program.as_str()).chain(command.args.iter().map(Str::as_str)), + )?; + match args { + Args::Lint { args } => Ok(HandledCommand::Synthesized( + synthesize_node_modules_bin_task("oxlint", &args, &command.envs, &command.cwd)?, + )), + Args::Test { args } => Ok(HandledCommand::Synthesized( + synthesize_node_modules_bin_task("vitest", &args, &command.envs, &command.cwd)?, + )), + Args::EnvTest { name, value } => { let mut envs = HashMap::clone(&command.envs); envs.insert( Arc::from(OsStr::new(name.as_str())), @@ -117,7 +120,7 @@ impl vite_task::CommandHandler for CommandHandler { envs: Arc::new(envs), })) } - _ => Ok(HandledCommand::NotSynthesized { is_vite_task_entry: true }), + Args::Task(cli_command) => Ok(HandledCommand::ViteTaskCommand(cli_command)), } } } From ed7b9d0590b06910c01710d4321323cedab47e5d Mon Sep 17 00:00:00 2001 From: branchseer Date: Sat, 7 Feb 2026 01:52:34 +0800 Subject: [PATCH 08/10] feat: add CacheSubcommand and simplify Session API Extract RunCommand struct from Command::Run, add CacheSubcommand::Clean to Command, and introduce Session::main() as the primary CLI entry point. This makes reporter types and execute() internal, simplifying the public API. When `vite cache clean` appears in a script, PlanRequestParser generates a synthesized task with caching disabled. Co-Authored-By: Claude Opus 4.6 --- Cargo.lock | 1 + crates/vite_task/Cargo.toml | 1 + crates/vite_task/src/cli/mod.rs | 124 ++++++++++-------- crates/vite_task/src/lib.rs | 6 +- crates/vite_task/src/session/execute/mod.rs | 2 +- crates/vite_task/src/session/mod.rs | 62 +++++++-- crates/vite_task_bin/src/main.rs | 20 +-- .../fixtures/cache-subcommand/package.json | 3 + .../fixtures/cache-subcommand/snapshots.toml | 8 ++ .../snapshots/cache clean.snap | 60 +++++++++ .../fixtures/cache-subcommand/test.txt | 1 + .../fixtures/cache-subcommand/vite-task.json | 9 ++ .../cache-subcommand/node_modules/.bin/vite | 2 + .../node_modules/.bin/vite.cmd | 2 + .../fixtures/cache-subcommand/package.json | 6 + .../fixtures/cache-subcommand/snapshots.toml | 3 + .../query - cache clean in script.snap | 58 ++++++++ .../snapshots/task graph.snap | 35 +++++ .../fixtures/cache-subcommand/vite-task.json | 3 + .../tests/plan_snapshots/main.rs | 6 +- 20 files changed, 326 insertions(+), 86 deletions(-) create mode 100644 crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-subcommand/package.json create mode 100644 crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-subcommand/snapshots.toml create mode 100644 crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-subcommand/snapshots/cache clean.snap create mode 100644 crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-subcommand/test.txt create mode 100644 crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-subcommand/vite-task.json create mode 100755 crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-subcommand/node_modules/.bin/vite create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-subcommand/node_modules/.bin/vite.cmd create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-subcommand/package.json create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-subcommand/snapshots.toml create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-subcommand/snapshots/query - cache clean in script.snap create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-subcommand/snapshots/task graph.snap create mode 100644 crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-subcommand/vite-task.json diff --git a/Cargo.lock b/Cargo.lock index 0585ead9..3babd57a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3298,6 +3298,7 @@ dependencies = [ "diff-struct", "fspy", "futures-util", + "monostate", "nix 0.30.1", "once_cell", "owo-colors", diff --git a/crates/vite_task/Cargo.toml b/crates/vite_task/Cargo.toml index e319b82a..f1529009 100644 --- a/crates/vite_task/Cargo.toml +++ b/crates/vite_task/Cargo.toml @@ -21,6 +21,7 @@ derive_more = { workspace = true, features = ["from"] } diff-struct = { workspace = true } fspy = { workspace = true } futures-util = { workspace = true } +monostate = { workspace = true } once_cell = { workspace = true } owo-colors = { workspace = true } petgraph = { workspace = true } diff --git a/crates/vite_task/src/cli/mod.rs b/crates/vite_task/src/cli/mod.rs index d1ec7257..5abdbc95 100644 --- a/crates/vite_task/src/cli/mod.rs +++ b/crates/vite_task/src/cli/mod.rs @@ -6,29 +6,44 @@ use vite_str::Str; use vite_task_graph::{TaskSpecifier, query::TaskQueryKind}; use vite_task_plan::plan_request::{PlanOptions, PlanRequest, QueryPlanRequest}; -/// vite task CLI subcommands -#[derive(Debug, Parser)] -pub enum Command { - /// Run tasks - Run { - /// `packageName#taskName` or `taskName`. - task_specifier: TaskSpecifier, +#[derive(Debug, Clone, clap::Subcommand)] +pub enum CacheSubcommand { + /// Clean up all the cache + Clean, +} + +/// Arguments for the `run` subcommand. +#[derive(Debug, clap::Args)] +pub struct RunCommand { + /// `packageName#taskName` or `taskName`. + pub task_specifier: TaskSpecifier, + + /// Run tasks found in all packages in the workspace, in topological order based on package dependencies. + #[clap(default_value = "false", short, long)] + pub recursive: bool, - /// Run tasks found in all packages in the workspace, in topological order based on package dependencies. - #[clap(default_value = "false", short, long)] - recursive: bool, + /// Run tasks found in the current package and all its transitive dependencies, in topological order based on package dependencies. + #[clap(default_value = "false", short, long)] + pub transitive: bool, - /// Run tasks found in the current package and all its transitive dependencies, in topological order based on package dependencies. - #[clap(default_value = "false", short, long)] - transitive: bool, + /// Do not run dependencies specified in `dependsOn` fields. + #[clap(default_value = "false", long)] + pub ignore_depends_on: bool, - /// Do not run dependencies specified in `dependsOn` fields. - #[clap(default_value = "false", long)] - ignore_depends_on: bool, + /// Additional arguments to pass to the tasks + #[clap(trailing_var_arg = true, allow_hyphen_values = true)] + pub additional_args: Vec, +} - /// Additional arguments to pass to the tasks - #[clap(trailing_var_arg = true, allow_hyphen_values = true)] - additional_args: Vec, +/// vite task CLI subcommands +#[derive(Debug, Parser)] +pub enum Command { + /// Run tasks + Run(RunCommand), + /// Manage the task cache + Cache { + #[clap(subcommand)] + subcmd: CacheSubcommand, }, } @@ -41,50 +56,45 @@ pub enum CLITaskQueryError { PackageNameSpecifiedWithRecursive { package_name: Str, task_name: Str }, } -impl Command { +impl RunCommand { /// Convert to `PlanRequest`, or return an error if invalid. pub fn into_plan_request( self, cwd: &Arc, ) -> Result { - match self { - Self::Run { - task_specifier, - recursive, - transitive, - ignore_depends_on, - additional_args, - } => { - let include_explicit_deps = !ignore_depends_on; + let RunCommand { + task_specifier, + recursive, + transitive, + ignore_depends_on, + additional_args, + } = self; + + let include_explicit_deps = !ignore_depends_on; - let query_kind = if recursive { - if transitive { - return Err(CLITaskQueryError::RecursiveTransitiveConflict); - } - let task_name = if let Some(package_name) = task_specifier.package_name { - return Err(CLITaskQueryError::PackageNameSpecifiedWithRecursive { - package_name, - task_name: task_specifier.task_name, - }); - } else { - task_specifier.task_name - }; - TaskQueryKind::Recursive { task_names: [task_name].into() } - } else { - TaskQueryKind::Normal { - task_specifiers: [task_specifier].into(), - cwd: Arc::clone(cwd), - include_topological_deps: transitive, - } - }; - Ok(PlanRequest::Query(QueryPlanRequest { - query: vite_task_graph::query::TaskQuery { - kind: query_kind, - include_explicit_deps, - }, - plan_options: PlanOptions { extra_args: additional_args.into() }, - })) + let query_kind = if recursive { + if transitive { + return Err(CLITaskQueryError::RecursiveTransitiveConflict); + } + let task_name = if let Some(package_name) = task_specifier.package_name { + return Err(CLITaskQueryError::PackageNameSpecifiedWithRecursive { + package_name, + task_name: task_specifier.task_name, + }); + } else { + task_specifier.task_name + }; + TaskQueryKind::Recursive { task_names: [task_name].into() } + } else { + TaskQueryKind::Normal { + task_specifiers: [task_specifier].into(), + cwd: Arc::clone(cwd), + include_topological_deps: transitive, } - } + }; + Ok(PlanRequest::Query(QueryPlanRequest { + query: vite_task_graph::query::TaskQuery { kind: query_kind, include_explicit_deps }, + plan_options: PlanOptions { extra_args: additional_args.into() }, + })) } } diff --git a/crates/vite_task/src/lib.rs b/crates/vite_task/src/lib.rs index 61311f25..0d86d9d2 100644 --- a/crates/vite_task/src/lib.rs +++ b/crates/vite_task/src/lib.rs @@ -4,10 +4,8 @@ mod maybe_str; pub mod session; // Public exports for vite_task_bin -pub use cli::Command; -pub use session::{ - CommandHandler, HandledCommand, LabeledReporter, Reporter, Session, SessionCallbacks, -}; +pub use cli::{CacheSubcommand, Command, RunCommand}; +pub use session::{CommandHandler, ExitStatus, HandledCommand, Session, SessionCallbacks}; pub use vite_task_graph::{ config::{ self, diff --git a/crates/vite_task/src/session/execute/mod.rs b/crates/vite_task/src/session/execute/mod.rs index 19fdcc61..494ec5b7 100644 --- a/crates/vite_task/src/session/execute/mod.rs +++ b/crates/vite_task/src/session/execute/mod.rs @@ -355,7 +355,7 @@ impl<'a> Session<'a> { /// /// The return type isn't just ExitStatus because we want to distinguish between normal successful execution, /// and execution that failed and needs to exit with a specific code which can be zero. - pub async fn execute( + pub(crate) async fn execute( &self, plan: ExecutionPlan, mut reporter: Box, diff --git a/crates/vite_task/src/session/mod.rs b/crates/vite_task/src/session/mod.rs index f551e94d..36833ca1 100644 --- a/crates/vite_task/src/session/mod.rs +++ b/crates/vite_task/src/session/mod.rs @@ -1,7 +1,7 @@ mod cache; mod event; mod execute; -pub mod reporter; +pub(crate) mod reporter; // Re-export types that are part of the public API use std::{ffi::OsStr, fmt::Debug, sync::Arc}; @@ -9,11 +9,17 @@ use std::{ffi::OsStr, fmt::Debug, sync::Arc}; use cache::ExecutionCache; pub use cache::{CacheMiss, FingerprintMismatch}; pub use event::ExecutionEvent; +use monostate::MustBe; use once_cell::sync::OnceCell; -pub use reporter::{LabeledReporter, Reporter}; +pub use reporter::ExitStatus; +use reporter::LabeledReporter; use vite_path::{AbsolutePath, AbsolutePathBuf}; use vite_str::Str; -use vite_task_graph::{IndexedTaskGraph, TaskGraph, TaskGraphLoadError, loader::UserConfigLoader}; +use vite_task_graph::{ + IndexedTaskGraph, TaskGraph, TaskGraphLoadError, + config::user::{UserCacheConfig, UserTaskOptions}, + loader::UserConfigLoader, +}; use vite_task_plan::{ ExecutionPlan, TaskGraphLoader, TaskPlanErrorKind, plan_request::{PlanRequest, ScriptCommand, SyntheticPlanRequest}, @@ -21,7 +27,10 @@ use vite_task_plan::{ }; use vite_workspace::{WorkspaceRoot, find_workspace_root}; -use crate::{cli::Command, collections::HashMap}; +use crate::{ + cli::{CacheSubcommand, Command, RunCommand}, + collections::HashMap, +}; #[derive(Debug)] enum LazyTaskGraph<'a> { @@ -92,9 +101,18 @@ impl vite_task_plan::PlanRequestParser for PlanRequestParser<'_> { ) -> anyhow::Result> { match self.command_handler.handle_command(command).await? { HandledCommand::Synthesized(synthetic) => Ok(Some(PlanRequest::Synthetic(synthetic))), - HandledCommand::ViteTaskCommand(cli_command) => { - Ok(Some(cli_command.into_plan_request(&command.cwd)?)) - } + HandledCommand::ViteTaskCommand(cli_command) => match cli_command { + Command::Cache { .. } => Ok(Some(PlanRequest::Synthetic(SyntheticPlanRequest { + program: Arc::from(OsStr::new(command.program.as_str())), + args: Arc::clone(&command.args), + task_options: UserTaskOptions { + cache_config: UserCacheConfig::Disabled { cache: MustBe!(false) }, + ..Default::default() + }, + envs: Arc::clone(&command.envs), + }))), + Command::Run(run_command) => Ok(Some(run_command.into_plan_request(&command.cwd)?)), + }, HandledCommand::Verbatim => Ok(None), } } @@ -171,6 +189,34 @@ impl<'a> Session<'a> { }) } + /// Primary entry point for CLI usage. Plans and executes the given command. + pub async fn main(mut self, command: Command) -> anyhow::Result { + match command { + Command::Cache { subcmd } => self.handle_cache_command(subcmd), + Command::Run(run_command) => { + let cwd = Arc::clone(&self.cwd); + let plan = self.plan_from_cli(cwd, run_command).await?; + let reporter = LabeledReporter::new(std::io::stdout(), self.workspace_path()); + Ok(self + .execute(plan, Box::new(reporter)) + .await + .err() + .unwrap_or(ExitStatus::SUCCESS)) + } + } + } + + fn handle_cache_command(&self, subcmd: CacheSubcommand) -> anyhow::Result { + match subcmd { + CacheSubcommand::Clean => { + if self.cache_path.as_path().exists() { + std::fs::remove_dir_all(&self.cache_path)?; + } + Ok(ExitStatus::SUCCESS) + } + } + } + /// Lazily initializes and returns the execution cache. /// The cache is only created when first accessed to avoid SQLite race conditions /// when multiple processes start simultaneously. @@ -200,7 +246,7 @@ impl<'a> Session<'a> { pub async fn plan_from_cli( &mut self, cwd: Arc, - command: Command, + command: RunCommand, ) -> Result { let plan_request = command.into_plan_request(&cwd).map_err(|error| { TaskPlanErrorKind::ParsePlanRequestError { diff --git a/crates/vite_task_bin/src/main.rs b/crates/vite_task_bin/src/main.rs index f896ca93..b711ab07 100644 --- a/crates/vite_task_bin/src/main.rs +++ b/crates/vite_task_bin/src/main.rs @@ -1,11 +1,7 @@ -use std::{process::ExitCode, sync::Arc}; +use std::process::ExitCode; use clap::Parser; -use vite_path::{AbsolutePath, current_dir}; -use vite_task::{ - Command, Session, - session::reporter::{ExitStatus, LabeledReporter}, -}; +use vite_task::{Command, Session}; use vite_task_bin::OwnedSessionCallbacks; #[derive(Parser)] @@ -21,15 +17,9 @@ async fn main() -> anyhow::Result { Ok(exit_status.0.into()) } -async fn run() -> anyhow::Result { - let cwd: Arc = current_dir()?.into(); +async fn run() -> anyhow::Result { let cli = Cli::parse(); - let mut owned_callbacks = OwnedSessionCallbacks::default(); - let mut session = Session::init(owned_callbacks.as_callbacks())?; - let plan = session.plan_from_cli(cwd, cli.command).await?; - - // Create reporter and execute - let reporter = LabeledReporter::new(std::io::stdout(), session.workspace_path()); - Ok(session.execute(plan, Box::new(reporter)).await.err().unwrap_or(ExitStatus::SUCCESS)) + let session = Session::init(owned_callbacks.as_callbacks())?; + session.main(cli.command).await } diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-subcommand/package.json b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-subcommand/package.json new file mode 100644 index 00000000..538e7f4d --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-subcommand/package.json @@ -0,0 +1,3 @@ +{ + "name": "@test/cache-subcommand" +} diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-subcommand/snapshots.toml b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-subcommand/snapshots.toml new file mode 100644 index 00000000..e647b042 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-subcommand/snapshots.toml @@ -0,0 +1,8 @@ +[[e2e]] +name = "cache clean" +steps = [ + "vite run cached-task # cache miss", + "vite run cached-task # cache hit", + "vite cache clean", + "vite run cached-task # cache miss after clean", +] diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-subcommand/snapshots/cache clean.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-subcommand/snapshots/cache clean.snap new file mode 100644 index 00000000..9288581b --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-subcommand/snapshots/cache clean.snap @@ -0,0 +1,60 @@ +--- +source: crates/vite_task_bin/tests/e2e_snapshots/main.rs +expression: e2e_outputs +input_file: crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-subcommand +--- +> vite run cached-task # cache miss +$ print-file test.txt +test content + + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Vite+ Task Runner • Execution Summary +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Statistics: 1 tasks • 0 cache hits • 1 cache misses +Performance: 0% cache hit rate + +Task Details: +──────────────────────────────────────────────── + [1] @test/cache-subcommand#cached-task: $ print-file test.txt ✓ + → Cache miss: no previous cache entry found +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +> vite run cached-task # cache hit +$ print-file test.txt ✓ cache hit, replaying +test content + + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Vite+ Task Runner • Execution Summary +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Statistics: 1 tasks • 1 cache hits • 0 cache misses +Performance: 100% cache hit rate, saved in total + +Task Details: +──────────────────────────────────────────────── + [1] @test/cache-subcommand#cached-task: $ print-file test.txt ✓ + → Cache hit - output replayed - saved +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +> vite cache clean + +> vite run cached-task # cache miss after clean +$ print-file test.txt +test content + + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Vite+ Task Runner • Execution Summary +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Statistics: 1 tasks • 0 cache hits • 1 cache misses +Performance: 0% cache hit rate + +Task Details: +──────────────────────────────────────────────── + [1] @test/cache-subcommand#cached-task: $ print-file test.txt ✓ + → Cache miss: no previous cache entry found +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-subcommand/test.txt b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-subcommand/test.txt new file mode 100644 index 00000000..d670460b --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-subcommand/test.txt @@ -0,0 +1 @@ +test content diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-subcommand/vite-task.json b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-subcommand/vite-task.json new file mode 100644 index 00000000..d1bedfe4 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-subcommand/vite-task.json @@ -0,0 +1,9 @@ +{ + "cacheScripts": true, + "tasks": { + "cached-task": { + "command": "print-file test.txt", + "cache": true + } + } +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-subcommand/node_modules/.bin/vite b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-subcommand/node_modules/.bin/vite new file mode 100755 index 00000000..c635c2dc --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-subcommand/node_modules/.bin/vite @@ -0,0 +1,2 @@ +# Dummy executable so `which("vite")` succeeds during plan resolution. +# This file is never actually executed. diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-subcommand/node_modules/.bin/vite.cmd b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-subcommand/node_modules/.bin/vite.cmd new file mode 100644 index 00000000..0efc832a --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-subcommand/node_modules/.bin/vite.cmd @@ -0,0 +1,2 @@ +@REM Dummy executable so `which("vite")` succeeds during plan resolution on Windows. +@REM This file is never actually executed. diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-subcommand/package.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-subcommand/package.json new file mode 100644 index 00000000..225cb648 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-subcommand/package.json @@ -0,0 +1,6 @@ +{ + "name": "@test/cache-subcommand", + "scripts": { + "clean-cache": "vite cache clean" + } +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-subcommand/snapshots.toml b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-subcommand/snapshots.toml new file mode 100644 index 00000000..150dd755 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-subcommand/snapshots.toml @@ -0,0 +1,3 @@ +[[plan]] +name = "cache clean in script" +args = ["run", "clean-cache"] diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-subcommand/snapshots/query - cache clean in script.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-subcommand/snapshots/query - cache clean in script.snap new file mode 100644 index 00000000..184aaf14 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-subcommand/snapshots/query - cache clean in script.snap @@ -0,0 +1,58 @@ +--- +source: crates/vite_task_plan/tests/plan_snapshots/main.rs +expression: "&plan_json" +input_file: crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-subcommand +--- +{ + "root_node": { + "Expanded": [ + { + "key": [ + "/", + "clean-cache" + ], + "node": { + "task_display": { + "package_name": "@test/cache-subcommand", + "task_name": "clean-cache", + "package_path": "/" + }, + "items": [ + { + "execution_item_display": { + "task_display": { + "package_name": "@test/cache-subcommand", + "task_name": "clean-cache", + "package_path": "/" + }, + "command": "vite cache clean", + "and_item_index": null, + "cwd": "/" + }, + "kind": { + "Leaf": { + "Spawn": { + "cache_metadata": null, + "spawn_command": { + "program_path": "/node_modules/.bin/vite", + "args": [ + "cache", + "clean" + ], + "all_envs": { + "NO_COLOR": "1", + "PATH": "/node_modules/.bin:/node_modules/.bin" + }, + "cwd": "/" + } + } + } + } + } + ] + }, + "neighbors": [] + } + ] + } +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-subcommand/snapshots/task graph.snap b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-subcommand/snapshots/task graph.snap new file mode 100644 index 00000000..422476a8 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-subcommand/snapshots/task graph.snap @@ -0,0 +1,35 @@ +--- +source: crates/vite_task_plan/tests/plan_snapshots/main.rs +expression: task_graph_json +input_file: crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-subcommand +--- +[ + { + "key": [ + "/", + "clean-cache" + ], + "node": { + "task_display": { + "package_name": "@test/cache-subcommand", + "task_name": "clean-cache", + "package_path": "/" + }, + "resolved_config": { + "command": "vite cache clean", + "resolved_options": { + "cwd": "/", + "cache_config": { + "env_config": { + "fingerprinted_envs": [], + "pass_through_envs": [ + "" + ] + } + } + } + } + }, + "neighbors": [] + } +] diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-subcommand/vite-task.json b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-subcommand/vite-task.json new file mode 100644 index 00000000..1d0fe9f2 --- /dev/null +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache-subcommand/vite-task.json @@ -0,0 +1,3 @@ +{ + "cacheScripts": true +} diff --git a/crates/vite_task_plan/tests/plan_snapshots/main.rs b/crates/vite_task_plan/tests/plan_snapshots/main.rs index c6714efd..77765162 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/main.rs +++ b/crates/vite_task_plan/tests/plan_snapshots/main.rs @@ -137,9 +137,13 @@ fn run_case_inner( } }; let Cli::Command(command) = cli; + let run_command = match command { + Command::Run(run_command) => run_command, + _ => panic!("only `run` commands supported in plan tests"), + }; let plan_result = - session.plan_from_cli(workspace_root.path.join(plan.cwd).into(), command).await; + session.plan_from_cli(workspace_root.path.join(plan.cwd).into(), run_command).await; let plan = match plan_result { Ok(plan) => plan, From 593c070046d40dca6874fa4733299818cbb638d7 Mon Sep 17 00:00:00 2001 From: branchseer Date: Sat, 7 Feb 2026 02:44:18 +0800 Subject: [PATCH 09/10] feat: add Session::exec API and expose Args from vite_task_bin Add Session::exec for executing synthetic commands with cache support independently of Session::main. In vite-plus, this pattern is used for auto-install. Also expose Args enum and find_executable from vite_task_bin, remove the Cli wrapper in main.rs, and add e2e tests validating exec caching behavior. Co-Authored-By: Claude Opus 4.6 --- crates/vite_task/src/session/mod.rs | 27 ++++++++- crates/vite_task_bin/src/lib.rs | 9 +-- crates/vite_task_bin/src/main.rs | 55 ++++++++++++++----- .../fixtures/exec-api/package.json | 6 ++ .../fixtures/exec-api/snapshots.toml | 13 +++++ .../exec-api/snapshots/exec caching.snap | 17 ++++++ .../exec not triggered from script.snap | 23 ++++++++ .../fixtures/exec-api/vite-task.json | 3 + 8 files changed, 135 insertions(+), 18 deletions(-) create mode 100644 crates/vite_task_bin/tests/e2e_snapshots/fixtures/exec-api/package.json create mode 100644 crates/vite_task_bin/tests/e2e_snapshots/fixtures/exec-api/snapshots.toml create mode 100644 crates/vite_task_bin/tests/e2e_snapshots/fixtures/exec-api/snapshots/exec caching.snap create mode 100644 crates/vite_task_bin/tests/e2e_snapshots/fixtures/exec-api/snapshots/exec not triggered from script.snap create mode 100644 crates/vite_task_bin/tests/e2e_snapshots/fixtures/exec-api/vite-task.json diff --git a/crates/vite_task/src/session/mod.rs b/crates/vite_task/src/session/mod.rs index 36833ca1..b1df3a79 100644 --- a/crates/vite_task/src/session/mod.rs +++ b/crates/vite_task/src/session/mod.rs @@ -235,7 +235,32 @@ impl<'a> Session<'a> { } } - pub fn plan_exec( + pub fn envs(&self) -> &Arc, Arc>> { + &self.envs + } + + pub fn cwd(&self) -> &Arc { + &self.cwd + } + + /// Execute a synthetic command with cache support. + /// + /// This is for executing a command with cache before/without the entrypoint [`Session::main`]. + /// In vite-plus, this is used for auto-install. + pub async fn exec( + &self, + synthetic_plan_request: SyntheticPlanRequest, + cache_key: Arc<[Str]>, + silent_if_cache_hit: bool, + ) -> anyhow::Result { + let plan = self.plan_exec(synthetic_plan_request, cache_key)?; + let mut reporter = LabeledReporter::new(std::io::stdout(), self.workspace_path()); + reporter.set_hide_summary(true); + reporter.set_silent_if_cache_hit(silent_if_cache_hit); + Ok(self.execute(plan, Box::new(reporter)).await.err().unwrap_or(ExitStatus::SUCCESS)) + } + + fn plan_exec( &self, synthetic_plan_request: SyntheticPlanRequest, cache_key: Arc<[Str]>, diff --git a/crates/vite_task_bin/src/lib.rs b/crates/vite_task_bin/src/lib.rs index 45177e0e..d665358a 100644 --- a/crates/vite_task_bin/src/lib.rs +++ b/crates/vite_task_bin/src/lib.rs @@ -7,7 +7,7 @@ use std::{ sync::Arc, }; -use clap::Parser as _; +use clap::Parser; use vite_path::AbsolutePath; use vite_str::Str; use vite_task::{ @@ -18,7 +18,7 @@ use vite_task::{ #[derive(Debug, Default)] pub struct CommandHandler(()); -fn find_executable( +pub fn find_executable( path_env: Option<&Arc>, cwd: &AbsolutePath, executable: &str, @@ -53,8 +53,9 @@ fn synthesize_node_modules_bin_task( }) } -#[derive(clap::Parser)] -enum Args { +#[derive(Debug, Parser)] +#[command(name = "vite", version)] +pub enum Args { Lint { #[clap(trailing_var_arg = true, allow_hyphen_values = true)] args: Vec, diff --git a/crates/vite_task_bin/src/main.rs b/crates/vite_task_bin/src/main.rs index b711ab07..bfe1f623 100644 --- a/crates/vite_task_bin/src/main.rs +++ b/crates/vite_task_bin/src/main.rs @@ -1,15 +1,12 @@ -use std::process::ExitCode; +use std::{process::ExitCode, sync::Arc}; use clap::Parser; -use vite_task::{Command, Session}; -use vite_task_bin::OwnedSessionCallbacks; - -#[derive(Parser)] -#[command(name = "vite", version)] -struct Cli { - #[command(subcommand)] - command: Command, -} +use vite_str::Str; +use vite_task::{ + EnabledCacheConfig, ExitStatus, Session, UserCacheConfig, UserTaskOptions, get_path_env, + plan_request::SyntheticPlanRequest, +}; +use vite_task_bin::{Args, OwnedSessionCallbacks, find_executable}; #[tokio::main] async fn main() -> anyhow::Result { @@ -17,9 +14,41 @@ async fn main() -> anyhow::Result { Ok(exit_status.0.into()) } -async fn run() -> anyhow::Result { - let cli = Cli::parse(); +async fn run() -> anyhow::Result { + let args = Args::parse(); let mut owned_callbacks = OwnedSessionCallbacks::default(); let session = Session::init(owned_callbacks.as_callbacks())?; - session.main(cli.command).await + match args { + Args::Task(command) => session.main(command).await, + args => { + // If env FOO is set, run `print-env FOO` via Session::exec before proceeding. + // In vite-plus, Session::exec is used for auto-install. + let envs = session.envs(); + if envs.contains_key(std::ffi::OsStr::new("FOO")) { + let program = find_executable(get_path_env(envs), session.cwd(), "print-env")?; + let request = SyntheticPlanRequest { + program, + args: [Str::from("FOO")].into(), + task_options: UserTaskOptions { + cache_config: UserCacheConfig::Enabled { + cache: None, + enabled_cache_config: EnabledCacheConfig { + envs: Some(Box::from([Str::from("FOO")])), + pass_through_envs: None, + }, + }, + ..Default::default() + }, + envs: Arc::clone(envs), + }; + let cache_key: Arc<[Str]> = Arc::from([Str::from("print-env-foo")]); + let status = session.exec(request, cache_key, true).await?; + if status != ExitStatus::SUCCESS { + return Ok(status); + } + } + println!("{:?}", args); + Ok(ExitStatus::SUCCESS) + } + } } diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/exec-api/package.json b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/exec-api/package.json new file mode 100644 index 00000000..bd72b0f4 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/exec-api/package.json @@ -0,0 +1,6 @@ +{ + "name": "@test/exec-api", + "scripts": { + "lint-task": "vite lint" + } +} diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/exec-api/snapshots.toml b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/exec-api/snapshots.toml new file mode 100644 index 00000000..884832c1 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/exec-api/snapshots.toml @@ -0,0 +1,13 @@ +[[e2e]] +name = "exec not triggered from script" +steps = [ + "FOO=bar vite run lint-task # no print-env FOO output", +] + +[[e2e]] +name = "exec caching" +steps = [ + "FOO=bar vite lint # cache miss", + "FOO=bar vite lint # cache hit, silent", + "FOO=baz vite lint # env changed, cache miss", +] diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/exec-api/snapshots/exec caching.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/exec-api/snapshots/exec caching.snap new file mode 100644 index 00000000..98f77ba3 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/exec-api/snapshots/exec caching.snap @@ -0,0 +1,17 @@ +--- +source: crates/vite_task_bin/tests/e2e_snapshots/main.rs +expression: e2e_outputs +input_file: crates/vite_task_bin/tests/e2e_snapshots/fixtures/exec-api +--- +> FOO=bar vite lint # cache miss +bar + +Lint { args: [] } + +> FOO=bar vite lint # cache hit, silent +Lint { args: [] } + +> FOO=baz vite lint # env changed, cache miss +baz + +Lint { args: [] } diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/exec-api/snapshots/exec not triggered from script.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/exec-api/snapshots/exec not triggered from script.snap new file mode 100644 index 00000000..3a822a4b --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/exec-api/snapshots/exec not triggered from script.snap @@ -0,0 +1,23 @@ +--- +source: crates/vite_task_bin/tests/e2e_snapshots/main.rs +expression: e2e_outputs +input_file: crates/vite_task_bin/tests/e2e_snapshots/fixtures/exec-api +--- +> FOO=bar vite run lint-task # no print-env FOO output +$ vite lint +Found 0 warnings and 0 errors. +Finished in on 0 files with 90 rules using threads. + + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Vite+ Task Runner • Execution Summary +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Statistics: 1 tasks • 0 cache hits • 1 cache misses +Performance: 0% cache hit rate + +Task Details: +──────────────────────────────────────────────── + [1] @test/exec-api#lint-task: $ vite lint ✓ + → Cache miss: no previous cache entry found +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/exec-api/vite-task.json b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/exec-api/vite-task.json new file mode 100644 index 00000000..1d0fe9f2 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/exec-api/vite-task.json @@ -0,0 +1,3 @@ +{ + "cacheScripts": true +} From 6cbd9ce53aa1eab4f0b74029dda8c7a69b48695f Mon Sep 17 00:00:00 2001 From: branchseer Date: Sun, 8 Feb 2026 18:10:15 +0800 Subject: [PATCH 10/10] refactor: rename exec to execute_synthetic and update plan_exec to plan_synthetic --- crates/vite_task/src/session/mod.rs | 17 +++++++---------- crates/vite_task_bin/src/main.rs | 2 +- crates/vite_task_plan/src/lib.rs | 2 +- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/crates/vite_task/src/session/mod.rs b/crates/vite_task/src/session/mod.rs index b1df3a79..36a8203c 100644 --- a/crates/vite_task/src/session/mod.rs +++ b/crates/vite_task/src/session/mod.rs @@ -247,27 +247,24 @@ impl<'a> Session<'a> { /// /// This is for executing a command with cache before/without the entrypoint [`Session::main`]. /// In vite-plus, this is used for auto-install. - pub async fn exec( + pub async fn execute_synthetic( &self, synthetic_plan_request: SyntheticPlanRequest, cache_key: Arc<[Str]>, silent_if_cache_hit: bool, ) -> anyhow::Result { - let plan = self.plan_exec(synthetic_plan_request, cache_key)?; + let plan = ExecutionPlan::plan_synthetic( + &self.workspace_path, + &self.cwd, + synthetic_plan_request, + cache_key, + )?; let mut reporter = LabeledReporter::new(std::io::stdout(), self.workspace_path()); reporter.set_hide_summary(true); reporter.set_silent_if_cache_hit(silent_if_cache_hit); Ok(self.execute(plan, Box::new(reporter)).await.err().unwrap_or(ExitStatus::SUCCESS)) } - fn plan_exec( - &self, - synthetic_plan_request: SyntheticPlanRequest, - cache_key: Arc<[Str]>, - ) -> Result { - ExecutionPlan::plan_exec(&self.workspace_path, &self.cwd, synthetic_plan_request, cache_key) - } - pub async fn plan_from_cli( &mut self, cwd: Arc, diff --git a/crates/vite_task_bin/src/main.rs b/crates/vite_task_bin/src/main.rs index bfe1f623..8fba7320 100644 --- a/crates/vite_task_bin/src/main.rs +++ b/crates/vite_task_bin/src/main.rs @@ -42,7 +42,7 @@ async fn run() -> anyhow::Result { envs: Arc::clone(envs), }; let cache_key: Arc<[Str]> = Arc::from([Str::from("print-env-foo")]); - let status = session.exec(request, cache_key, true).await?; + let status = session.execute_synthetic(request, cache_key, true).await?; if status != ExitStatus::SUCCESS { return Ok(status); } diff --git a/crates/vite_task_plan/src/lib.rs b/crates/vite_task_plan/src/lib.rs index 698972a3..d434ae14 100644 --- a/crates/vite_task_plan/src/lib.rs +++ b/crates/vite_task_plan/src/lib.rs @@ -226,7 +226,7 @@ impl ExecutionPlan { Ok(Self { root_node }) } - pub fn plan_exec( + pub fn plan_synthetic( workspace_path: &Arc, cwd: &Arc, synthetic_plan_request: SyntheticPlanRequest,