From 1dcb4bfe95aeae0c8910883ecde60de378808d74 Mon Sep 17 00:00:00 2001 From: Amos Wenger Date: Fri, 21 Nov 2025 22:50:10 +0100 Subject: [PATCH] fix(cli): queue file changes that arrive during builds instead of dropping them Previously, when a file change event arrived while a build was in progress (i.e., client stage was not Success/Failed/Aborted), the event was logged and silently discarded. This caused issues with tools like stylance, tailwind watch, or sass --watch that generate output files in response to source file changes. The typical failure case: 1. User saves a .module.scss file 2. dx starts rebuilding 3. stylance CLI (watching the same file) regenerates stylance.css 4. dx receives the stylance.css change event but discards it 5. Build completes with stale CSS Now, file changes that arrive during a build are queued in a pending_file_changes vector and processed after the build completes. --- packages/cli/src/serve/mod.rs | 52 ++++++++++++++++++++------------ packages/cli/src/serve/runner.rs | 16 +++++++++- 2 files changed, 48 insertions(+), 20 deletions(-) diff --git a/packages/cli/src/serve/mod.rs b/packages/cli/src/serve/mod.rs index 176e192cdd..36008540d9 100644 --- a/packages/cli/src/serve/mod.rs +++ b/packages/cli/src/serve/mod.rs @@ -173,30 +173,44 @@ pub(crate) async fn serve_all(args: ServeArgs, tracer: &TraceController) -> Resu return Err(err); } } - BuilderUpdate::BuildReady { bundle } => match bundle.mode { - BuildMode::Thin { ref cache, .. } => { - if let Err(err) = - builder.hotpatch(&bundle, id, cache, &mut devserver).await - { - tracing::error!("Failed to hot-patch app: {err}"); - - if let Some(_patching) = - err.downcast_ref::() + BuilderUpdate::BuildReady { bundle } => { + match bundle.mode { + BuildMode::Thin { ref cache, .. } => { + if let Err(err) = + builder.hotpatch(&bundle, id, cache, &mut devserver).await { - tracing::info!("Starting full rebuild: {err}"); - builder.full_rebuild().await; - devserver.send_reload_start().await; - devserver.start_build().await; + tracing::error!("Failed to hot-patch app: {err}"); + + if let Some(_patching) = + err.downcast_ref::() + { + tracing::info!("Starting full rebuild: {err}"); + builder.full_rebuild().await; + devserver.send_reload_start().await; + devserver.start_build().await; + } } } + BuildMode::Base { .. } | BuildMode::Fat => { + _ = builder + .open(&bundle, &mut devserver) + .await + .inspect_err(|e| tracing::error!("Failed to open app: {}", e)); + } } - BuildMode::Base { .. } | BuildMode::Fat => { - _ = builder - .open(&bundle, &mut devserver) - .await - .inspect_err(|e| tracing::error!("Failed to open app: {}", e)); + + // Process any file changes that were queued while the build was in progress. + // This handles tools like stylance, tailwind, or sass that generate files + // in response to source changes - those changes would otherwise be lost. + let pending = builder.take_pending_file_changes(); + if !pending.is_empty() { + tracing::debug!( + "Processing {} pending file changes after build", + pending.len() + ); + builder.handle_file_change(&pending, &mut devserver).await; } - }, + } BuilderUpdate::StdoutReceived { msg } => { screen.push_stdio(bundle_format, msg, tracing::Level::INFO); } diff --git a/packages/cli/src/serve/runner.rs b/packages/cli/src/serve/runner.rs index 4d06ae7131..cc08901c78 100644 --- a/packages/cli/src/serve/runner.rs +++ b/packages/cli/src/serve/runner.rs @@ -81,6 +81,9 @@ pub(crate) struct AppServer { // Additional plugin-type tools pub(crate) tw_watcher: tokio::task::JoinHandle>, + + // File changes that arrived while a build was in progress, to be processed after build completes + pub(crate) pending_file_changes: Vec, } pub(crate) struct CachedFile { @@ -209,6 +212,7 @@ impl AppServer { tw_watcher, server_args, client_args, + pending_file_changes: Vec::new(), }; // Only register the hot-reload stuff if we're watching the filesystem @@ -240,6 +244,12 @@ impl AppServer { } } + /// Take any pending file changes that were queued while a build was in progress. + /// Returns the files and clears the pending list. + pub(crate) fn take_pending_file_changes(&mut self) -> Vec { + std::mem::take(&mut self.pending_file_changes) + } + pub(crate) async fn rebuild_ssg(&mut self, devserver: &WebServer) { if self.client.stage != BuildStage::Success { return; @@ -348,10 +358,14 @@ impl AppServer { self.client.stage, BuildStage::Failed | BuildStage::Aborted | BuildStage::Success ) { + // Queue file changes that arrive during a build, so we can process them after the build completes. + // This prevents losing changes from tools like stylance, tailwind, or sass that generate files + // in response to source changes. tracing::debug!( - "Ignoring file change: client is not ready to receive hotreloads. Files: {:#?}", + "Queueing file change: client is not ready to receive hotreloads. Files: {:#?}", files ); + self.pending_file_changes.extend(files.iter().cloned()); return; }