fix(cli): queue file changes that arrive during builds instead of dropping them (#5007)

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.
This commit is contained in:
Amos Wenger authored and GitHub committed 2025-12-04 14:01:29 -06:00
1 parent 5b8bc358db
commit e5a98d094d
2 files changed
+48 -20

No files matched your search

+33 -19
View File
@@ -173,30 +173,44 @@ pub(crate) async fn serve_all(args: ServeArgs, tracer: &TraceController) -> Resu
return Err(err); return Err(err);
} }
} }
BuilderUpdate::BuildReady { bundle } => match bundle.mode { BuilderUpdate::BuildReady { bundle } => {
BuildMode::Thin { ref cache, .. } => { match bundle.mode {
if let Err(err) = BuildMode::Thin { ref cache, .. } => {
builder.hotpatch(&bundle, id, cache, &mut devserver).await 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::<crate::build::PatchError>()
{ {
tracing::info!("Starting full rebuild: {err}"); tracing::error!("Failed to hot-patch app: {err}");
builder.full_rebuild().await;
devserver.send_reload_start().await; if let Some(_patching) =
devserver.start_build().await; err.downcast_ref::<crate::build::PatchError>()
{
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 // Process any file changes that were queued while the build was in progress.
.open(&bundle, &mut devserver) // This handles tools like stylance, tailwind, or sass that generate files
.await // in response to source changes - those changes would otherwise be lost.
.inspect_err(|e| tracing::error!("Failed to open app: {}", e)); 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 } => { BuilderUpdate::StdoutReceived { msg } => {
screen.push_stdio(bundle_format, msg, tracing::Level::INFO); screen.push_stdio(bundle_format, msg, tracing::Level::INFO);
} }
+15 -1
View File
@@ -81,6 +81,9 @@ pub(crate) struct AppServer {
// Additional plugin-type tools // Additional plugin-type tools
pub(crate) tw_watcher: tokio::task::JoinHandle<Result<()>>, pub(crate) tw_watcher: tokio::task::JoinHandle<Result<()>>,
// File changes that arrived while a build was in progress, to be processed after build completes
pub(crate) pending_file_changes: Vec<PathBuf>,
} }
pub(crate) struct CachedFile { pub(crate) struct CachedFile {
@@ -209,6 +212,7 @@ impl AppServer {
tw_watcher, tw_watcher,
server_args, server_args,
client_args, client_args,
pending_file_changes: Vec::new(),
}; };
// Only register the hot-reload stuff if we're watching the filesystem // 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<PathBuf> {
std::mem::take(&mut self.pending_file_changes)
}
pub(crate) async fn rebuild_ssg(&mut self, devserver: &WebServer) { pub(crate) async fn rebuild_ssg(&mut self, devserver: &WebServer) {
if self.client.stage != BuildStage::Success { if self.client.stage != BuildStage::Success {
return; return;
@@ -348,10 +358,14 @@ impl AppServer {
self.client.stage, self.client.stage,
BuildStage::Failed | BuildStage::Aborted | BuildStage::Success 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!( 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 files
); );
self.pending_file_changes.extend(files.iter().cloned());
return; return;
} }