diff --git a/CHANGELOG.md b/CHANGELOG.md index b702c12e9..add778222 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +- **Changed** Cached logs are stored with colors intact (`FORCE_COLOR=1` is auto-injected into spawned tasks). Colors are then stripped at display time when the terminal does not support them. Other color-related env vars (`NO_COLOR`, `COLORTERM`, `TERM`, `TERM_PROGRAM`) are no longer passed through by default. Opt in via a task's `env`/`untrackedEnv` ([#378](https://github.com/voidzero-dev/vite-task/pull/378)) - **Added** `output` field for cached tasks: archives matching files after a successful run and restores them on cache hit ([#375](https://github.com/voidzero-dev/vite-task/pull/375)) - **Fixed** Windows cached tasks can now run package shims rewritten through PowerShell; default env passthrough now preserves `PATHEXT` ([#366](https://github.com/voidzero-dev/vite-task/pull/366)) - **Added** Platform support for targets without `input` auto-inference (e.g. Android). Tasks still run; those relying on auto-inference run uncached, with the summary noting that `input` must be configured manually to enable caching ([#352](https://github.com/voidzero-dev/vite-task/pull/352)) diff --git a/Cargo.lock b/Cargo.lock index 179a9470e..5c645d323 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4093,6 +4093,7 @@ dependencies = [ name = "vite_task" version = "0.0.0" dependencies = [ + "anstream 0.6.21", "anyhow", "async-trait", "clap", @@ -4110,6 +4111,7 @@ dependencies = [ "rustc-hash", "serde", "serde_json", + "supports-color 3.0.2", "tar", "tempfile", "thiserror 2.0.18", @@ -4206,7 +4208,6 @@ dependencies = [ "sha2 0.11.0", "shell-escape", "snapshot_test", - "supports-color 3.0.2", "tempfile", "thiserror 2.0.18", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 5583f09b0..f7f77892f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ future_not_send = "allow" [workspace.dependencies] allocator-api2 = { version = "0.2.21", default-features = false, features = ["alloc", "std"] } +anstream = "0.6.21" anyhow = "1.0.98" assert2 = "0.4.0" assertables = "9.8.1" diff --git a/crates/pty_terminal/src/terminal.rs b/crates/pty_terminal/src/terminal.rs index b77673bb9..164c6c186 100644 --- a/crates/pty_terminal/src/terminal.rs +++ b/crates/pty_terminal/src/terminal.rs @@ -128,6 +128,53 @@ impl PtyReader { self.parser.lock().unwrap().screen().contents() } + /// Returns the screen contents row-by-row with inline ANSI SGR escapes + /// preserved — useful for snapshot tests that need to assert colour/style. + /// + /// Rows are produced via [`vt100::Screen::rows_formatted`], which emits + /// only the SGR attribute escapes (no cursor positioning, no + /// screen-erase sequences), so the output is platform-stable. Trailing + /// fully-empty rows are dropped; remaining rows are joined with `\n`. + /// + /// Bare SGR-reset sequences (`\x1b[m`) are also stripped: Unix PTYs emit + /// them between styled spans and at the end of styled runs, but Windows + /// `ConPTY` consolidates the byte stream and elides those resets. Stripping + /// them produces identical output on all platforms while preserving the + /// non-reset SGR transitions that the test actually cares about. + /// + /// # Panics + /// + /// Panics if the parser lock is poisoned. + #[expect( + clippy::significant_drop_tightening, + reason = "vt100::Screen::rows_formatted yields borrowed iterators that need the guard alive" + )] + #[must_use] + pub fn screen_contents_formatted(&self) -> Vec { + const RESET: &[u8] = b"\x1b[m"; + let guard = self.parser.lock().unwrap(); + let screen = guard.screen(); + let cols = screen.size().1; + let rows: Vec> = screen + .rows_formatted(0, cols) + .map(|mut row| { + while let Some(idx) = row.windows(RESET.len()).position(|w| w == RESET) { + row.drain(idx..idx + RESET.len()); + } + row + }) + .collect(); + let last_non_empty = rows.iter().rposition(|r| !r.is_empty()).map_or(0, |i| i + 1); + let mut out = Vec::new(); + for (i, row) in rows[..last_non_empty].iter().enumerate() { + if i > 0 { + out.push(b'\n'); + } + out.extend_from_slice(row); + } + out + } + /// Drains and returns all unhandled OSC sequences received since the last call. /// /// Each entry is a list of byte-vector parameters from a single OSC sequence diff --git a/crates/pty_terminal_test/src/lib.rs b/crates/pty_terminal_test/src/lib.rs index b0cbe0d17..af724599e 100644 --- a/crates/pty_terminal_test/src/lib.rs +++ b/crates/pty_terminal_test/src/lib.rs @@ -52,6 +52,13 @@ impl Reader { contents } + /// Returns the screen contents with inline ANSI SGR escape codes preserved. + /// Useful for snapshot tests that need to assert colour or style attributes. + #[must_use] + pub fn screen_contents_formatted(&self) -> Vec { + self.pty.get_ref().screen_contents_formatted() + } + /// Reads from the PTY until a milestone with the given name is encountered. /// /// Returns the terminal screen contents at the moment the milestone is detected. diff --git a/crates/vite_task/Cargo.toml b/crates/vite_task/Cargo.toml index f5d77780b..90f510572 100644 --- a/crates/vite_task/Cargo.toml +++ b/crates/vite_task/Cargo.toml @@ -12,6 +12,7 @@ rust-version.workspace = true workspace = true [dependencies] +anstream = { workspace = true } anyhow = { workspace = true } async-trait = { workspace = true } wincode = { workspace = true, features = ["derive"] } @@ -28,6 +29,7 @@ rusqlite = { workspace = true, features = ["bundled"] } rustc-hash = { workspace = true } serde = { workspace = true, features = ["derive", "rc"] } serde_json = { workspace = true } +supports-color = { workspace = true } thiserror = { workspace = true } tar = { workspace = true } tokio = { workspace = true, features = [ diff --git a/crates/vite_task/src/session/mod.rs b/crates/vite_task/src/session/mod.rs index 478421976..ed1fd5cb8 100644 --- a/crates/vite_task/src/session/mod.rs +++ b/crates/vite_task/src/session/mod.rs @@ -12,7 +12,7 @@ use clap::Parser as _; use once_cell::sync::OnceCell; pub use reporter::ExitStatus; use reporter::{ - GroupedReporterBuilder, InterleavedReporterBuilder, LabeledReporterBuilder, + ColorSupport, GroupedReporterBuilder, InterleavedReporterBuilder, LabeledReporterBuilder, SummaryReporterBuilder, summary::{LastRunSummary, ReadSummaryError, format_full_summary}, }; @@ -313,20 +313,36 @@ impl<'a> Session<'a> { let workspace_path = self.workspace_path(); let writer: Box = Box::new(std::io::stdout()); - let inner: Box = - 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, - )), - }; + // Detect color support once at the point where reporters are + // constructed. The reporters and their pipe writers then strip + // ANSI escapes from cached/replayed output if the terminal + // can't render them. Detect per-stream so a redirected stdout + // doesn't trigger stripping of an interactive stderr. + let color_support = ColorSupport { + stdout: stdout_supports_color(), + stderr: stderr_supports_color(), + }; + + let inner: Box = match run_command + .flags + .log + { + crate::cli::LogMode::Interleaved => Box::new(InterleavedReporterBuilder::new( + Arc::clone(&workspace_path), + writer, + color_support, + )), + crate::cli::LogMode::Labeled => Box::new(LabeledReporterBuilder::new( + Arc::clone(&workspace_path), + writer, + color_support, + )), + crate::cli::LogMode::Grouped => Box::new(GroupedReporterBuilder::new( + Arc::clone(&workspace_path), + writer, + color_support, + )), + }; let builder = Box::new(SummaryReporterBuilder::new( inner, @@ -335,6 +351,7 @@ impl<'a> Session<'a> { run_command.flags.verbose, Some(self.make_summary_writer()), self.program_name.clone(), + color_support, )); // Don't let SIGINT/CTRL_C kill the runner. Child tasks receive // the signal directly from the terminal driver and handle it @@ -590,6 +607,10 @@ impl<'a> Session<'a> { let path = self.summary_file_path(); match LastRunSummary::read_from_path(&path) { Ok(Some(summary)) => { + // `format_full_summary` decides colour vs plain text per + // styled span via `ColorizeExt` (which consults + // `supports-color`), so the buffer already matches the + // terminal's capability and we write it to stdout directly. let buf = format_full_summary(&summary); { use std::io::Write; @@ -668,8 +689,11 @@ impl<'a> Session<'a> { let cache = self.cache()?; // Create a plain (standalone) reporter — no graph awareness, no summary - let plain_reporter = - reporter::PlainReporter::new(silent_if_cache_hit, Box::new(std::io::stdout())); + let plain_reporter = reporter::PlainReporter::new( + silent_if_cache_hit, + Box::new(std::io::stdout()), + ColorSupport { stdout: stdout_supports_color(), stderr: stderr_supports_color() }, + ); // Execute the spawn directly using the free function, bypassing the graph pipeline let outcome = execute::execute_spawn( @@ -770,3 +794,21 @@ impl<'a> Session<'a> { .await } } + +/// Whether stdout supports ANSI color output for the current process. Honors +/// `NO_COLOR`/`FORCE_COLOR` and detects TTY capability via the `supports-color` +/// crate. Result is cached for the process lifetime. +fn stdout_supports_color() -> bool { + use std::sync::OnceLock; + static CACHE: OnceLock = OnceLock::new(); + *CACHE.get_or_init(|| supports_color::on(supports_color::Stream::Stdout).is_some()) +} + +/// Whether stderr supports ANSI color output. Detected independently from +/// stdout so a redirected stdout (non-TTY) does not strip ANSI from a stderr +/// that is still an interactive terminal. +fn stderr_supports_color() -> bool { + use std::sync::OnceLock; + static CACHE: OnceLock = OnceLock::new(); + *CACHE.get_or_init(|| supports_color::on(supports_color::Stream::Stderr).is_some()) +} diff --git a/crates/vite_task/src/session/reporter/grouped/mod.rs b/crates/vite_task/src/session/reporter/grouped/mod.rs index fd1816845..d3c508808 100644 --- a/crates/vite_task/src/session/reporter/grouped/mod.rs +++ b/crates/vite_task/src/session/reporter/grouped/mod.rs @@ -7,9 +7,10 @@ use vite_path::AbsolutePath; use vite_task_plan::{ExecutionItemDisplay, LeafExecutionKind}; use super::{ - ColorizeExt, ExitStatus, GraphExecutionReporter, GraphExecutionReporterBuilder, + ColorSupport, ColorizeExt, ExitStatus, GraphExecutionReporter, GraphExecutionReporterBuilder, LeafExecutionReporter, PipeWriters, StdioConfig, StdioSuggestion, - format_command_with_cache_status, format_task_label, write_leaf_trailing_output, + format_command_with_cache_status, format_task_label, maybe_strip_writer, + write_leaf_trailing_output, }; use crate::session::event::{CacheStatus, CacheUpdateStatus, ExecutionError}; @@ -20,11 +21,21 @@ use writer::GroupedWriter; pub struct GroupedReporterBuilder { workspace_path: Arc, writer: Box, + color_support: ColorSupport, } impl GroupedReporterBuilder { - pub fn new(workspace_path: Arc, writer: Box) -> Self { - Self { workspace_path, writer } + /// Grouped mode buffers child output and flushes it through `writer` + /// at finish time. The pipe writers themselves (see + /// `LeafExecutionReporter::start`) strip ANSI on the way into the buffer, + /// so by the time the buffer reaches `writer` it already matches the + /// terminal's colour capability. `writer` is therefore stored unwrapped. + pub fn new( + workspace_path: Arc, + writer: Box, + color_support: ColorSupport, + ) -> Self { + Self { workspace_path, writer, color_support } } } @@ -33,6 +44,7 @@ impl GraphExecutionReporterBuilder for GroupedReporterBuilder { Box::new(GroupedGraphReporter { writer: Rc::new(RefCell::new(self.writer)), workspace_path: self.workspace_path, + color_support: self.color_support, }) } } @@ -40,6 +52,7 @@ impl GraphExecutionReporterBuilder for GroupedReporterBuilder { struct GroupedGraphReporter { writer: Rc>>, workspace_path: Arc, + color_support: ColorSupport, } impl GraphExecutionReporter for GroupedGraphReporter { @@ -56,6 +69,7 @@ impl GraphExecutionReporter for GroupedGraphReporter { label, started: false, grouped_buffer: None, + color_support: self.color_support, }) } @@ -73,6 +87,7 @@ struct GroupedLeafReporter { label: vite_str::Str, started: bool, grouped_buffer: Option>>>, + color_support: ColorSupport, } impl LeafExecutionReporter for GroupedLeafReporter { @@ -95,8 +110,14 @@ impl LeafExecutionReporter for GroupedLeafReporter { StdioConfig { suggestion: StdioSuggestion::Piped, writers: PipeWriters { - stdout_writer: Box::new(GroupedWriter::new(Rc::clone(&buffer))), - stderr_writer: Box::new(GroupedWriter::new(buffer)), + stdout_writer: maybe_strip_writer( + Box::new(GroupedWriter::new(Rc::clone(&buffer))), + self.color_support.stdout, + ), + stderr_writer: maybe_strip_writer( + Box::new(GroupedWriter::new(buffer)), + self.color_support.stderr, + ), }, } } @@ -152,7 +173,11 @@ mod tests { let task = spawn_task("build"); let item = &task.items[0]; - let builder = Box::new(GroupedReporterBuilder::new(test_path(), Box::new(std::io::sink()))); + let builder = Box::new(GroupedReporterBuilder::new( + test_path(), + Box::new(std::io::sink()), + ColorSupport::uniform(false), + )); 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)); diff --git a/crates/vite_task/src/session/reporter/interleaved/mod.rs b/crates/vite_task/src/session/reporter/interleaved/mod.rs index e7f17f83b..9722e95e0 100644 --- a/crates/vite_task/src/session/reporter/interleaved/mod.rs +++ b/crates/vite_task/src/session/reporter/interleaved/mod.rs @@ -6,20 +6,30 @@ use vite_path::AbsolutePath; use vite_task_plan::{ExecutionItemDisplay, LeafExecutionKind}; use super::{ - ExitStatus, GraphExecutionReporter, GraphExecutionReporterBuilder, LeafExecutionReporter, - PipeWriters, StdioConfig, StdioSuggestion, format_command_with_cache_status, - write_leaf_trailing_output, + ColorSupport, ExitStatus, GraphExecutionReporter, GraphExecutionReporterBuilder, + LeafExecutionReporter, PipeWriters, StdioConfig, StdioSuggestion, + format_command_with_cache_status, maybe_strip_writer, write_leaf_trailing_output, }; use crate::session::event::{CacheStatus, CacheUpdateStatus, ExecutionError}; pub struct InterleavedReporterBuilder { workspace_path: Arc, writer: Box, + color_support: ColorSupport, } impl InterleavedReporterBuilder { - pub fn new(workspace_path: Arc, writer: Box) -> Self { - Self { workspace_path, writer } + /// The reporter's own writes (command lines, error banners) decide + /// colour-vs-plain at format time via `ColorizeExt`, so `writer` is + /// stored unwrapped. `color_support` is forwarded to the pipe writers + /// in `LeafExecutionReporter::start`, where ANSI emitted by child tasks is stripped + /// for non-terminal sinks. + pub fn new( + workspace_path: Arc, + writer: Box, + color_support: ColorSupport, + ) -> Self { + Self { workspace_path, writer, color_support } } } @@ -28,6 +38,7 @@ impl GraphExecutionReporterBuilder for InterleavedReporterBuilder { Box::new(InterleavedGraphReporter { writer: Rc::new(RefCell::new(self.writer)), workspace_path: self.workspace_path, + color_support: self.color_support, }) } } @@ -35,6 +46,7 @@ impl GraphExecutionReporterBuilder for InterleavedReporterBuilder { struct InterleavedGraphReporter { writer: Rc>>, workspace_path: Arc, + color_support: ColorSupport, } impl GraphExecutionReporter for InterleavedGraphReporter { @@ -54,6 +66,7 @@ impl GraphExecutionReporter for InterleavedGraphReporter { workspace_path: Arc::clone(&self.workspace_path), stdio_suggestion, started: false, + color_support: self.color_support, }) } @@ -70,6 +83,7 @@ struct InterleavedLeafReporter { workspace_path: Arc, stdio_suggestion: StdioSuggestion, started: bool, + color_support: ColorSupport, } impl LeafExecutionReporter for InterleavedLeafReporter { @@ -86,8 +100,14 @@ impl LeafExecutionReporter for InterleavedLeafReporter { StdioConfig { suggestion: self.stdio_suggestion, writers: PipeWriters { - stdout_writer: Box::new(std::io::stdout()), - stderr_writer: Box::new(std::io::stderr()), + stdout_writer: maybe_strip_writer( + Box::new(std::io::stdout()), + self.color_support.stdout, + ), + stderr_writer: maybe_strip_writer( + Box::new(std::io::stderr()), + self.color_support.stderr, + ), }, } } @@ -126,8 +146,11 @@ mod tests { display: &ExecutionItemDisplay, leaf_kind: &LeafExecutionKind, ) -> StdioSuggestion { - let builder = - Box::new(InterleavedReporterBuilder::new(test_path(), Box::new(std::io::sink()))); + let builder = Box::new(InterleavedReporterBuilder::new( + test_path(), + Box::new(std::io::sink()), + ColorSupport::uniform(false), + )); let mut reporter = builder.build(); let mut leaf = reporter.new_leaf_execution(display, leaf_kind); leaf.start(CacheStatus::Disabled(CacheDisabledReason::NoCacheMetadata)).suggestion diff --git a/crates/vite_task/src/session/reporter/labeled/mod.rs b/crates/vite_task/src/session/reporter/labeled/mod.rs index 500e190ef..fa41fa641 100644 --- a/crates/vite_task/src/session/reporter/labeled/mod.rs +++ b/crates/vite_task/src/session/reporter/labeled/mod.rs @@ -6,8 +6,9 @@ use vite_path::AbsolutePath; use vite_task_plan::{ExecutionItemDisplay, LeafExecutionKind}; use super::{ - ExitStatus, GraphExecutionReporter, GraphExecutionReporterBuilder, LeafExecutionReporter, - PipeWriters, StdioConfig, StdioSuggestion, format_command_with_cache_status, format_task_label, + ColorSupport, ExitStatus, GraphExecutionReporter, GraphExecutionReporterBuilder, + LeafExecutionReporter, PipeWriters, StdioConfig, StdioSuggestion, + format_command_with_cache_status, format_task_label, maybe_strip_writer, write_leaf_trailing_output, }; use crate::session::event::{CacheStatus, CacheUpdateStatus, ExecutionError}; @@ -19,11 +20,19 @@ use writer::LabeledWriter; pub struct LabeledReporterBuilder { workspace_path: Arc, writer: Box, + color_support: ColorSupport, } impl LabeledReporterBuilder { - pub fn new(workspace_path: Arc, writer: Box) -> Self { - Self { workspace_path, writer } + /// `writer` is stored unwrapped — the reporter's own writes pick + /// colour-vs-plain at format time via `ColorizeExt`. Child-process + /// pipes are stripped per-stream inside `LeafExecutionReporter::start`. + pub fn new( + workspace_path: Arc, + writer: Box, + color_support: ColorSupport, + ) -> Self { + Self { workspace_path, writer, color_support } } } @@ -32,6 +41,7 @@ impl GraphExecutionReporterBuilder for LabeledReporterBuilder { Box::new(LabeledGraphReporter { writer: Rc::new(RefCell::new(self.writer)), workspace_path: self.workspace_path, + color_support: self.color_support, }) } } @@ -39,6 +49,7 @@ impl GraphExecutionReporterBuilder for LabeledReporterBuilder { struct LabeledGraphReporter { writer: Rc>>, workspace_path: Arc, + color_support: ColorSupport, } impl GraphExecutionReporter for LabeledGraphReporter { @@ -52,6 +63,7 @@ impl GraphExecutionReporter for LabeledGraphReporter { display: display.clone(), workspace_path: Arc::clone(&self.workspace_path), started: false, + color_support: self.color_support, }) } @@ -67,6 +79,7 @@ struct LabeledLeafReporter { display: ExecutionItemDisplay, workspace_path: Arc, started: bool, + color_support: ColorSupport, } impl LeafExecutionReporter for LabeledLeafReporter { @@ -88,11 +101,11 @@ impl LeafExecutionReporter for LabeledLeafReporter { suggestion: StdioSuggestion::Piped, writers: PipeWriters { stdout_writer: Box::new(LabeledWriter::new( - Box::new(std::io::stdout()), + maybe_strip_writer(Box::new(std::io::stdout()), self.color_support.stdout), prefix.as_bytes().to_vec(), )), stderr_writer: Box::new(LabeledWriter::new( - Box::new(std::io::stderr()), + maybe_strip_writer(Box::new(std::io::stderr()), self.color_support.stderr), prefix.as_bytes().to_vec(), )), }, @@ -134,7 +147,11 @@ mod tests { let task = spawn_task("build"); let item = &task.items[0]; - let builder = Box::new(LabeledReporterBuilder::new(test_path(), Box::new(std::io::sink()))); + let builder = Box::new(LabeledReporterBuilder::new( + test_path(), + Box::new(std::io::sink()), + ColorSupport::uniform(false), + )); 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)); diff --git a/crates/vite_task/src/session/reporter/mod.rs b/crates/vite_task/src/session/reporter/mod.rs index 82724ecd7..d8ca219ac 100644 --- a/crates/vite_task/src/session/reporter/mod.rs +++ b/crates/vite_task/src/session/reporter/mod.rs @@ -30,12 +30,12 @@ mod plain; pub mod summary; mod summary_reporter; -use std::{io::Write, process::ExitStatus as StdExitStatus, sync::LazyLock}; +use std::{io::Write, process::ExitStatus as StdExitStatus}; pub use grouped::GroupedReporterBuilder; pub use interleaved::InterleavedReporterBuilder; pub use labeled::LabeledReporterBuilder; -use owo_colors::{Style, Styled}; +use owo_colors::Style; pub use plain::PlainReporter; pub use summary_reporter::SummaryReporterBuilder; use vite_path::AbsolutePath; @@ -105,6 +105,38 @@ pub struct PipeWriters { pub stderr_writer: Box, } +/// Color-support decision split per output stream. Reporter builders receive +/// one of these so a non-TTY stdout doesn't accidentally strip colours from +/// a TTY stderr (or vice versa). +#[derive(Debug, Clone, Copy)] +pub struct ColorSupport { + /// Whether the reporter's stdout writer (and stdout-bound pipe writers + /// for spawned tasks) supports ANSI escapes. + pub stdout: bool, + /// Whether stderr-bound pipe writers support ANSI escapes. + pub stderr: bool, +} + +#[cfg(test)] +impl ColorSupport { + /// Treat both streams the same — only used in tests to avoid duplicating + /// field assignments. + pub(super) const fn uniform(supported: bool) -> Self { + Self { stdout: supported, stderr: supported } + } +} + +/// Wrap a writer with [`anstream::StripStream`] when `color_support` is +/// `false`. Used by reporter builders to ensure ANSI escape sequences emitted +/// by the reporter or by spawned tasks are stripped at display time when the +/// user's terminal cannot render them. +/// +/// [`anstream::StripStream`] is incremental: a single escape sequence split +/// across multiple `write` calls is still removed correctly. +pub(super) fn maybe_strip_writer(writer: Box, color_support: bool) -> Box { + if color_support { writer } else { Box::new(anstream::StripStream::new(writer)) } +} + // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Typestate traits // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @@ -182,22 +214,30 @@ pub trait LeafExecutionReporter { // Shared display helpers // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -/// Wrap of `OwoColorize` that ignores style if `NO_COLOR` is set. -trait ColorizeExt { - fn style(&self, style: Style) -> Styled<&Self>; +const COMMAND_STYLE: Style = Style::new().blue(); +const CACHE_MISS_STYLE: Style = Style::new().bright_black(); + +/// Apply `style` to `self` only when stdout supports ANSI colours +/// (auto-detected via the `supports-color` crate, honouring `NO_COLOR`, +/// `FORCE_COLOR`, and TTY). Used by the format helpers that write to the +/// reporter's main writer / saved-summary buffer; for child-process pipes +/// see [`maybe_strip_writer`] instead, which strips bytes the runner does +/// not control. +trait ColorizeExt: owo_colors::OwoColorize { + fn style(&self, style: Style) -> impl std::fmt::Display + '_; } -impl ColorizeExt for T { - fn style(&self, style: Style) -> Styled<&Self> { - static NO_COLOR: LazyLock = - LazyLock::new(|| std::env::var_os("NO_COLOR").is_some_and(|v| !v.is_empty())); - owo_colors::OwoColorize::style(self, if *NO_COLOR { Style::new() } else { style }) +impl ColorizeExt for T +where + T: owo_colors::OwoColorize + std::fmt::Display, +{ + fn style(&self, style: Style) -> impl std::fmt::Display + '_ { + self.if_supports_color(owo_colors::Stream::Stdout, move |s| { + owo_colors::OwoColorize::style(s, style) + }) } } -const COMMAND_STYLE: Style = Style::new().blue(); -const CACHE_MISS_STYLE: Style = Style::new().bright_black(); - /// Format the display's cwd as a string relative to the workspace root. /// Returns an empty string if the cwd equals the workspace root. fn format_cwd_relative(display: &ExecutionItemDisplay, workspace_path: &AbsolutePath) -> Str { @@ -301,6 +341,89 @@ fn format_cache_hit_message() -> Str { vite_str::format!("{}\n", "◉ cache hit, logs replayed".style(Style::new().green().dimmed())) } +#[cfg(test)] +mod strip_writer_tests { + use std::io::Write; + + use super::maybe_strip_writer; + + /// Collect every byte written to an inner `Vec` via a wrapping writer. + /// Helper used to inspect what `maybe_strip_writer` actually emitted. + struct SharedSink(std::rc::Rc>>); + + impl Write for SharedSink { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.0.borrow_mut().extend_from_slice(buf); + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } + } + + fn captured(color_support: bool, chunks: &[&[u8]]) -> Vec { + let sink: std::rc::Rc>> = std::rc::Rc::default(); + let mut writer = + maybe_strip_writer(Box::new(SharedSink(std::rc::Rc::clone(&sink))), color_support); + for chunk in chunks { + writer.write_all(chunk).unwrap(); + } + writer.flush().unwrap(); + drop(writer); + sink.take() + } + + #[test] + fn keeps_ansi_when_color_supported() { + let bytes = captured(true, &[b"\x1b[31mred\x1b[0m"]); + assert_eq!(bytes, b"\x1b[31mred\x1b[0m"); + } + + #[test] + fn strips_ansi_in_single_write() { + let bytes = captured(false, &[b"\x1b[31mred\x1b[0m plain"]); + assert_eq!(bytes, b"red plain"); + } + + #[test] + fn strips_ansi_across_write_split_at_csi() { + // `\x1b[` arrives, then the rest of the SGR. + let bytes = captured(false, &[b"hello \x1b[", b"31mWORLD\x1b[0m tail"]); + assert_eq!(bytes, b"hello WORLD tail"); + } + + #[test] + fn strips_ansi_across_write_split_inside_params() { + // Split inside the parameter section of a CSI SGR. + let bytes = captured(false, &[b"\x1b[3", b"8;5;208m", b"orange\x1b[0m"]); + assert_eq!(bytes, b"orange"); + } + + #[test] + fn strips_ansi_across_write_split_byte_by_byte() { + // Worst case: one byte per write. + let escape = b"\x1b[31mhi\x1b[0m"; + let chunks: Vec<&[u8]> = escape.iter().map(std::slice::from_ref).collect(); + let bytes = captured(false, &chunks); + assert_eq!(bytes, b"hi"); + } + + #[test] + fn strips_osc_hyperlink_across_writes() { + // OSC 8 hyperlink sequence ESC ] 8 ; ; URL ESC \ TEXT ESC ] 8 ; ; ESC \ + let bytes = + captured(false, &[b"\x1b]8;;https://example.com\x1b\\", b"link", b"\x1b]8;;\x1b\\"]); + assert_eq!(bytes, b"link"); + } + + #[test] + fn leaves_plain_bytes_alone_when_stripping() { + let bytes = captured(false, &[b"plain text\n", b"another line\n"]); + assert_eq!(bytes, b"plain text\nanother line\n"); + } +} + // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Test fixtures (shared by child module tests) // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ diff --git a/crates/vite_task/src/session/reporter/plain.rs b/crates/vite_task/src/session/reporter/plain.rs index ce7b7abc4..ad34f409a 100644 --- a/crates/vite_task/src/session/reporter/plain.rs +++ b/crates/vite_task/src/session/reporter/plain.rs @@ -6,9 +6,12 @@ use std::io::Write; use super::{ - LeafExecutionReporter, PipeWriters, StdioConfig, StdioSuggestion, format_cache_hit_message, - format_error_message, + ColorSupport, LeafExecutionReporter, PipeWriters, StdioConfig, StdioSuggestion, + format_cache_hit_message, format_error_message, maybe_strip_writer, }; +// `maybe_strip_writer` is used for the child-process pipe writers; reporter +// output decides colour-vs-plain at format time via the `ColorizeExt` helpers +// in [`super`]. use crate::session::event::{CacheStatus, CacheUpdateStatus, ExecutionError}; /// A self-contained [`LeafExecutionReporter`] for single-leaf executions @@ -30,6 +33,11 @@ pub struct PlainReporter { silent_if_cache_hit: bool, /// Whether the current execution is a cache hit, set by `start()`. is_cache_hit: bool, + /// Per-stream colour support — stdout decides stripping of the reporter's + /// own writes and stdout-bound pipe output; stderr decides stripping of + /// the stderr pipe writer (kept independent so a TTY stderr doesn't get + /// stripped just because stdout is redirected). + color_support: ColorSupport, } impl PlainReporter { @@ -37,8 +45,13 @@ impl PlainReporter { /// /// - `silent_if_cache_hit`: If true, suppress all output when the execution is a cache hit. /// - `writer`: Writer for reporter display output. - pub fn new(silent_if_cache_hit: bool, writer: Box) -> Self { - Self { writer, silent_if_cache_hit, is_cache_hit: false } + /// - `color_support`: Per-stream colour-support decision. + pub fn new( + silent_if_cache_hit: bool, + writer: Box, + color_support: ColorSupport, + ) -> Self { + Self { writer, silent_if_cache_hit, is_cache_hit: false, color_support } } /// Returns true if output should be suppressed for this execution. @@ -71,8 +84,14 @@ impl LeafExecutionReporter for PlainReporter { StdioConfig { suggestion: StdioSuggestion::Inherited, writers: PipeWriters { - stdout_writer: Box::new(std::io::stdout()), - stderr_writer: Box::new(std::io::stderr()), + stdout_writer: maybe_strip_writer( + Box::new(std::io::stdout()), + self.color_support.stdout, + ), + stderr_writer: maybe_strip_writer( + Box::new(std::io::stderr()), + self.color_support.stderr, + ), }, } } @@ -109,7 +128,8 @@ mod tests { #[test] fn plain_reporter_always_suggests_inherited() { - let mut reporter = PlainReporter::new(false, Box::new(std::io::sink())); + let mut reporter = + PlainReporter::new(false, Box::new(std::io::sink()), ColorSupport::uniform(false)); let stdio_config = reporter.start(CacheStatus::Disabled(CacheDisabledReason::NoCacheMetadata)); assert_eq!(stdio_config.suggestion, StdioSuggestion::Inherited); @@ -117,7 +137,8 @@ mod tests { #[test] fn plain_reporter_suggests_inherited_even_when_silent() { - let mut reporter = PlainReporter::new(true, Box::new(std::io::sink())); + let mut reporter = + PlainReporter::new(true, Box::new(std::io::sink()), ColorSupport::uniform(false)); let stdio_config = reporter.start(CacheStatus::Disabled(CacheDisabledReason::NoCacheMetadata)); assert_eq!(stdio_config.suggestion, StdioSuggestion::Inherited); diff --git a/crates/vite_task/src/session/reporter/summary_reporter.rs b/crates/vite_task/src/session/reporter/summary_reporter.rs index 06d9f9752..789bc29a2 100644 --- a/crates/vite_task/src/session/reporter/summary_reporter.rs +++ b/crates/vite_task/src/session/reporter/summary_reporter.rs @@ -11,8 +11,8 @@ use vite_str::Str; use vite_task_plan::{ExecutionItemDisplay, LeafExecutionKind}; use super::{ - ExitStatus, GraphExecutionReporter, GraphExecutionReporterBuilder, LeafExecutionReporter, - StdioConfig, + ColorSupport, ExitStatus, GraphExecutionReporter, GraphExecutionReporterBuilder, + LeafExecutionReporter, StdioConfig, }; use crate::session::{ event::{CacheStatus, CacheUpdateStatus, ExecutionError}, @@ -36,6 +36,10 @@ pub struct SummaryReporterBuilder { } impl SummaryReporterBuilder { + /// `writer` is the summary output stream. The wrapped inner builder + /// owns per-stream stripping of the child-process pipe writers; the + /// reporter's own summary text picks colour-vs-plain at format time + /// via `ColorizeExt`, so `writer` is stored unwrapped. pub fn new( inner: Box, workspace_path: Arc, @@ -43,6 +47,7 @@ impl SummaryReporterBuilder { show_details: bool, write_summary: Option, program_name: Str, + _color_support: ColorSupport, ) -> Self { Self { inner, workspace_path, writer, show_details, write_summary, program_name } } diff --git a/crates/vite_task_bin/src/vtt/main.rs b/crates/vite_task_bin/src/vtt/main.rs index 66fbcba0a..d6dcb1af7 100644 --- a/crates/vite_task_bin/src/vtt/main.rs +++ b/crates/vite_task_bin/src/vtt/main.rs @@ -15,6 +15,7 @@ mod list_dir; mod mkdir; mod pipe_stdin; mod print; +mod print_color; mod print_cwd; mod print_env; mod print_file; @@ -29,7 +30,7 @@ fn main() { if args.len() < 2 { eprintln!("Usage: vtt [args...]"); eprintln!( - "Subcommands: barrier, check-tty, cp, exit, exit-on-ctrlc, list-dir, mkdir, pipe-stdin, print, print-cwd, print-env, print-file, read-stdin, replace-file-content, rm, touch-file, write-file" + "Subcommands: barrier, check-tty, cp, exit, exit-on-ctrlc, list-dir, mkdir, pipe-stdin, print, print-color, print-cwd, print-env, print-file, read-stdin, replace-file-content, rm, touch-file, write-file" ); std::process::exit(1); } @@ -50,6 +51,7 @@ fn main() { print::run(&args[2..]); Ok(()) } + "print-color" => print_color::run(&args[2..]), "print-cwd" => print_cwd::run(), "print-env" => print_env::run(&args[2..]), "print-file" => print_file::run(&args[2..]), diff --git a/crates/vite_task_bin/src/vtt/print_color.rs b/crates/vite_task_bin/src/vtt/print_color.rs new file mode 100644 index 000000000..47641247f --- /dev/null +++ b/crates/vite_task_bin/src/vtt/print_color.rs @@ -0,0 +1,34 @@ +//! `vtt print-color ...` — prints `text` wrapped in an ANSI SGR +//! escape sequence when `FORCE_COLOR` is set to a non-zero value, otherwise +//! prints plain text. Used by e2e tests to verify color-env handling. + +pub fn run(args: &[String]) -> Result<(), Box> { + if args.len() < 2 { + return Err("Usage: vtt print-color ...".into()); + } + let color = args[0].as_str(); + let text = args[1..].join(" "); + + let force_color = std::env::var("FORCE_COLOR").ok(); + let want_color = match force_color.as_deref() { + Some("" | "0") | None => false, + Some(_) => true, + }; + + let code: u8 = match color { + "red" => 31, + "green" => 32, + "yellow" => 33, + "blue" => 34, + "magenta" => 35, + "cyan" => 36, + other => return Err(format!("Unknown color: {other}").into()), + }; + + if want_color { + println!("\x1b[{code}m{text}\x1b[0m"); + } else { + println!("{text}"); + } + Ok(()) +} diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/color_env_handling/package.json b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/color_env_handling/package.json new file mode 100644 index 000000000..cf301ba7f --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/color_env_handling/package.json @@ -0,0 +1,4 @@ +{ + "name": "color-env-handling", + "private": true +} diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/color_env_handling/snapshots.toml b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/color_env_handling/snapshots.toml new file mode 100644 index 000000000..bdc3044eb --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/color_env_handling/snapshots.toml @@ -0,0 +1,65 @@ +[[e2e]] +name = "force_color_env_is_always_one_in_cached_child" +comment = """ +The parent shell sets `FORCE_COLOR=0`, but a cached task's child always sees +`FORCE_COLOR=1`. Color-related env vars are not passed through by default for +cached tasks — the runner injects `FORCE_COLOR=1` so cached output is always +colored, and the reporter strips colors at display time when the terminal +does not support them. (For uncached tasks the parent's environment flows +through untouched.) +""" +steps = [ + { argv = [ + "vt", + "run", + "print-force-color", + ], envs = [ + [ + "FORCE_COLOR", + "0", + ], + ], comment = "parent FORCE_COLOR=0, cached child should still see FORCE_COLOR=1" }, +] + +[[e2e]] +name = "cached_output_keeps_colors_across_force_color_values" +comment = """ +A task that emits ANSI escape sequences is run twice. Each cache entry +carries the raw coloured bytes — the runner spawns cached tasks with +`FORCE_COLOR=1` regardless of the parent's value — and the reporter +strips colours at the writer level only when the user's terminal cannot +render them. + +Plain-text snapshot steps use the default `vt100::Screen::contents()` +renderer, which flattens any rendered ANSI styling into plain characters +(so colors don't pollute snapshots). The two `formatted-snapshot = true` +steps switch to `vt100::Screen::rows_formatted` (joined with newlines), +which preserves SGR escapes without emitting cursor positioning or other +rendering state — the latter varies across platforms and would make the +snapshot flaky. Bytes are then routed through `std::ascii::escape_default` +so escapes appear as `\\xNN`. Those two steps prove that the cached +bytes contained colour all along — even when the corresponding initial +run had displayed them in plain text. +""" + +[[e2e.steps]] +argv = ["vt", "run", "print-colored-a"] +envs = [["FORCE_COLOR", "1"]] +comment = "task A — cache miss; parent FORCE_COLOR=1; formatted snapshot proves the child emitted ANSI codes" +formatted-snapshot = true + +[[e2e.steps]] +argv = ["vt", "run", "print-colored-a"] +envs = [["FORCE_COLOR", "0"]] +comment = "task A — cache hit replayed; plain snapshot collapses any colour styling to text" + +[[e2e.steps]] +argv = ["vt", "run", "print-colored-b"] +envs = [["FORCE_COLOR", "0"]] +comment = "task B — cache miss; parent FORCE_COLOR=0, plain snapshot collapses styling. The cache still records the coloured bytes." + +[[e2e.steps]] +argv = ["vt", "run", "print-colored-b"] +envs = [["FORCE_COLOR", "1"]] +comment = "task B — cache hit replayed; formatted snapshot proves the cached bytes were coloured, even though the cache-miss run (above) showed them as plain text" +formatted-snapshot = true diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/color_env_handling/snapshots/cached_output_keeps_colors_across_force_color_values.md b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/color_env_handling/snapshots/cached_output_keeps_colors_across_force_color_values.md new file mode 100644 index 000000000..89a6c718f --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/color_env_handling/snapshots/cached_output_keeps_colors_across_force_color_values.md @@ -0,0 +1,60 @@ +# cached_output_keeps_colors_across_force_color_values + +A task that emits ANSI escape sequences is run twice. Each cache entry +carries the raw coloured bytes — the runner spawns cached tasks with +`FORCE_COLOR=1` regardless of the parent's value — and the reporter +strips colours at the writer level only when the user's terminal cannot +render them. + +Plain-text snapshot steps use the default `vt100::Screen::contents()` +renderer, which flattens any rendered ANSI styling into plain characters +(so colors don't pollute snapshots). The two `formatted-snapshot = true` +steps switch to `vt100::Screen::rows_formatted` (joined with newlines), +which preserves SGR escapes without emitting cursor positioning or other +rendering state — the latter varies across platforms and would make the +snapshot flaky. Bytes are then routed through `std::ascii::escape_default` +so escapes appear as `\xNN`. Those two steps prove that the cached +bytes contained colour all along — even when the corresponding initial +run had displayed them in plain text. + +## `FORCE_COLOR=1 vt run print-colored-a` + +task A — cache miss; parent FORCE_COLOR=1; formatted snapshot proves the child emitted ANSI codes + +``` +\x1b[34m$ vtt print-color red hello-world +\x1b[31mhello-world +``` + +## `FORCE_COLOR=0 vt run print-colored-a` + +task A — cache hit replayed; plain snapshot collapses any colour styling to text + +``` +$ vtt print-color red hello-world ◉ cache hit, replaying +hello-world + +--- +vt run: cache hit. +``` + +## `FORCE_COLOR=0 vt run print-colored-b` + +task B — cache miss; parent FORCE_COLOR=0, plain snapshot collapses styling. The cache still records the coloured bytes. + +``` +$ vtt print-color blue hello-again +hello-again +``` + +## `FORCE_COLOR=1 vt run print-colored-b` + +task B — cache hit replayed; formatted snapshot proves the cached bytes were coloured, even though the cache-miss run (above) showed them as plain text + +``` +\x1b[34m$ vtt print-color blue hello-again \x1b[32m\xe2\x97\x89 \x1b[90mcache hit, replaying +\x1b[34mhello-again + +\x1b[90m--- +\x1b[34;1mvt run: cache hit, \x1b[32;1m saved. +``` diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/color_env_handling/snapshots/force_color_env_is_always_one_in_cached_child.md b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/color_env_handling/snapshots/force_color_env_is_always_one_in_cached_child.md new file mode 100644 index 000000000..2fb658289 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/color_env_handling/snapshots/force_color_env_is_always_one_in_cached_child.md @@ -0,0 +1,17 @@ +# force_color_env_is_always_one_in_cached_child + +The parent shell sets `FORCE_COLOR=0`, but a cached task's child always sees +`FORCE_COLOR=1`. Color-related env vars are not passed through by default for +cached tasks — the runner injects `FORCE_COLOR=1` so cached output is always +colored, and the reporter strips colors at display time when the terminal +does not support them. (For uncached tasks the parent's environment flows +through untouched.) + +## `FORCE_COLOR=0 vt run print-force-color` + +parent FORCE_COLOR=0, cached child should still see FORCE_COLOR=1 + +``` +$ vtt print-env FORCE_COLOR +1 +``` diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/color_env_handling/vite-task.json b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/color_env_handling/vite-task.json new file mode 100644 index 000000000..3ba4b0d82 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/color_env_handling/vite-task.json @@ -0,0 +1,16 @@ +{ + "tasks": { + "print-force-color": { + "command": "vtt print-env FORCE_COLOR", + "cache": true + }, + "print-colored-a": { + "command": "vtt print-color red hello-world", + "cache": true + }, + "print-colored-b": { + "command": "vtt print-color blue hello-again", + "cache": true + } + } +} diff --git a/crates/vite_task_bin/tests/e2e_snapshots/main.rs b/crates/vite_task_bin/tests/e2e_snapshots/main.rs index fb578a496..d222696cd 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/main.rs +++ b/crates/vite_task_bin/tests/e2e_snapshots/main.rs @@ -46,6 +46,11 @@ struct StepConfig { envs: Vec<(Str, Str)>, #[serde(default)] interactions: Vec, + /// When true, render the terminal snapshot with inline ANSI escape codes + /// (made visible as `\e[…m`) so colour/style attributes are part of the + /// assertion. Default `false` keeps the plain-text behaviour. + #[serde(default, rename = "formatted-snapshot")] + formatted_snapshot: bool, } impl Step { @@ -108,6 +113,13 @@ impl Step { Self::Simple(_) => &[], } } + + const fn formatted_snapshot(&self) -> bool { + match self { + Self::Detailed(config) => config.formatted_snapshot, + Self::Simple(_) => false, + } + } } #[derive(serde::Deserialize, Debug, Clone)] @@ -254,6 +266,23 @@ fn resolve_env_placeholder(raw: &str) -> std::borrow::Cow<'_, OsStr> { } } +/// Render the byte stream produced by `screen_contents_formatted` (which uses +/// `vt100::Screen::rows_formatted` — see [`pty_terminal`]) into a +/// snapshot-friendly string. Newlines (added as row separators by the PTY +/// helper) stay literal so the snapshot remains multi-line; SGR escapes and +/// other bytes outside printable ASCII come out as `\xNN`, `\t`, etc. +#[expect(clippy::disallowed_types, reason = "String required for snapshot rendering")] +fn render_formatted_screen(bytes: &[u8]) -> String { + let mut out = String::with_capacity(bytes.len()); + for &b in bytes { + match b { + b'\n' => out.push('\n'), + _ => out.extend(std::ascii::escape_default(b).map(char::from)), + } + } + out +} + /// Append a fenced markdown block containing `body`. The opening and closing /// fences sit on their own lines, and trailing whitespace inside `body` is /// trimmed so the close fence isn't preceded by blank lines. @@ -352,8 +381,14 @@ fn run_case( } cmd.env_clear(); cmd.env("PATH", &e2e_env_path); - cmd.env("NO_COLOR", "1"); - cmd.env("TERM", "dumb"); + // Use `xterm-256color` and report color support so the runner does + // NOT wrap its output writers with [`anstream::StripStream`]. The + // strip layer would otherwise eat OSC8 milestone sequences that + // the test harness relies on to synchronise with the child. ANSI + // escapes emitted by the runner still get flattened to plain text + // by vt100's `Screen::contents()` when the snapshot is rendered, + // so this does not introduce colour-noise into existing snapshots. + cmd.env("TERM", "xterm-256color"); // On Windows, ensure common executable extensions are included in PATHEXT for command resolution in subprocesses. if cfg!(windows) { cmd.env("PATHEXT", ".COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC"); @@ -367,6 +402,7 @@ fn run_case( let terminal = TestTerminal::spawn(SCREEN_SIZE, cmd).unwrap(); let mut killer = terminal.child_handle.clone(); let interactions = step.interactions().to_vec(); + let formatted_snapshot = step.formatted_snapshot(); let output = Arc::new(Mutex::new(String::new())); let output_for_thread = Arc::clone(&output); let (tx, rx) = mpsc::channel(); @@ -421,7 +457,11 @@ fn run_case( } let status = terminal.reader.wait_for_exit().unwrap(); - let screen = terminal.reader.screen_contents(); + let screen = if formatted_snapshot { + render_formatted_screen(&terminal.reader.screen_contents_formatted()) + } else { + terminal.reader.screen_contents() + }; { let mut output = output_for_thread.lock().unwrap(); diff --git a/crates/vite_task_graph/src/config/mod.rs b/crates/vite_task_graph/src/config/mod.rs index 4ab8231bd..4a165dcb8 100644 --- a/crates/vite_task_graph/src/config/mod.rs +++ b/crates/vite_task_graph/src/config/mod.rs @@ -421,12 +421,16 @@ pub const DEFAULT_UNTRACKED_ENV: &[&str] = &[ "DYLD_INSERT_LIBRARIES", "LIBPATH", // Terminal/display - "COLORTERM", - "TERM", - "TERM_PROGRAM", + // + // The only color-related var allowed through by default is `FORCE_COLOR`, + // which the planner pre-injects with value `1` before env resolution so + // cached output is always colored. The reporter strips colors at the + // writer level when the user's terminal cannot render them. Other + // color-related vars (`NO_COLOR`, `COLORTERM`, `TERM`, `TERM_PROGRAM`) + // are intentionally NOT included — users may opt in to passing them + // through via a task's `env`/`untrackedEnv` config. "DISPLAY", "FORCE_COLOR", - "NO_COLOR", // Temporary directories "TMP", "TEMP", diff --git a/crates/vite_task_plan/Cargo.toml b/crates/vite_task_plan/Cargo.toml index f63fec687..31290667d 100644 --- a/crates/vite_task_plan/Cargo.toml +++ b/crates/vite_task_plan/Cargo.toml @@ -22,7 +22,6 @@ rustc-hash = { workspace = true } serde = { workspace = true, features = ["derive"] } sha2 = { workspace = true } shell-escape = { workspace = true } -supports-color = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } vite_glob = { workspace = true } diff --git a/crates/vite_task_plan/src/envs.rs b/crates/vite_task_plan/src/envs.rs index ab6c39efd..12d5fdce4 100644 --- a/crates/vite_task_plan/src/envs.rs +++ b/crates/vite_task_plan/src/envs.rs @@ -3,7 +3,6 @@ use std::{collections::BTreeMap, ffi::OsStr, fmt::Write as _, mem::MaybeUninit, use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; use sha2::{Digest as _, Sha256}; -use supports_color::{Stream, on}; use vite_glob::GlobPatternSet; use vite_str::Str; use vite_task_graph::config::EnvConfig; @@ -76,45 +75,27 @@ impl EnvFingerprints { /// /// Before the call, `all_envs` is expected to contain all available envs. /// After the call, it will be modified to contain only envs to be passed to the execution (fingerprinted + untracked). + /// + /// `FORCE_COLOR` is pre-inserted with value `"1"` so cached output is + /// always colored. Because `FORCE_COLOR` is part of `DEFAULT_UNTRACKED_ENV`, + /// the pattern filter below keeps it; its value (`"1"`) is left untracked + /// (not part of the cache fingerprint). pub fn resolve( all_envs: &mut FxHashMap, Arc>, env_config: &EnvConfig, ) -> Result { - // Collect all envs matching fingerprinted or untracked envs in env_config - *all_envs = { - let mut new_all_envs = resolve_envs_with_patterns( - all_envs.iter(), - &env_config - .untracked_env - .iter() - .map(std::convert::AsRef::as_ref) - .chain(env_config.fingerprinted_envs.iter().map(std::convert::AsRef::as_ref)) - .collect::>(), - )?; + all_envs.insert(OsStr::new("FORCE_COLOR").into(), Arc::::from(OsStr::new("1"))); - // Automatically add FORCE_COLOR environment variable if not already set - // This enables color output in subprocesses when color is supported - // TODO: will remove this temporarily until we have a better solution - if !all_envs.contains_key(OsStr::new("FORCE_COLOR")) - && !all_envs.contains_key(OsStr::new("NO_COLOR")) - && let Some(support) = on(Stream::Stdout) - { - let force_color_value = if support.has_16m { - "3" // True color (16 million colors) - } else if support.has_256 { - "2" // 256 colors - } else if support.has_basic { - "1" // Basic ANSI colors - } else { - "0" // No color support - }; - new_all_envs.insert( - OsStr::new("FORCE_COLOR").into(), - Arc::::from(OsStr::new(force_color_value)), - ); - } - new_all_envs - }; + // Collect all envs matching fingerprinted or untracked envs in env_config + *all_envs = resolve_envs_with_patterns( + all_envs.iter(), + &env_config + .untracked_env + .iter() + .map(std::convert::AsRef::as_ref) + .chain(env_config.fingerprinted_envs.iter().map(std::convert::AsRef::as_ref)) + .collect::>(), + )?; // Resolve fingerprinted envs let mut fingerprinted_envs = BTreeMap::>::new(); @@ -242,50 +223,57 @@ mod tests { } #[test] - fn test_force_color_auto_detection() { - // Test when FORCE_COLOR is not already set - let mut all_envs = create_test_envs(vec![("PATH", "/usr/bin")]); - let env_config = create_env_config(&[], &["PATH"]); - - let result = EnvFingerprints::resolve(&mut all_envs, &env_config).unwrap(); - - // FORCE_COLOR should be automatically added if color is supported - // Note: This test might vary based on the test environment - let force_color_present = all_envs.contains_key(OsStr::new("FORCE_COLOR")); - if force_color_present { - let force_color_value = all_envs.get(OsStr::new("FORCE_COLOR")).unwrap(); - let force_color_str = force_color_value.to_str().unwrap(); - // Should be a valid FORCE_COLOR level - assert!(matches!(force_color_str, "0" | "1" | "2" | "3")); - } - - // Test when FORCE_COLOR is already set - should not be overridden + fn test_force_color_always_set_to_one() { + // `FORCE_COLOR=1` is pre-injected before pattern filtering so cached + // output is always colored. Because the merged untracked-env list + // (config resolution adds DEFAULT_UNTRACKED_ENV, which includes + // `FORCE_COLOR`) keeps it, the child sees `FORCE_COLOR=1` regardless + // of the parent's value. let mut all_envs = create_test_envs(vec![("PATH", "/usr/bin"), ("FORCE_COLOR", "2")]); let env_config = create_env_config(&[], &["PATH", "FORCE_COLOR"]); let _result = EnvFingerprints::resolve(&mut all_envs, &env_config).unwrap(); - // Should contain the original FORCE_COLOR value - assert!(all_envs.contains_key(OsStr::new("FORCE_COLOR"))); - let force_color_value = all_envs.get(OsStr::new("FORCE_COLOR")).unwrap(); - assert_eq!(force_color_value.to_str().unwrap(), "2"); - - // FORCE_COLOR should not be in fingerprinted_envs since it's not declared - assert!(!result.fingerprinted_envs.contains_key("FORCE_COLOR")); + let force_color_value = all_envs + .get(OsStr::new("FORCE_COLOR")) + .expect("FORCE_COLOR should be present after resolution"); + assert_eq!(force_color_value.to_str().unwrap(), "1"); + } - // Test when NO_COLOR is already set - FORCE_COLOR should not be automatically added - let mut all_envs = create_test_envs(vec![("PATH", "/usr/bin"), ("NO_COLOR", "1")]); - let env_config = create_env_config(&[], &["PATH", "NO_COLOR"]); + #[test] + fn test_force_color_dropped_when_pattern_does_not_allow_it() { + // The resolver itself only pre-injects; it does not force-keep + // `FORCE_COLOR` through the filter. Real callers always provide + // patterns that include `FORCE_COLOR` (via `DEFAULT_UNTRACKED_ENV`), + // but this test pins the contract: if `FORCE_COLOR` is absent from + // the merged pattern list, the filter drops it. + let mut all_envs = create_test_envs(vec![("PATH", "/usr/bin")]); + let env_config = create_env_config(&[], &["PATH"]); let _result = EnvFingerprints::resolve(&mut all_envs, &env_config).unwrap(); - assert!(all_envs.contains_key(OsStr::new("NO_COLOR"))); - let no_color_value = all_envs.get(OsStr::new("NO_COLOR")).unwrap(); - assert_eq!(no_color_value.to_str().unwrap(), "1"); - // FORCE_COLOR should not be automatically added since NO_COLOR is set assert!(!all_envs.contains_key(OsStr::new("FORCE_COLOR"))); } + #[test] + fn test_force_color_value_one_overrides_user_fingerprinted_value() { + // A user can list `FORCE_COLOR` as a fingerprinted env, but the + // pre-injection still wins — fingerprint records `"1"`, not the + // parent's value. (`FORCE_COLOR` is the colour-pipeline contract; + // users wanting a different colour level should configure the tool + // they're running, not the runner.) + let mut all_envs = create_test_envs(vec![("FORCE_COLOR", "3")]); + let env_config = create_env_config(&["FORCE_COLOR"], &[]); + + let result = EnvFingerprints::resolve(&mut all_envs, &env_config).unwrap(); + + assert_eq!(all_envs.get(OsStr::new("FORCE_COLOR")).unwrap().to_str().unwrap(), "1"); + assert_eq!( + result.fingerprinted_envs.get("FORCE_COLOR").map(std::convert::AsRef::as_ref), + Some("1") + ); + } + #[test] #[cfg(unix)] fn test_task_envs_stable_ordering() { diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/additional_env/snapshots/query_tool_synthetic_task_in_user_task.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/additional_env/snapshots/query_tool_synthetic_task_in_user_task.jsonc index 63bad854a..463724f0c 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/additional_env/snapshots/query_tool_synthetic_task_in_user_task.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/additional_env/snapshots/query_tool_synthetic_task_in_user_task.jsonc @@ -74,7 +74,7 @@ "TEST_VAR" ], "all_envs": { - "NO_COLOR": "1", + "FORCE_COLOR": "1", "PATH": "/node_modules/.bin:", "TEST_VAR": "hello_world" }, diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_cli_override/snapshots/query___cache_enables_script_caching.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_cli_override/snapshots/query___cache_enables_script_caching.jsonc index 63272b8d8..a5691b143 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_cli_override/snapshots/query___cache_enables_script_caching.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_cli_override/snapshots/query___cache_enables_script_caching.jsonc @@ -72,7 +72,7 @@ "package.json" ], "all_envs": { - "NO_COLOR": "1", + "FORCE_COLOR": "1", "PATH": "/node_modules/.bin:" }, "cwd": "/" diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_cli_override/snapshots/query___cache_enables_task_caching_even_when_cache_tasks_is_false.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_cli_override/snapshots/query___cache_enables_task_caching_even_when_cache_tasks_is_false.jsonc index af84a0ef1..30c866edb 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_cli_override/snapshots/query___cache_enables_task_caching_even_when_cache_tasks_is_false.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_cli_override/snapshots/query___cache_enables_task_caching_even_when_cache_tasks_is_false.jsonc @@ -72,7 +72,7 @@ "package.json" ], "all_envs": { - "NO_COLOR": "1", + "FORCE_COLOR": "1", "PATH": "/node_modules/.bin:" }, "cwd": "/" diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_cli_override/snapshots/query___cache_on_task_with_per_task_cache_true_enables_caching.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_cli_override/snapshots/query___cache_on_task_with_per_task_cache_true_enables_caching.jsonc index 44c82a701..47e5b0324 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_cli_override/snapshots/query___cache_on_task_with_per_task_cache_true_enables_caching.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_cli_override/snapshots/query___cache_on_task_with_per_task_cache_true_enables_caching.jsonc @@ -72,7 +72,7 @@ "package.json" ], "all_envs": { - "NO_COLOR": "1", + "FORCE_COLOR": "1", "PATH": "/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.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_echo_and_lint_with_extra_args.jsonc index f589579d4..48ad4b955 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_echo_and_lint_with_extra_args.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_echo_and_lint_with_extra_args.jsonc @@ -102,7 +102,7 @@ "--fix" ], "all_envs": { - "NO_COLOR": "1", + "FORCE_COLOR": "1", "PATH": "/node_modules/.bin:" }, "cwd": "/" diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_lint_and_echo_with_extra_args.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_lint_and_echo_with_extra_args.jsonc index 586844bfc..4ba219e17 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_lint_and_echo_with_extra_args.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_lint_and_echo_with_extra_args.jsonc @@ -72,7 +72,7 @@ "lint" ], "all_envs": { - "NO_COLOR": "1", + "FORCE_COLOR": "1", "PATH": "/node_modules/.bin:" }, "cwd": "/" diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_normal_task_with_extra_args.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_normal_task_with_extra_args.jsonc index 07660f771..c838f9435 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_normal_task_with_extra_args.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_normal_task_with_extra_args.jsonc @@ -74,7 +74,7 @@ "a.txt" ], "all_envs": { - "NO_COLOR": "1", + "FORCE_COLOR": "1", "PATH": "/node_modules/.bin:" }, "cwd": "/" diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_synthetic_task_in_user_task.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_synthetic_task_in_user_task.jsonc index a75b9dfc1..4a8e20b3b 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_synthetic_task_in_user_task.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_synthetic_task_in_user_task.jsonc @@ -72,7 +72,7 @@ "lint" ], "all_envs": { - "NO_COLOR": "1", + "FORCE_COLOR": "1", "PATH": "/node_modules/.bin:" }, "cwd": "/" diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_synthetic_task_in_user_task_with_cwd.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_synthetic_task_in_user_task_with_cwd.jsonc index a75b9dfc1..4a8e20b3b 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_synthetic_task_in_user_task_with_cwd.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_synthetic_task_in_user_task_with_cwd.jsonc @@ -72,7 +72,7 @@ "lint" ], "all_envs": { - "NO_COLOR": "1", + "FORCE_COLOR": "1", "PATH": "/node_modules/.bin:" }, "cwd": "/" diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_synthetic_task_with_extra_args_in_user_task.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_synthetic_task_with_extra_args_in_user_task.jsonc index 71683ee96..def52132d 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_synthetic_task_with_extra_args_in_user_task.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_keys/snapshots/query_synthetic_task_with_extra_args_in_user_task.jsonc @@ -76,7 +76,7 @@ "--fix" ], "all_envs": { - "NO_COLOR": "1", + "FORCE_COLOR": "1", "PATH": "/node_modules/.bin:" }, "cwd": "/" diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_scripts_task_override/snapshots/query_another_task_cached_by_default.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_scripts_task_override/snapshots/query_another_task_cached_by_default.jsonc index b344b082b..ae61543a1 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_scripts_task_override/snapshots/query_another_task_cached_by_default.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_scripts_task_override/snapshots/query_another_task_cached_by_default.jsonc @@ -72,7 +72,7 @@ "package.json" ], "all_envs": { - "NO_COLOR": "1", + "FORCE_COLOR": "1", "PATH": "/node_modules/.bin:" }, "cwd": "/" diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_scripts_task_override/snapshots/query_task_cached_by_default.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_scripts_task_override/snapshots/query_task_cached_by_default.jsonc index 78a406fa1..fd3f1f354 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_scripts_task_override/snapshots/query_task_cached_by_default.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_scripts_task_override/snapshots/query_task_cached_by_default.jsonc @@ -72,7 +72,7 @@ "package.json" ], "all_envs": { - "NO_COLOR": "1", + "FORCE_COLOR": "1", "PATH": "/node_modules/.bin:" }, "cwd": "/" diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_true_no_force_enable/snapshots/query_script_cached_when_global_cache_true.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_true_no_force_enable/snapshots/query_script_cached_when_global_cache_true.jsonc index a82d5e2f9..0e38dd63c 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_true_no_force_enable/snapshots/query_script_cached_when_global_cache_true.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_true_no_force_enable/snapshots/query_script_cached_when_global_cache_true.jsonc @@ -72,7 +72,7 @@ "package.json" ], "all_envs": { - "NO_COLOR": "1", + "FORCE_COLOR": "1", "PATH": "/node_modules/.bin:" }, "cwd": "/" diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_true_no_force_enable/snapshots/query_task_cached_when_global_cache_true.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_true_no_force_enable/snapshots/query_task_cached_when_global_cache_true.jsonc index a16181471..88f1e516f 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_true_no_force_enable/snapshots/query_task_cached_when_global_cache_true.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cache_true_no_force_enable/snapshots/query_task_cached_when_global_cache_true.jsonc @@ -72,7 +72,7 @@ "package.json" ], "all_envs": { - "NO_COLOR": "1", + "FORCE_COLOR": "1", "PATH": "/node_modules/.bin:" }, "cwd": "/" diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cd_in_scripts/snapshots/query_cd_before_vt_lint_should_put_synthetic_task_under_cwd.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cd_in_scripts/snapshots/query_cd_before_vt_lint_should_put_synthetic_task_under_cwd.jsonc index d226a413b..93e467496 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cd_in_scripts/snapshots/query_cd_before_vt_lint_should_put_synthetic_task_under_cwd.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cd_in_scripts/snapshots/query_cd_before_vt_lint_should_put_synthetic_task_under_cwd.jsonc @@ -72,7 +72,7 @@ "lint" ], "all_envs": { - "NO_COLOR": "1", + "FORCE_COLOR": "1", "PATH": "/node_modules/.bin:" }, "cwd": "/src" diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cd_in_scripts/snapshots/query_cd_before_vt_run_should_not_affect_expanded_task_cwd.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cd_in_scripts/snapshots/query_cd_before_vt_run_should_not_affect_expanded_task_cwd.jsonc index 4142b0960..838e481e0 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/cd_in_scripts/snapshots/query_cd_before_vt_run_should_not_affect_expanded_task_cwd.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/cd_in_scripts/snapshots/query_cd_before_vt_run_should_not_affect_expanded_task_cwd.jsonc @@ -98,7 +98,7 @@ "package.json" ], "all_envs": { - "NO_COLOR": "1", + "FORCE_COLOR": "1", "PATH": "/node_modules/.bin:" }, "cwd": "/" diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/extra_args_not_forwarded_to_depends_on/snapshots/query_extra_args_only_reach_requested_task.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/extra_args_not_forwarded_to_depends_on/snapshots/query_extra_args_only_reach_requested_task.jsonc index da9501116..65177af19 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/extra_args_not_forwarded_to_depends_on/snapshots/query_extra_args_only_reach_requested_task.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/extra_args_not_forwarded_to_depends_on/snapshots/query_extra_args_only_reach_requested_task.jsonc @@ -72,7 +72,7 @@ "build" ], "all_envs": { - "NO_COLOR": "1", + "FORCE_COLOR": "1", "PATH": "/node_modules/.bin:" }, "cwd": "/" @@ -160,7 +160,7 @@ "some-filter" ], "all_envs": { - "NO_COLOR": "1", + "FORCE_COLOR": "1", "PATH": "/node_modules/.bin:" }, "cwd": "/" diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/nested_cache_override/snapshots/query_nested___cache_enables_inner_task_caching.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/nested_cache_override/snapshots/query_nested___cache_enables_inner_task_caching.jsonc index 759b8f84e..9b2bd1fc8 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/nested_cache_override/snapshots/query_nested___cache_enables_inner_task_caching.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/nested_cache_override/snapshots/query_nested___cache_enables_inner_task_caching.jsonc @@ -98,7 +98,7 @@ "package.json" ], "all_envs": { - "NO_COLOR": "1", + "FORCE_COLOR": "1", "PATH": "/node_modules/.bin:" }, "cwd": "/" diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/nested_cache_override/snapshots/query_outer___cache_propagates_to_nested_run_without_flags.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/nested_cache_override/snapshots/query_outer___cache_propagates_to_nested_run_without_flags.jsonc index a4f58176f..c069f63c4 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/nested_cache_override/snapshots/query_outer___cache_propagates_to_nested_run_without_flags.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/nested_cache_override/snapshots/query_outer___cache_propagates_to_nested_run_without_flags.jsonc @@ -98,7 +98,7 @@ "package.json" ], "all_envs": { - "NO_COLOR": "1", + "FORCE_COLOR": "1", "PATH": "/node_modules/.bin:" }, "cwd": "/" diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/nested_cache_override/snapshots/query_outer___no_cache_does_not_propagate_into_nested___cache.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/nested_cache_override/snapshots/query_outer___no_cache_does_not_propagate_into_nested___cache.jsonc index 6e384f70a..8d6f1a58a 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/nested_cache_override/snapshots/query_outer___no_cache_does_not_propagate_into_nested___cache.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/nested_cache_override/snapshots/query_outer___no_cache_does_not_propagate_into_nested___cache.jsonc @@ -98,7 +98,7 @@ "package.json" ], "all_envs": { - "NO_COLOR": "1", + "FORCE_COLOR": "1", "PATH": "/node_modules/.bin:" }, "cwd": "/" diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/shell_fallback/snapshots/query_shell_fallback_for_pipe_command.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/shell_fallback/snapshots/query_shell_fallback_for_pipe_command.jsonc index b3da6fda6..37f7a9b0f 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/shell_fallback/snapshots/query_shell_fallback_for_pipe_command.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/shell_fallback/snapshots/query_shell_fallback_for_pipe_command.jsonc @@ -72,7 +72,7 @@ "echo hello | node -e \"process.stdin.pipe(process.stdout)\"" ], "all_envs": { - "NO_COLOR": "1", + "FORCE_COLOR": "1", "PATH": "/node_modules/.bin:" }, "cwd": "/" diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_cache_disabled/snapshots/query_parent_cache_false_does_not_affect_expanded_query_tasks.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_cache_disabled/snapshots/query_parent_cache_false_does_not_affect_expanded_query_tasks.jsonc index 2df26b1a1..e5fab7d13 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_cache_disabled/snapshots/query_parent_cache_false_does_not_affect_expanded_query_tasks.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_cache_disabled/snapshots/query_parent_cache_false_does_not_affect_expanded_query_tasks.jsonc @@ -98,7 +98,7 @@ "lint" ], "all_envs": { - "NO_COLOR": "1", + "FORCE_COLOR": "1", "PATH": "/node_modules/.bin:" }, "cwd": "/" diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_cache_disabled/snapshots/query_script_cache_false_does_not_affect_expanded_synthetic_cache.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_cache_disabled/snapshots/query_script_cache_false_does_not_affect_expanded_synthetic_cache.jsonc index d5e183324..847792c4d 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_cache_disabled/snapshots/query_script_cache_false_does_not_affect_expanded_synthetic_cache.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_cache_disabled/snapshots/query_script_cache_false_does_not_affect_expanded_synthetic_cache.jsonc @@ -98,7 +98,7 @@ "lint" ], "all_envs": { - "NO_COLOR": "1", + "FORCE_COLOR": "1", "PATH": "/node_modules/.bin:" }, "cwd": "/" diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_cache_disabled/snapshots/query_task_untrackedEnv_inherited_by_synthetic.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_cache_disabled/snapshots/query_task_untrackedEnv_inherited_by_synthetic.jsonc index 812734076..24c7eb377 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_cache_disabled/snapshots/query_task_untrackedEnv_inherited_by_synthetic.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_cache_disabled/snapshots/query_task_untrackedEnv_inherited_by_synthetic.jsonc @@ -73,7 +73,7 @@ "lint" ], "all_envs": { - "NO_COLOR": "1", + "FORCE_COLOR": "1", "PATH": "/node_modules/.bin:" }, "cwd": "/" diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_cache_disabled/snapshots/query_task_with_cache_true_enables_synthetic_cache.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_cache_disabled/snapshots/query_task_with_cache_true_enables_synthetic_cache.jsonc index 9ccca7a11..a96337e79 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_cache_disabled/snapshots/query_task_with_cache_true_enables_synthetic_cache.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_cache_disabled/snapshots/query_task_with_cache_true_enables_synthetic_cache.jsonc @@ -72,7 +72,7 @@ "lint" ], "all_envs": { - "NO_COLOR": "1", + "FORCE_COLOR": "1", "PATH": "/node_modules/.bin:" }, "cwd": "/" diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_in_subpackage/snapshots/query_synthetic_in_subpackage.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_in_subpackage/snapshots/query_synthetic_in_subpackage.jsonc index 5a40fe048..5e47fdc2c 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_in_subpackage/snapshots/query_synthetic_in_subpackage.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/synthetic_in_subpackage/snapshots/query_synthetic_in_subpackage.jsonc @@ -98,7 +98,7 @@ "lint" ], "all_envs": { - "NO_COLOR": "1", + "FORCE_COLOR": "1", "PATH": "/packages/a/node_modules/.bin:/node_modules/.bin:" }, "cwd": "/packages/a" diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/windows_cmd_shim_rewrite/snapshots/query_dev_filter_from_root.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/windows_cmd_shim_rewrite/snapshots/query_dev_filter_from_root.jsonc index ba1f67e9e..175ac4e20 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/windows_cmd_shim_rewrite/snapshots/query_dev_filter_from_root.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/windows_cmd_shim_rewrite/snapshots/query_dev_filter_from_root.jsonc @@ -84,7 +84,7 @@ "3000" ], "all_envs": { - "NO_COLOR": "1", + "FORCE_COLOR": "1", "PATH": "/packages/foo/node_modules/.bin:/node_modules/.bin:" }, "cwd": "/packages/foo" diff --git a/crates/vite_task_plan/tests/plan_snapshots/fixtures/windows_cmd_shim_rewrite/snapshots/query_dev_in_subpackage.jsonc b/crates/vite_task_plan/tests/plan_snapshots/fixtures/windows_cmd_shim_rewrite/snapshots/query_dev_in_subpackage.jsonc index dc5f9a939..2ce0827c1 100644 --- a/crates/vite_task_plan/tests/plan_snapshots/fixtures/windows_cmd_shim_rewrite/snapshots/query_dev_in_subpackage.jsonc +++ b/crates/vite_task_plan/tests/plan_snapshots/fixtures/windows_cmd_shim_rewrite/snapshots/query_dev_in_subpackage.jsonc @@ -84,7 +84,7 @@ "3000" ], "all_envs": { - "NO_COLOR": "1", + "FORCE_COLOR": "1", "PATH": "/packages/foo/node_modules/.bin:/node_modules/.bin:" }, "cwd": "/packages/foo"