Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 3 additions & 9 deletions crates/vite_task/src/session/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use vite_task_graph::{
loader::UserConfigLoader,
};
use vite_task_plan::{
ExecutionGraph, ExecutionPlan, TaskGraphLoader,
ExecutionGraph, TaskGraphLoader,
plan_request::{PlanRequest, ScriptCommand, SyntheticPlanRequest},
prepend_path_env,
};
Expand Down Expand Up @@ -457,18 +457,12 @@ impl<'a> Session<'a> {
) -> anyhow::Result<ExitStatus> {
// Plan the synthetic execution — returns a SpawnExecution directly
// (synthetic plans are always a single spawned process)
let execution_plan = ExecutionPlan::plan_synthetic(
let spawn_execution = vite_task_plan::plan_synthetic(
&self.workspace_path,
&self.cwd,
synthetic_plan_request,
cache_key,
)?;
let vite_task_plan::ExecutionItemKind::Leaf(vite_task_plan::LeafExecutionKind::Spawn(
spawn_execution,
)) = execution_plan.into_root_node()
else {
unreachable!("plan_synthetic always produces a Leaf(Spawn(..)) node")
};

// Initialize cache (needed for cache-aware execution)
let cache = self.cache()?;
Expand Down Expand Up @@ -531,7 +525,7 @@ impl<'a> Session<'a> {
});
}
};
ExecutionPlan::plan_query(
vite_task_plan::plan_query(
query_plan_request,
&self.workspace_path,
&cwd,
Expand Down
7 changes: 7 additions & 0 deletions crates/vite_task_plan/src/execution_graph.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::ops::Deref;

use petgraph::graph::{DefaultIx, DiGraph, EdgeIndex, IndexType, NodeIndex};
use serde::{Serialize, Serializer};

use crate::TaskExecution;

Expand Down Expand Up @@ -135,3 +136,9 @@ impl Deref for ExecutionGraph {
&self.graph
}
}

impl Serialize for ExecutionGraph {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
vite_graph_ser::serialize_by_key(&self.graph, serializer)
}
}
180 changes: 50 additions & 130 deletions crates/vite_task_plan/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ 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 rustc_hash::FxHashMap;
use serde::{Serialize, Serializer, ser::SerializeMap as _};
use serde::{Serialize, ser::SerializeMap as _};
use vite_path::AbsolutePath;
use vite_str::Str;
use vite_task_graph::{TaskGraphLoadError, display::TaskDisplay};
Expand Down Expand Up @@ -142,18 +142,11 @@ pub enum LeafExecutionKind {
///
/// `vite_graph_ser::serialize_by_key` expects `&DiGraph<N, E, Ix>`, so we call `.inner()`
/// to get the underlying `DiGraph` reference.
fn serialize_execution_graph_by_key<S: Serializer>(
graph: &ExecutionGraph,
serializer: S,
) -> Result<S::Ok, S::Error> {
vite_graph_ser::serialize_by_key(graph.inner(), serializer)
}

/// An execution item, from a split subcommand in a task's command (`item1 && item2 && ...`).
#[derive(Debug, Serialize)]
pub enum ExecutionItemKind {
/// Expanded from a known vp subcommand, like `vp run ...` or a synthesized task.
Expanded(#[serde(serialize_with = "serialize_execution_graph_by_key")] ExecutionGraph),
Expanded(ExecutionGraph),
/// A normal execution that spawns a child process, like `tsc --noEmit`.
Leaf(LeafExecutionKind),
}
Expand Down Expand Up @@ -186,127 +179,54 @@ pub trait TaskGraphLoader {
) -> Result<&vite_task_graph::IndexedTaskGraph, TaskGraphLoadError>;
}

#[derive(Debug, Serialize)]
pub struct ExecutionPlan {
root_node: ExecutionItemKind,
/// Plan a query execution: load the task graph, query it, and build the execution graph.
///
/// # Errors
/// Returns an error if task graph loading, query, or execution planning fails.
#[expect(clippy::future_not_send, reason = "PlanRequestParser and TaskGraphLoader are !Send")]
#[expect(clippy::implicit_hasher, reason = "FxHashMap is the only hasher used in this codebase")]
pub async fn plan_query(
query_plan_request: QueryPlanRequest,
workspace_path: &Arc<AbsolutePath>,
cwd: &Arc<AbsolutePath>,
envs: &FxHashMap<Arc<OsStr>, Arc<OsStr>>,
plan_request_parser: &mut (dyn PlanRequestParser + '_),
task_graph_loader: &mut (dyn TaskGraphLoader + '_),
) -> Result<ExecutionGraph, Error> {
let indexed_task_graph = task_graph_loader.load_task_graph().await?;

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

impl ExecutionPlan {
#[must_use]
pub const fn root_node(&self) -> &ExecutionItemKind {
&self.root_node
}

#[must_use]
pub fn into_root_node(self) -> ExecutionItemKind {
self.root_node
}

/// Returns `true` if the plan contains no tasks to execute.
#[must_use]
pub fn is_empty(&self) -> bool {
match &self.root_node {
ExecutionItemKind::Expanded(graph) => graph.node_count() == 0,
ExecutionItemKind::Leaf(_) => false,
}
}

/// Create an execution plan from an execution graph.
#[must_use]
pub const fn from_execution_graph(execution_graph: ExecutionGraph) -> Self {
Self { root_node: ExecutionItemKind::Expanded(execution_graph) }
}

/// Plan a query execution: load the task graph, query it, and build the execution graph.
///
/// # Errors
/// Returns an error if task graph loading, query, or execution planning fails.
#[expect(clippy::future_not_send, reason = "PlanRequestParser and TaskGraphLoader are !Send")]
pub async fn plan_query(
query_plan_request: QueryPlanRequest,
workspace_path: &Arc<AbsolutePath>,
cwd: &Arc<AbsolutePath>,
envs: &FxHashMap<Arc<OsStr>, Arc<OsStr>>,
plan_request_parser: &mut (dyn PlanRequestParser + '_),
task_graph_loader: &mut (dyn TaskGraphLoader + '_),
) -> Result<ExecutionGraph, Error> {
let indexed_task_graph = task_graph_loader.load_task_graph().await?;

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

/// Plan an execution from a plan request.
///
/// # Errors
/// Returns an error if task graph loading, query, or execution planning fails.
#[expect(clippy::future_not_send, reason = "PlanRequestParser and TaskGraphLoader are !Send")]
pub async fn plan(
plan_request: PlanRequest,
workspace_path: &Arc<AbsolutePath>,
cwd: &Arc<AbsolutePath>,
envs: &FxHashMap<Arc<OsStr>, Arc<OsStr>>,
plan_request_parser: &mut (dyn PlanRequestParser + '_),
task_graph_loader: &mut (dyn TaskGraphLoader + '_),
) -> Result<Self, Error> {
let root_node = match plan_request {
PlanRequest::Query(query_plan_request) => {
let execution_graph = Self::plan_query(
query_plan_request,
workspace_path,
cwd,
envs,
plan_request_parser,
task_graph_loader,
)
.await?;
ExecutionItemKind::Expanded(execution_graph)
}
PlanRequest::Synthetic(synthetic_plan_request) => {
let execution = plan_synthetic_request(
workspace_path,
&BTreeMap::default(),
synthetic_plan_request,
None,
cwd,
ParentCacheConfig::None,
)?;
ExecutionItemKind::Leaf(LeafExecutionKind::Spawn(execution))
}
};
Ok(Self { root_node })
}

/// Plan a synthetic task execution, returning the resolved [`SpawnExecution`] directly.
///
/// Unlike `plan_query` which returns a full execution graph, synthetic executions
/// are always a single spawned process. The caller can execute it directly using
/// `execute_spawn`.
///
/// # Errors
/// Returns an error if the program is not found or path fingerprinting fails.
#[expect(clippy::result_large_err, reason = "Error is large for diagnostics")]
pub fn plan_synthetic(
workspace_path: &Arc<AbsolutePath>,
cwd: &Arc<AbsolutePath>,
synthetic_plan_request: SyntheticPlanRequest,
cache_key: Arc<[Str]>,
) -> Result<Self, Error> {
let execution_cache_key = cache_metadata::ExecutionCacheKey::ExecAPI(cache_key);
let execution = plan_synthetic_request(
workspace_path,
&BTreeMap::default(),
synthetic_plan_request,
Some(execution_cache_key),
cwd,
ParentCacheConfig::None,
)?;
Ok(Self { root_node: ExecutionItemKind::Leaf(LeafExecutionKind::Spawn(execution)) })
}
/// Plan a synthetic task execution, returning the resolved [`SpawnExecution`] directly.
///
/// Unlike [`plan_query`] which returns a full execution graph, synthetic executions
/// are always a single spawned process. The caller can execute it directly using
/// `execute_spawn`.
///
/// # Errors
/// Returns an error if the program is not found or path fingerprinting fails.
#[expect(clippy::result_large_err, reason = "Error is large for diagnostics")]
pub fn plan_synthetic(
workspace_path: &Arc<AbsolutePath>,
cwd: &Arc<AbsolutePath>,
synthetic_plan_request: SyntheticPlanRequest,
cache_key: Arc<[Str]>,
) -> Result<SpawnExecution, Error> {
let execution_cache_key = cache_metadata::ExecutionCacheKey::ExecAPI(cache_key);
plan_synthetic_request(
workspace_path,
&BTreeMap::default(),
synthetic_plan_request,
Some(execution_cache_key),
cwd,
ParentCacheConfig::None,
)
}
Loading
Loading