Skip to content
Draft
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
16 changes: 16 additions & 0 deletions crates/vite_task/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ use vite_task_graph::{TaskSpecifier, query::TaskQuery};
use vite_task_plan::plan_request::{CacheOverride, PlanOptions, QueryPlanRequest};
use vite_workspace::package_filter::{PackageQueryArgs, PackageQueryError};

/// Controls how task output is displayed.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, clap::ValueEnum)]
pub enum LogMode {
/// Output streams directly to the terminal as tasks produce it.
#[default]
Interleaved,
/// Each line is prefixed with `[packageName#taskName]`.
Labeled,
/// Output is buffered per task and printed as a block after each task completes.
Grouped,
}

#[derive(Debug, Clone, clap::Subcommand)]
pub enum CacheSubcommand {
/// Clean up all the cache
Expand Down Expand Up @@ -35,6 +47,10 @@ pub struct RunFlags {
/// Force caching off for all tasks and scripts.
#[clap(long, conflicts_with = "cache")]
pub no_cache: bool,

/// How task output is displayed.
#[clap(long, default_value = "interleaved")]
pub log: LogMode,
}

impl RunFlags {
Expand Down
4 changes: 2 additions & 2 deletions crates/vite_task/src/session/cache/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ pub struct ExecutionCache {

const BINCODE_CONFIG: bincode::config::Configuration = bincode::config::standard();

#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[expect(
clippy::large_enum_variant,
reason = "FingerprintMismatch contains SpawnFingerprint which is intentionally large; boxing would add unnecessary indirection for a short-lived enum"
Expand All @@ -93,7 +93,7 @@ pub enum InputChangeKind {
Removed,
}

#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FingerprintMismatch {
/// Found a previous cache entry key for the same task, but the spawn fingerprint differs.
/// This happens when the command itself or an env changes.
Expand Down
4 changes: 2 additions & 2 deletions crates/vite_task/src/session/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ pub enum ExecutionError {
PostRunFingerprint(#[source] anyhow::Error),
}

#[derive(Debug)]
#[derive(Debug, Clone)]
pub enum CacheDisabledReason {
InProcessExecution,
NoCacheMetadata,
Expand Down Expand Up @@ -77,7 +77,7 @@ pub enum CacheUpdateStatus {
NotUpdated(CacheNotUpdatedReason),
}

#[derive(Debug)]
#[derive(Debug, Clone)]
#[expect(
clippy::large_enum_variant,
reason = "CacheMiss variant is intentionally large and infrequently cloned"
Expand Down
36 changes: 7 additions & 29 deletions crates/vite_task/src/session/execute/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,22 +66,13 @@ impl ExecutionContext<'_> {
/// We compute a topological order and iterate in reverse to get execution order
/// (dependencies before dependents).
///
/// `all_ancestors_single_node` tracks whether every graph in the ancestry chain
/// (from the root down to this level) contains exactly one node. The initial call
/// passes `graph.node_count() == 1`; recursive calls AND with the nested graph's
/// node count.
///
/// Fast-fail: if any task fails (non-zero exit or infrastructure error), remaining
/// tasks and `&&`-chained items are skipped. Leaf-level errors are reported through
/// the reporter. Cycle detection is handled at plan time.
///
/// Returns `true` if all tasks succeeded, `false` if any task failed.
#[tracing::instrument(level = "debug", skip_all)]
async fn execute_expanded_graph(
&mut self,
graph: &ExecutionGraph,
all_ancestors_single_node: bool,
) -> bool {
async fn execute_expanded_graph(&mut self, graph: &ExecutionGraph) -> bool {
// `compute_topological_order()` returns nodes in topological order: for every
// edge A→B, A appears before B. Since our edges mean "A depends on B",
// dependencies (B) appear after their dependents (A). We iterate in reverse
Expand All @@ -97,23 +88,13 @@ impl ExecutionContext<'_> {
for item in &task_execution.items {
let failed = match &item.kind {
ExecutionItemKind::Leaf(leaf_kind) => {
self.execute_leaf(
&item.execution_item_display,
leaf_kind,
all_ancestors_single_node,
)
.boxed_local()
.await
}
ExecutionItemKind::Expanded(nested_graph) => {
!self
.execute_expanded_graph(
nested_graph,
all_ancestors_single_node && nested_graph.node_count() == 1,
)
self.execute_leaf(&item.execution_item_display, leaf_kind)
.boxed_local()
.await
}
ExecutionItemKind::Expanded(nested_graph) => {
!self.execute_expanded_graph(nested_graph).boxed_local().await
}
};
if failed {
return false;
Expand All @@ -134,10 +115,8 @@ impl ExecutionContext<'_> {
&mut self,
display: &ExecutionItemDisplay,
leaf_kind: &LeafExecutionKind,
all_ancestors_single_node: bool,
) -> bool {
let mut leaf_reporter =
self.reporter.new_leaf_execution(display, leaf_kind, all_ancestors_single_node);
let mut leaf_reporter = self.reporter.new_leaf_execution(display, leaf_kind);

match leaf_kind {
LeafExecutionKind::InProcess(in_process_execution) => {
Expand Down Expand Up @@ -541,8 +520,7 @@ impl Session<'_> {

// Execute the graph with fast-fail: if any task fails, remaining tasks
// are skipped. Leaf-level errors are reported through the reporter.
let all_single_node = execution_graph.node_count() == 1;
execution_context.execute_expanded_graph(&execution_graph, all_single_node).await;
execution_context.execute_expanded_graph(&execution_graph).await;

// Leaf-level errors and non-zero exit statuses are tracked internally
// by the reporter.
Expand Down
30 changes: 25 additions & 5 deletions crates/vite_task/src/session/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ use clap::Parser as _;
use once_cell::sync::OnceCell;
pub use reporter::ExitStatus;
use reporter::{
LabeledReporterBuilder,
GroupedReporterBuilder, InterleavedReporterBuilder, LabeledReporterBuilder,
SummaryReporterBuilder,
summary::{LastRunSummary, ReadSummaryError, format_full_summary},
};
use rustc_hash::FxHashMap;
Expand Down Expand Up @@ -300,14 +301,33 @@ impl<'a> Session<'a> {
self.plan_from_query(qpr).await?
};

let builder = LabeledReporterBuilder::new(
self.workspace_path(),
let workspace_path = self.workspace_path();
let writer: Box<dyn std::io::Write> = Box::new(std::io::stdout());

let inner: Box<dyn reporter::GraphExecutionReporterBuilder> =
match run_command.flags.log {
crate::cli::LogMode::Interleaved => Box::new(
InterleavedReporterBuilder::new(Arc::clone(&workspace_path), writer),
),
crate::cli::LogMode::Labeled => Box::new(LabeledReporterBuilder::new(
Arc::clone(&workspace_path),
writer,
)),
crate::cli::LogMode::Grouped => Box::new(GroupedReporterBuilder::new(
Arc::clone(&workspace_path),
writer,
)),
};

let builder = Box::new(SummaryReporterBuilder::new(
inner,
workspace_path,
Box::new(std::io::stdout()),
run_command.flags.verbose,
Some(self.make_summary_writer()),
self.program_name.clone(),
);
self.execute_graph(graph, Box::new(builder)).await.map_err(SessionError::EarlyExit)
));
self.execute_graph(graph, builder).await.map_err(SessionError::EarlyExit)
}
}
}
Expand Down
153 changes: 153 additions & 0 deletions crates/vite_task/src/session/reporter/grouped/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
//! Grouped reporter — buffers output per task, prints as a block on completion.

use std::{cell::RefCell, io::Write, process::ExitStatus as StdExitStatus, rc::Rc, sync::Arc};

use vite_path::AbsolutePath;
use vite_task_plan::{ExecutionItemDisplay, LeafExecutionKind};

use super::{
ExitStatus, GraphExecutionReporter, GraphExecutionReporterBuilder, LeafExecutionReporter,
StdioConfig, StdioSuggestion, format_command_with_cache_status, format_task_label,
write_leaf_trailing_output,
};
use crate::session::event::{CacheStatus, CacheUpdateStatus, ExecutionError};

mod writer;

use writer::GroupedWriter;

pub struct GroupedReporterBuilder {
workspace_path: Arc<AbsolutePath>,
writer: Box<dyn Write>,
}

impl GroupedReporterBuilder {
pub fn new(workspace_path: Arc<AbsolutePath>, writer: Box<dyn Write>) -> Self {
Self { workspace_path, writer }
}
}

impl GraphExecutionReporterBuilder for GroupedReporterBuilder {
fn build(self: Box<Self>) -> Box<dyn GraphExecutionReporter> {
Box::new(GroupedGraphReporter {
writer: Rc::new(RefCell::new(self.writer)),
workspace_path: self.workspace_path,
})
}
}

struct GroupedGraphReporter {
writer: Rc<RefCell<Box<dyn Write>>>,
workspace_path: Arc<AbsolutePath>,
}

impl GraphExecutionReporter for GroupedGraphReporter {
fn new_leaf_execution(
&mut self,
display: &ExecutionItemDisplay,
_leaf_kind: &LeafExecutionKind,
) -> Box<dyn LeafExecutionReporter> {
let label = format_task_label(display);
Box::new(GroupedLeafReporter {
writer: Rc::clone(&self.writer),
display: display.clone(),
workspace_path: Arc::clone(&self.workspace_path),
label,
started: false,
grouped_buffer: None,
})
}

fn finish(self: Box<Self>) -> Result<(), ExitStatus> {
let mut writer = self.writer.borrow_mut();
let _ = writer.flush();
Ok(())
}
}

struct GroupedLeafReporter {
writer: Rc<RefCell<Box<dyn Write>>>,
display: ExecutionItemDisplay,
workspace_path: Arc<AbsolutePath>,
label: vite_str::Str,
started: bool,
grouped_buffer: Option<Rc<RefCell<Vec<u8>>>>,
}

impl LeafExecutionReporter for GroupedLeafReporter {
fn start(&mut self, cache_status: CacheStatus) -> StdioConfig {
let line =
format_command_with_cache_status(&self.display, &self.workspace_path, &cache_status);

self.started = true;

// Print labeled command line immediately (before output is buffered).
let labeled_line = vite_str::format!("{} {line}", self.label);
let mut writer = self.writer.borrow_mut();
let _ = writer.write_all(labeled_line.as_bytes());
let _ = writer.flush();

// Create shared buffer for both stdout and stderr.
let buffer = Rc::new(RefCell::new(Vec::new()));
self.grouped_buffer = Some(Rc::clone(&buffer));

StdioConfig {
suggestion: StdioSuggestion::Piped,
stdout_writer: Box::new(GroupedWriter::new(Rc::clone(&buffer))),
stderr_writer: Box::new(GroupedWriter::new(buffer)),
}
}

fn finish(
self: Box<Self>,
_status: Option<StdExitStatus>,
_cache_update_status: CacheUpdateStatus,
error: Option<ExecutionError>,
) {
// Build grouped block: header + buffered output.
let mut extra = Vec::new();
if let Some(ref grouped_buffer) = self.grouped_buffer {
let content = grouped_buffer.borrow();
if !content.is_empty() {
let header = vite_str::format!("── {} ──\n", self.label);
extra.extend_from_slice(header.as_bytes());
extra.extend_from_slice(&content);
}
}

write_leaf_trailing_output(&self.writer, error, self.started, &extra);
}
}

#[cfg(test)]
mod tests {
use vite_task_plan::ExecutionItemKind;

use super::*;
use crate::session::{
event::CacheDisabledReason,
reporter::{
StdioSuggestion,
test_fixtures::{spawn_task, test_path},
},
};

fn leaf_kind(item: &vite_task_plan::ExecutionItem) -> &LeafExecutionKind {
match &item.kind {
ExecutionItemKind::Leaf(kind) => kind,
ExecutionItemKind::Expanded(_) => panic!("test fixture item must be a Leaf"),
}
}

#[test]
fn always_suggests_piped() {
let task = spawn_task("build");
let item = &task.items[0];

let builder = Box::new(GroupedReporterBuilder::new(test_path(), Box::new(std::io::sink())));
let mut reporter = builder.build();
let mut leaf = reporter.new_leaf_execution(&item.execution_item_display, leaf_kind(item));
let stdio_config = leaf.start(CacheStatus::Disabled(CacheDisabledReason::NoCacheMetadata));
assert_eq!(stdio_config.suggestion, StdioSuggestion::Piped);
}
}
Loading
Loading