From 9dd5fc1a11d75ab36d86883a62d74478c938afe2 Mon Sep 17 00:00:00 2001 From: Paul Kim Date: Sun, 19 Apr 2026 14:48:48 +0200 Subject: [PATCH 1/4] Rewatch: show warnings in completion output --- rewatch/src/build.rs | 91 ++++++++++++++++++++++++++++++++++++------ rewatch/src/helpers.rs | 14 +++---- rewatch/src/watcher.rs | 48 +++++++++++----------- 3 files changed, 108 insertions(+), 45 deletions(-) diff --git a/rewatch/src/build.rs b/rewatch/src/build.rs index a566c76a63e..2ce72a06b08 100644 --- a/rewatch/src/build.rs +++ b/rewatch/src/build.rs @@ -56,6 +56,40 @@ pub struct CompilerArgs { pub parser_args: Vec, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct IncrementalBuildResult { + pub has_warnings: bool, +} + +fn has_output(output: &str) -> bool { + helpers::contains_ascii_characters(output) +} + +fn has_config_warnings(build_state: &BuildCommandState) -> bool { + build_state.packages.iter().any(|(_, package)| { + package.is_local_dep + && (!package.config.get_unsupported_fields().is_empty() + || !package.config.get_unknown_fields().is_empty()) + }) +} + +pub fn format_finished_compilation_message( + compilation_kind: Option<&str>, + has_warnings: bool, + duration: Duration, +) -> String { + let compilation_kind = compilation_kind + .map(|kind| format!("{kind} ")) + .unwrap_or_default(); + let status = if has_warnings { WARNING } else { CHECKMARK }; + let warning_suffix = if has_warnings { " with warnings" } else { "" }; + + format!( + "{LINE_CLEAR}{status}Finished {compilation_kind}compilation{warning_suffix} in {:.2}s", + duration.as_secs_f64() + ) +} + pub fn get_compiler_args(rescript_file_path: &Path) -> Result { let filename = &helpers::get_abs_path(rescript_file_path); let current_package = helpers::get_abs_path( @@ -246,7 +280,7 @@ pub fn incremental_build( only_incremental: bool, create_sourcedirs: bool, plain_output: bool, -) -> Result<(), IncrementalBuildError> { +) -> Result { let build_folder = build_state.root_folder.to_string_lossy().to_string(); let _lock = get_lock_or_exit(LockKind::Build, &build_folder); @@ -264,7 +298,7 @@ pub fn incremental_build( ProgressStyle::with_template(&format!( "{} {}Parsing... {{spinner}} {{pos}}/{{len}} {{msg}}", format_step(current_step, total_steps), - CODE + PARSE )) .unwrap(), ); @@ -314,13 +348,14 @@ pub fn incremental_build( "{}{} {}Parsed {} source files in {:.2}s", LINE_CLEAR, format_step(current_step, total_steps), - CODE, + PARSE, num_dirty_modules, default_timing.unwrap_or(timing_parse_total).as_secs_f64() ); } } - if helpers::contains_ascii_characters(&parse_warnings) { + let has_parse_warnings = has_output(&parse_warnings); + if has_parse_warnings { eprintln!("{}", &parse_warnings); } @@ -389,13 +424,13 @@ pub fn incremental_build( ); } } - if helpers::contains_ascii_characters(&compile_warnings) { + if has_output(&compile_warnings) { eprintln!("{}", &compile_warnings); } if initial_build { log_config_warnings(build_state); } - if helpers::contains_ascii_characters(&compile_errors) { + if has_output(&compile_errors) { eprintln!("{}", &compile_errors); } @@ -406,6 +441,10 @@ pub fn incremental_build( plain_output, }) } else { + let has_compile_warnings = has_output(&compile_warnings); + let has_config_warning_output = initial_build && has_config_warnings(build_state); + let has_warnings = has_parse_warnings || has_compile_warnings || has_config_warning_output; + if show_progress { if plain_output { println!("Compiled {num_compiled_modules} modules") @@ -421,7 +460,7 @@ pub fn incremental_build( } } - if helpers::contains_ascii_characters(&compile_warnings) { + if has_compile_warnings { eprintln!("{}", &compile_warnings); } if initial_build { @@ -432,7 +471,7 @@ pub fn incremental_build( write_compiler_info(build_state); let _lock = drop_lock(LockKind::Build, &build_folder); - Ok(()) + Ok(IncrementalBuildResult { has_warnings }) } } @@ -519,14 +558,16 @@ pub fn build( create_sourcedirs, plain_output, ) { - Ok(_) => { + Ok(result) => { if !plain_output && show_progress { let timing_total_elapsed = timing_total.elapsed(); println!( - "\n{}{}Finished Compilation in {:.2}s", - LINE_CLEAR, - SPARKLES, - default_timing.unwrap_or(timing_total_elapsed).as_secs_f64() + "\n{}", + format_finished_compilation_message( + None, + result.has_warnings, + default_timing.unwrap_or(timing_total_elapsed), + ) ); } clean::cleanup_after_build(&build_state); @@ -540,3 +581,27 @@ pub fn build( } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn formats_successful_completion_message() { + assert_eq!( + format_finished_compilation_message(None, false, Duration::from_millis(1500)), + format!("{LINE_CLEAR}{}Finished compilation in 1.50s", CHECKMARK) + ); + } + + #[test] + fn formats_warning_completion_message() { + assert_eq!( + format_finished_compilation_message(Some("incremental"), true, Duration::from_millis(1500)), + format!( + "{LINE_CLEAR}{}Finished incremental compilation with warnings in 1.50s", + WARNING + ) + ); + } +} diff --git a/rewatch/src/helpers.rs b/rewatch/src/helpers.rs index 600dd34ab69..46fa469629c 100644 --- a/rewatch/src/helpers.rs +++ b/rewatch/src/helpers.rs @@ -18,13 +18,13 @@ pub mod deserialize; pub mod emojis { use console::Emoji; - pub static COMMAND: Emoji<'_, '_> = Emoji("๐Ÿƒ ", ""); - pub static SWEEP: Emoji<'_, '_> = Emoji("๐Ÿงน ", ""); - pub static CODE: Emoji<'_, '_> = Emoji("๐Ÿงฑ ", ""); - pub static SWORDS: Emoji<'_, '_> = Emoji("๐Ÿคบ ", ""); - pub static CHECKMARK: Emoji<'_, '_> = Emoji("โœ… ", ""); - pub static CROSS: Emoji<'_, '_> = Emoji("โŒ ", ""); - pub static SPARKLES: Emoji<'_, '_> = Emoji("โœจ ", ""); + pub static COMMAND: Emoji<'_, '_> = Emoji("๐Ÿƒ ", "[run] "); + pub static SWEEP: Emoji<'_, '_> = Emoji("๐Ÿงน ", "[clean] "); + pub static PARSE: Emoji<'_, '_> = Emoji("๐Ÿงฑ ", "[parse] "); + pub static SWORDS: Emoji<'_, '_> = Emoji("๐Ÿคบ ", "[build] "); + pub static CHECKMARK: Emoji<'_, '_> = Emoji("โœ… ", "[ok] "); + pub static WARNING: Emoji<'_, '_> = Emoji("โš ๏ธ ", "[warn] "); + pub static CROSS: Emoji<'_, '_> = Emoji("โŒ ", "[error] "); pub static LINE_CLEAR: &str = "\x1b[2K\r"; } diff --git a/rewatch/src/watcher.rs b/rewatch/src/watcher.rs index 2c9ae73ecd6..17225046860 100644 --- a/rewatch/src/watcher.rs +++ b/rewatch/src/watcher.rs @@ -5,7 +5,6 @@ use crate::cmd; use crate::config; use crate::helpers; use crate::helpers::StrippedVerbatimPath; -use crate::helpers::emojis::*; use crate::lock::LockKind; use crate::queue::FifoQueue; use crate::queue::*; @@ -379,7 +378,7 @@ async fn async_watch( match needs_compile_type { CompileType::Incremental => { let timing_total = Instant::now(); - if build::incremental_build( + if let Ok(result) = build::incremental_build( &mut build_state, None, initial_build, @@ -387,9 +386,7 @@ async fn async_watch( !initial_build, create_sourcedirs, plain_output, - ) - .is_ok() - { + ) { if let Some(a) = after_build.clone() { cmd::run(a) } @@ -400,11 +397,12 @@ async fn async_watch( println!("Finished {compilation_type} compilation") } else { println!( - "\n{}{}Finished {} compilation in {:.2}s\n", - LINE_CLEAR, - SPARKLES, - compilation_type, - timing_total_elapsed.as_secs_f64() + "\n{}\n", + build::format_finished_compilation_message( + Some(compilation_type), + result.has_warnings, + timing_total_elapsed, + ) ); } } @@ -436,7 +434,7 @@ async fn async_watch( current_watch_paths = compute_watch_paths(&build_state, path); register_watches(watcher, ¤t_watch_paths); - let _ = build::incremental_build( + let result = build::incremental_build( &mut build_state, None, initial_build, @@ -445,25 +443,25 @@ async fn async_watch( create_sourcedirs, plain_output, ); - if let Some(a) = after_build.clone() { - cmd::run(a) - } - - build::write_build_ninja(&build_state); + if let Ok(result) = result { + if let Some(a) = after_build.clone() { + cmd::run(a) + } - let timing_total_elapsed = timing_total.elapsed(); - if show_progress { - if plain_output { - println!("Finished compilation") - } else { + let timing_total_elapsed = timing_total.elapsed(); + if !plain_output && show_progress { println!( - "\n{}{}Finished compilation in {:.2}s\n", - LINE_CLEAR, - SPARKLES, - timing_total_elapsed.as_secs_f64() + "\n{}\n", + build::format_finished_compilation_message( + None, + result.has_warnings, + timing_total_elapsed, + ) ); } } + + build::write_build_ninja(&build_state); needs_compile_type = CompileType::None; initial_build = false; } From 21ebac9865a33467067d3b8742a4cb99043233ad Mon Sep 17 00:00:00 2001 From: Paul Kim Date: Sun, 19 Apr 2026 15:03:10 +0200 Subject: [PATCH 2/4] Rewatch: use CompilationOutcome enum --- rewatch/src/build.rs | 33 ++++++++++++++++++++++----------- rewatch/src/watcher.rs | 8 ++------ 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/rewatch/src/build.rs b/rewatch/src/build.rs index 2ce72a06b08..972d9555406 100644 --- a/rewatch/src/build.rs +++ b/rewatch/src/build.rs @@ -57,8 +57,9 @@ pub struct CompilerArgs { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct IncrementalBuildResult { - pub has_warnings: bool, +pub enum CompilationOutcome { + Clean, + Warnings, } fn has_output(output: &str) -> bool { @@ -75,14 +76,16 @@ fn has_config_warnings(build_state: &BuildCommandState) -> bool { pub fn format_finished_compilation_message( compilation_kind: Option<&str>, - has_warnings: bool, + outcome: CompilationOutcome, duration: Duration, ) -> String { let compilation_kind = compilation_kind .map(|kind| format!("{kind} ")) .unwrap_or_default(); - let status = if has_warnings { WARNING } else { CHECKMARK }; - let warning_suffix = if has_warnings { " with warnings" } else { "" }; + let (status, warning_suffix) = match outcome { + CompilationOutcome::Clean => (CHECKMARK, ""), + CompilationOutcome::Warnings => (WARNING, " with warnings"), + }; format!( "{LINE_CLEAR}{status}Finished {compilation_kind}compilation{warning_suffix} in {:.2}s", @@ -280,7 +283,7 @@ pub fn incremental_build( only_incremental: bool, create_sourcedirs: bool, plain_output: bool, -) -> Result { +) -> Result { let build_folder = build_state.root_folder.to_string_lossy().to_string(); let _lock = get_lock_or_exit(LockKind::Build, &build_folder); @@ -443,7 +446,11 @@ pub fn incremental_build( } else { let has_compile_warnings = has_output(&compile_warnings); let has_config_warning_output = initial_build && has_config_warnings(build_state); - let has_warnings = has_parse_warnings || has_compile_warnings || has_config_warning_output; + let outcome = if has_parse_warnings || has_compile_warnings || has_config_warning_output { + CompilationOutcome::Warnings + } else { + CompilationOutcome::Clean + }; if show_progress { if plain_output { @@ -471,7 +478,7 @@ pub fn incremental_build( write_compiler_info(build_state); let _lock = drop_lock(LockKind::Build, &build_folder); - Ok(IncrementalBuildResult { has_warnings }) + Ok(outcome) } } @@ -565,7 +572,7 @@ pub fn build( "\n{}", format_finished_compilation_message( None, - result.has_warnings, + result, default_timing.unwrap_or(timing_total_elapsed), ) ); @@ -589,7 +596,7 @@ mod tests { #[test] fn formats_successful_completion_message() { assert_eq!( - format_finished_compilation_message(None, false, Duration::from_millis(1500)), + format_finished_compilation_message(None, CompilationOutcome::Clean, Duration::from_millis(1500),), format!("{LINE_CLEAR}{}Finished compilation in 1.50s", CHECKMARK) ); } @@ -597,7 +604,11 @@ mod tests { #[test] fn formats_warning_completion_message() { assert_eq!( - format_finished_compilation_message(Some("incremental"), true, Duration::from_millis(1500)), + format_finished_compilation_message( + Some("incremental"), + CompilationOutcome::Warnings, + Duration::from_millis(1500), + ), format!( "{LINE_CLEAR}{}Finished incremental compilation with warnings in 1.50s", WARNING diff --git a/rewatch/src/watcher.rs b/rewatch/src/watcher.rs index 17225046860..6812f4b658e 100644 --- a/rewatch/src/watcher.rs +++ b/rewatch/src/watcher.rs @@ -400,7 +400,7 @@ async fn async_watch( "\n{}\n", build::format_finished_compilation_message( Some(compilation_type), - result.has_warnings, + result, timing_total_elapsed, ) ); @@ -452,11 +452,7 @@ async fn async_watch( if !plain_output && show_progress { println!( "\n{}\n", - build::format_finished_compilation_message( - None, - result.has_warnings, - timing_total_elapsed, - ) + build::format_finished_compilation_message(None, result, timing_total_elapsed,) ); } } From 77563886541187ce2710679813171e2ad894e6d4 Mon Sep 17 00:00:00 2001 From: Paul Kim Date: Sun, 19 Apr 2026 15:25:29 +0200 Subject: [PATCH 3/4] feat(rewatch): add watch --clear-screen Make screen clearing opt-in so scripted stdout stays stable while interactive watch mode can drop stale warnings and errors between rebuilds. Also print explicit rebuild and failure markers so fixed vs broken state is easier to spot. Refs #8139 Signed-off-by: Paul Kim --- rewatch/src/cli.rs | 15 +++++ rewatch/src/main.rs | 1 + rewatch/src/watcher.rs | 126 ++++++++++++++++++++++++++++++++--------- 3 files changed, 114 insertions(+), 28 deletions(-) diff --git a/rewatch/src/cli.rs b/rewatch/src/cli.rs index 07c3cdfb9bb..2a7fc40100a 100644 --- a/rewatch/src/cli.rs +++ b/rewatch/src/cli.rs @@ -383,6 +383,16 @@ mod tests { } } + #[test] + fn watch_clear_screen_flag_is_parsed() { + let cli = parse(&["rescript", "watch", "--clear-screen"]).expect("expected watch command"); + + match cli.command { + Command::Watch(watch_args) => assert!(watch_args.clear_screen), + other => panic!("expected watch command, got {other:?}"), + } + } + #[test] fn clean_prod_flag_is_parsed() { let cli = parse(&["rescript", "clean", "--prod"]).expect("expected clean command"); @@ -428,6 +438,10 @@ pub struct WatchArgs { #[command(flatten)] pub warn_error: WarnErrorArg, + /// Clear terminal screen before each rebuild in interactive watch mode. + #[arg(long, default_value_t = false)] + pub clear_screen: bool, + /// Skip dev-dependencies and dev sources (type: "dev") #[arg(long, default_value_t = false)] pub prod: bool, @@ -440,6 +454,7 @@ impl From for WatchArgs { filter: build_args.filter, after_build: build_args.after_build, warn_error: build_args.warn_error, + clear_screen: false, prod: build_args.prod, } } diff --git a/rewatch/src/main.rs b/rewatch/src/main.rs index 8fe9c8d78cc..8ab4eaa68cd 100644 --- a/rewatch/src/main.rs +++ b/rewatch/src/main.rs @@ -79,6 +79,7 @@ fn main() -> Result<()> { true, // create_sourcedirs is now always enabled plain_output, (*watch_args.warn_error).clone(), + watch_args.clear_screen, watch_args.prod, ) { Err(e) => { diff --git a/rewatch/src/watcher.rs b/rewatch/src/watcher.rs index 6812f4b658e..53518758cc0 100644 --- a/rewatch/src/watcher.rs +++ b/rewatch/src/watcher.rs @@ -9,6 +9,7 @@ use crate::lock::LockKind; use crate::queue::FifoQueue; use crate::queue::*; use anyhow::{Context, Result}; +use console::Term; use futures_timer::Delay; use notify::event::ModifyKind; use notify::{Config, Error, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; @@ -177,6 +178,31 @@ fn carry_forward_compile_warnings(previous: &BuildCommandState, next: &mut Build } } +fn should_clear_screen( + clear_screen: bool, + show_progress: bool, + plain_output: bool, + initial_build: bool, +) -> bool { + clear_screen && show_progress && !plain_output && !initial_build +} + +fn clear_terminal_screen() { + let _ = Term::stdout().clear_screen(); +} + +fn print_rebuild_header(compile_type: CompileType) { + match compile_type { + CompileType::Incremental => println!("Change detected. Rebuilding..."), + CompileType::Full => println!("Change detected. Full rebuild..."), + CompileType::None => (), + } +} + +fn print_build_failed_footer() { + println!("\nBuild failed. Watching for changes..."); +} + struct AsyncWatchArgs<'a> { watcher: &'a mut RecommendedWatcher, current_watch_paths: Vec<(PathBuf, RecursiveMode)>, @@ -188,6 +214,7 @@ struct AsyncWatchArgs<'a> { after_build: Option, create_sourcedirs: bool, plain_output: bool, + clear_screen: bool, prod: bool, } @@ -203,6 +230,7 @@ async fn async_watch( after_build, create_sourcedirs, plain_output, + clear_screen, prod, }: AsyncWatchArgs<'_>, ) -> Result<()> { @@ -377,8 +405,13 @@ async fn async_watch( match needs_compile_type { CompileType::Incremental => { + if should_clear_screen(clear_screen, show_progress, plain_output, initial_build) { + clear_terminal_screen(); + print_rebuild_header(CompileType::Incremental); + } + let timing_total = Instant::now(); - if let Ok(result) = build::incremental_build( + let result = build::incremental_build( &mut build_state, None, initial_build, @@ -386,31 +419,46 @@ async fn async_watch( !initial_build, create_sourcedirs, plain_output, - ) { - if let Some(a) = after_build.clone() { - cmd::run(a) + ); + + match result { + Ok(result) => { + if let Some(a) = after_build.clone() { + cmd::run(a) + } + let timing_total_elapsed = timing_total.elapsed(); + if show_progress { + let compilation_type = if initial_build { "initial" } else { "incremental" }; + if plain_output { + println!("Finished {compilation_type} compilation") + } else { + println!( + "\n{}\n", + build::format_finished_compilation_message( + Some(compilation_type), + result, + timing_total_elapsed, + ) + ); + } + } } - let timing_total_elapsed = timing_total.elapsed(); - if show_progress { - let compilation_type = if initial_build { "initial" } else { "incremental" }; - if plain_output { - println!("Finished {compilation_type} compilation") - } else { - println!( - "\n{}\n", - build::format_finished_compilation_message( - Some(compilation_type), - result, - timing_total_elapsed, - ) - ); + Err(_) => { + if should_clear_screen(clear_screen, show_progress, plain_output, initial_build) { + print_build_failed_footer(); } } } + needs_compile_type = CompileType::None; initial_build = false; } CompileType::Full => { + if should_clear_screen(clear_screen, show_progress, plain_output, initial_build) { + clear_terminal_screen(); + print_rebuild_header(CompileType::Full); + } + let timing_total = Instant::now(); let mut next_build_state = build::initialize_build( None, @@ -443,17 +491,28 @@ async fn async_watch( create_sourcedirs, plain_output, ); - if let Ok(result) = result { - if let Some(a) = after_build.clone() { - cmd::run(a) - } + match result { + Ok(result) => { + if let Some(a) = after_build.clone() { + cmd::run(a) + } - let timing_total_elapsed = timing_total.elapsed(); - if !plain_output && show_progress { - println!( - "\n{}\n", - build::format_finished_compilation_message(None, result, timing_total_elapsed,) - ); + let timing_total_elapsed = timing_total.elapsed(); + if !plain_output && show_progress { + println!( + "\n{}\n", + build::format_finished_compilation_message( + None, + result, + timing_total_elapsed, + ) + ); + } + } + Err(_) => { + if should_clear_screen(clear_screen, show_progress, plain_output, initial_build) { + print_build_failed_footer(); + } } } @@ -479,6 +538,7 @@ pub fn start( create_sourcedirs: bool, plain_output: bool, warn_error: Option, + clear_screen: bool, prod: bool, ) -> Result<()> { futures::executor::block_on(async { @@ -518,6 +578,7 @@ pub fn start( after_build, create_sourcedirs, plain_output, + clear_screen, prod, }) .await @@ -649,6 +710,15 @@ mod tests { } } + #[test] + fn clears_screen_only_for_non_initial_interactive_rebuilds() { + assert!(should_clear_screen(true, true, false, false)); + assert!(!should_clear_screen(true, true, false, true)); + assert!(!should_clear_screen(true, true, true, false)); + assert!(!should_clear_screen(true, false, false, false)); + assert!(!should_clear_screen(false, true, false, false)); + } + #[test] fn carries_forward_implementation_warnings_for_matching_module_paths() { let previous = test_build_state( From 27abc66e6663cecdae9422f9b91cfd4be153d1b5 Mon Sep 17 00:00:00 2001 From: Paul Kim Date: Sun, 19 Apr 2026 16:03:26 +0200 Subject: [PATCH 4/4] fix(rewatch): restore full rebuild plain output Signed-off-by: Paul Kim --- rewatch/src/watcher.rs | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/rewatch/src/watcher.rs b/rewatch/src/watcher.rs index 53518758cc0..0cfa30a7586 100644 --- a/rewatch/src/watcher.rs +++ b/rewatch/src/watcher.rs @@ -498,15 +498,19 @@ async fn async_watch( } let timing_total_elapsed = timing_total.elapsed(); - if !plain_output && show_progress { - println!( - "\n{}\n", - build::format_finished_compilation_message( - None, - result, - timing_total_elapsed, - ) - ); + if show_progress { + if plain_output { + println!("Finished compilation") + } else { + println!( + "\n{}\n", + build::format_finished_compilation_message( + None, + result, + timing_total_elapsed, + ) + ); + } } } Err(_) => {