Skip to content
Merged
34 changes: 30 additions & 4 deletions crates/vite_task/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use clap::Parser;
use vite_path::AbsolutePath;
use vite_str::Str;
use vite_task_graph::{TaskSpecifier, query::TaskQuery};
use vite_task_plan::plan_request::{PlanOptions, QueryPlanRequest};
use vite_task_plan::plan_request::{CacheOverride, PlanOptions, QueryPlanRequest};
use vite_workspace::package_filter::{PackageQueryArgs, PackageQueryError};

#[derive(Debug, Clone, clap::Subcommand)]
Expand All @@ -15,6 +15,7 @@ pub enum CacheSubcommand {

/// Flags that control how a `run` command selects tasks.
#[derive(Debug, Clone, PartialEq, Eq, clap::Args)]
#[expect(clippy::struct_excessive_bools, reason = "CLI flags are naturally boolean")]
pub struct RunFlags {
#[clap(flatten)]
pub package_query: PackageQueryArgs,
Expand All @@ -26,6 +27,27 @@ pub struct RunFlags {
/// Show full detailed summary after execution.
#[clap(default_value = "false", short = 'v', long)]
pub verbose: bool,

/// Force caching on for all tasks and scripts.
#[clap(long, conflicts_with = "no_cache")]
pub cache: bool,

/// Force caching off for all tasks and scripts.
#[clap(long, conflicts_with = "cache")]
pub no_cache: bool,
}

impl RunFlags {
#[must_use]
pub const fn cache_override(&self) -> CacheOverride {
if self.cache {
CacheOverride::ForceEnabled
} else if self.no_cache {
CacheOverride::ForceDisabled
} else {
CacheOverride::None
}
}
}

// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Expand Down Expand Up @@ -155,19 +177,23 @@ impl ResolvedRunCommand {
let raw_specifier = self.task_specifier.ok_or(CLITaskQueryError::MissingTaskSpecifier)?;
let task_specifier = TaskSpecifier::parse_raw(&raw_specifier);

let cache_override = self.flags.cache_override();
let include_explicit_deps = !self.flags.ignore_depends_on;

let (package_query, is_cwd_only) =
self.flags.package_query.into_package_query(task_specifier.package_name, cwd)?;

let include_explicit_deps = !self.flags.ignore_depends_on;

Ok((
QueryPlanRequest {
query: TaskQuery {
package_query,
task_name: task_specifier.task_name,
include_explicit_deps,
},
plan_options: PlanOptions { extra_args: self.additional_args.into() },
plan_options: PlanOptions {
extra_args: self.additional_args.into(),
cache_override,
},
},
is_cwd_only,
))
Expand Down
19 changes: 4 additions & 15 deletions crates/vite_task_graph/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,29 +100,18 @@ pub enum ResolveTaskConfigError {
}

impl ResolvedTaskConfig {
/// Resolve from package.json script only (no vite-task.json config for this task)
/// Resolve from package.json script only (no config entry for this task).
///
/// The `cache_scripts` parameter determines whether caching is enabled for the script.
/// When `true`, caching is enabled with default settings.
/// When `false`, caching is disabled.
/// Always resolves with caching enabled (default settings).
/// The global cache config is applied at plan time, not here.
#[must_use]
pub fn resolve_package_json_script(
package_dir: &Arc<AbsolutePath>,
package_json_script: &str,
cache_scripts: bool,
) -> Self {
let cache_config = if cache_scripts {
UserCacheConfig::Enabled {
cache: None,
enabled_cache_config: EnabledCacheConfig { envs: None, pass_through_envs: None },
}
} else {
UserCacheConfig::Disabled { cache: MustBe!(false) }
};
let options = UserTaskOptions { cache_config, ..Default::default() };
Self {
command: package_json_script.into(),
resolved_options: ResolvedTaskOptions::resolve(options, package_dir),
resolved_options: ResolvedTaskOptions::resolve(UserTaskOptions::default(), package_dir),
}
}

Expand Down
1 change: 1 addition & 0 deletions crates/vite_task_graph/src/config/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ pub enum UserGlobalCacheConfig {
}

/// Resolved global cache configuration with concrete boolean values.
#[derive(Debug, Clone, Copy)]
pub struct ResolvedGlobalCacheConfig {
pub scripts: bool,
pub tasks: bool,
Expand Down
44 changes: 30 additions & 14 deletions crates/vite_task_graph/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ mod specifier;

use std::{convert::Infallible, sync::Arc};

use config::{ResolvedTaskConfig, UserRunConfig};
use config::{ResolvedGlobalCacheConfig, ResolvedTaskConfig, UserRunConfig};
use petgraph::graph::{DefaultIx, DiGraph, EdgeIndex, IndexType, NodeIndex};
use rustc_hash::{FxBuildHasher, FxHashMap};
use serde::Serialize;
Expand Down Expand Up @@ -47,6 +47,15 @@ pub(crate) struct TaskId {
pub task_name: Str,
}

/// Whether a task originates from the `tasks` map or from a package.json script.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum TaskSource {
/// Defined in the `tasks` map in the workspace config.
TaskConfig,
/// Pure package.json script (not in the tasks map).
PackageJsonScript,
}

/// A node in the task graph, representing a task with its resolved configuration.
#[derive(Debug, Serialize)]
pub struct TaskNode {
Expand All @@ -60,6 +69,9 @@ pub struct TaskNode {
///
/// However, it does not contain external factors like additional args from cli and env vars.
pub resolved_config: ResolvedTaskConfig,

/// Whether this task comes from the tasks map or a package.json script.
pub source: TaskSource,
}

impl vite_graph_ser::GetKey for TaskNode {
Expand Down Expand Up @@ -166,6 +178,9 @@ pub struct IndexedTaskGraph {

/// task indices by task id for quick lookup
pub(crate) node_indices_by_task_id: FxHashMap<TaskId, TaskNodeIndex>,

/// Global cache configuration resolved from the workspace root config.
resolved_global_cache: ResolvedGlobalCacheConfig,
}

pub type TaskGraph = DiGraph<TaskNode, TaskDependencyType, TaskIx>;
Expand Down Expand Up @@ -234,11 +249,9 @@ impl IndexedTaskGraph {
package_configs.push((package_index, package_dir, user_config));
}

let resolved_cache = config::ResolvedGlobalCacheConfig::resolve_from(root_cache.as_ref());
let cache_scripts = resolved_cache.scripts;
let cache_tasks = resolved_cache.tasks;
let resolved_global_cache = ResolvedGlobalCacheConfig::resolve_from(root_cache.as_ref());

// Second pass: create task nodes using resolved cache config
// Second pass: create task nodes (cache is NOT applied here; it's applied at plan time)
for (package_index, package_dir, user_config) in package_configs {
let package = &package_graph[package_index];

Expand All @@ -250,19 +263,15 @@ impl IndexedTaskGraph {
.map(|(name, value)| (name.as_str(), value.as_str()))
.collect();

for (task_name, mut task_user_config) in user_config.tasks.unwrap_or_default() {
// Apply cache.tasks kill switch: when false, override all tasks to disable caching
if !cache_tasks {
task_user_config.options.cache_config = config::UserCacheConfig::disabled();
}
// For each task defined in vite.config.*, look up the corresponding package.json script (if any)
for (task_name, task_user_config) in user_config.tasks.unwrap_or_default() {
// For each task defined in the config, look up the corresponding package.json script (if any)
let package_json_script = package_json_scripts.remove(task_name.as_str());

let task_id = TaskId { task_name: task_name.clone(), package_index };

let dependency_specifiers = task_user_config.options.depends_on.clone();

// Resolve the task configuration combining vite.config.* and package.json script
// Resolve the task configuration combining config and package.json script
let resolved_config = ResolvedTaskConfig::resolve(
task_user_config,
&package_dir,
Expand All @@ -284,20 +293,20 @@ impl IndexedTaskGraph {
package_path: Arc::clone(&package_dir),
},
resolved_config,
source: TaskSource::TaskConfig,
};

let node_index = task_graph.add_node(task_node);
task_ids_with_dependency_specifiers.push((task_id.clone(), dependency_specifiers));
node_indices_by_task_id.insert(task_id, node_index);
}

// For remaining package.json scripts not defined in vite.config.*, create tasks with default config
// For remaining package.json scripts not in the tasks map, create tasks with default config
for (script_name, package_json_script) in package_json_scripts {
let task_id = TaskId { task_name: Str::from(script_name), package_index };
let resolved_config = ResolvedTaskConfig::resolve_package_json_script(
&package_dir,
package_json_script,
cache_scripts,
);
let node_index = task_graph.add_node(TaskNode {
task_display: TaskDisplay {
Expand All @@ -306,6 +315,7 @@ impl IndexedTaskGraph {
package_path: Arc::clone(&package_dir),
},
resolved_config,
source: TaskSource::PackageJsonScript,
});
node_indices_by_task_id.insert(task_id, node_index);
}
Expand All @@ -316,6 +326,7 @@ impl IndexedTaskGraph {
task_graph,
indexed_package_graph: IndexedPackageGraph::index(package_graph),
node_indices_by_task_id,
resolved_global_cache,
};

// Add explicit dependencies
Expand Down Expand Up @@ -420,4 +431,9 @@ impl IndexedTaskGraph {
let index = self.indexed_package_graph.get_package_index_from_cwd(cwd)?;
Some(self.get_package_path(index))
}

#[must_use]
pub const fn global_cache_config(&self) -> &ResolvedGlobalCacheConfig {
&self.resolved_global_cache
}
}
16 changes: 15 additions & 1 deletion crates/vite_task_plan/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::{env::JoinPathsError, ffi::OsStr, ops::Range, sync::Arc};
use rustc_hash::FxHashMap;
use vite_path::AbsolutePath;
use vite_str::Str;
use vite_task_graph::{IndexedTaskGraph, TaskNodeIndex};
use vite_task_graph::{IndexedTaskGraph, TaskNodeIndex, config::ResolvedGlobalCacheConfig};

use crate::{PlanRequestParser, path_env::prepend_path_env};

Expand Down Expand Up @@ -39,6 +39,9 @@ pub struct PlanContext<'a> {
extra_args: Arc<[Str]>,

indexed_task_graph: &'a IndexedTaskGraph,

/// Final resolved global cache config, combining the graph's config with any CLI override.
resolved_global_cache: ResolvedGlobalCacheConfig,
}

impl<'a> PlanContext<'a> {
Expand All @@ -48,6 +51,7 @@ impl<'a> PlanContext<'a> {
envs: FxHashMap<Arc<OsStr>, Arc<OsStr>>,
callbacks: &'a mut (dyn PlanRequestParser + 'a),
indexed_task_graph: &'a IndexedTaskGraph,
resolved_global_cache: ResolvedGlobalCacheConfig,
) -> Self {
Self {
workspace_path,
Expand All @@ -57,6 +61,7 @@ impl<'a> PlanContext<'a> {
task_call_stack: Vec::new(),
indexed_task_graph,
extra_args: Arc::default(),
resolved_global_cache,
}
}

Expand Down Expand Up @@ -115,6 +120,14 @@ impl<'a> PlanContext<'a> {
self.extra_args = extra_args;
}

pub const fn resolved_global_cache(&self) -> &ResolvedGlobalCacheConfig {
&self.resolved_global_cache
}

pub const fn set_resolved_global_cache(&mut self, config: ResolvedGlobalCacheConfig) {
self.resolved_global_cache = config;
}

pub fn duplicate(&mut self) -> PlanContext<'_> {
PlanContext {
workspace_path: self.workspace_path,
Expand All @@ -124,6 +137,7 @@ impl<'a> PlanContext<'a> {
task_call_stack: self.task_call_stack.clone(),
indexed_task_graph: self.indexed_task_graph,
extra_args: Arc::clone(&self.extra_args),
resolved_global_cache: self.resolved_global_cache,
}
}
}
23 changes: 22 additions & 1 deletion crates/vite_task_plan/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pub use execution_graph::ExecutionGraph;
pub use in_process::InProcessExecution;
pub use path_env::{get_path_env, prepend_path_env};
use plan::{ParentCacheConfig, plan_query_request, plan_synthetic_request};
use plan_request::{PlanRequest, QueryPlanRequest, SyntheticPlanRequest};
use plan_request::{CacheOverride, PlanRequest, QueryPlanRequest, SyntheticPlanRequest};
use rustc_hash::FxHashMap;
use serde::{Serialize, ser::SerializeMap as _};
use vite_path::AbsolutePath;
Expand Down Expand Up @@ -200,16 +200,37 @@ pub async fn plan_query(
) -> Result<ExecutionGraph, Error> {
let indexed_task_graph = task_graph_loader.load_task_graph().await?;

let resolved_global_cache = resolve_cache_with_override(
*indexed_task_graph.global_cache_config(),
query_plan_request.plan_options.cache_override,
);

let context = PlanContext::new(
workspace_path,
Arc::clone(cwd),
envs.clone(),
plan_request_parser,
indexed_task_graph,
resolved_global_cache,
);
plan_query_request(query_plan_request, context).await
}

const fn resolve_cache_with_override(
graph_cache: vite_task_graph::config::ResolvedGlobalCacheConfig,
cache_override: CacheOverride,
) -> vite_task_graph::config::ResolvedGlobalCacheConfig {
match cache_override {
CacheOverride::ForceEnabled => {
vite_task_graph::config::ResolvedGlobalCacheConfig { scripts: true, tasks: true }
}
CacheOverride::ForceDisabled => {
vite_task_graph::config::ResolvedGlobalCacheConfig { scripts: false, tasks: false }
}
CacheOverride::None => graph_cache,
}
}

/// Plan a synthetic task execution, returning the resolved [`SpawnExecution`] directly.
///
/// Unlike [`plan_query`] which returns a full execution graph, synthetic executions
Expand Down
Loading