From a1dfc2b7ef6c41c5598e3a6b469852a74d5e6fa5 Mon Sep 17 00:00:00 2001 From: Joe Date: Tue, 3 Feb 2026 17:34:31 -0600 Subject: [PATCH] Implement atomic file writing for sourcemaps - Added a new `write_atomic` function to ensure sourcemaps are written to a temporary file before renaming, preventing file watchers from reading incomplete data. - Enhanced error handling during file operations with context-aware messages for better debugging. - Updated `write_sourcemap` to utilize the atomic write method, improving reliability and safety during sourcemap creation. --- src/cli/sourcemap.rs | 59 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 3 deletions(-) diff --git a/src/cli/sourcemap.rs b/src/cli/sourcemap.rs index 1b023630..41ec36b8 100644 --- a/src/cli/sourcemap.rs +++ b/src/cli/sourcemap.rs @@ -3,8 +3,11 @@ use std::{ io::{BufWriter, Write}, mem::forget, path::{self, Path, PathBuf}, + process, + time::{SystemTime, UNIX_EPOCH}, }; +use anyhow::Context; use clap::Parser; use fs_err::File; use memofs::Vfs; @@ -259,11 +262,13 @@ pub(crate) fn write_sourcemap( ); if let Some(output_path) = output { - let mut file = BufWriter::new(File::create(output_path)?); // Use standard JSON (not JSON5) for sourcemaps - required by external tools like LSPs let json_output = serde_json::to_string(&root_node)?; - file.write_all(json_output.as_bytes())?; - file.flush()?; + + // Use atomic write (temp file + rename) to prevent file watchers from + // reading partial files. Rename is atomic on all major filesystems when + // source and destination are on the same filesystem. + write_atomic(output_path, json_output.as_bytes())?; if !quiet { println!("Created sourcemap at {}", output_path.display()); @@ -276,3 +281,51 @@ pub(crate) fn write_sourcemap( Ok(()) } + +/// Writes data to a file atomically by writing to a temporary file first, +/// then renaming it to the target path. This ensures file watchers never +/// see partial file contents. +fn write_atomic(target: &Path, data: &[u8]) -> anyhow::Result<()> { + // Generate a unique temp filename in the same directory as the target. + // Using same directory ensures rename is atomic (same filesystem). + let parent = target.parent().unwrap_or_else(|| Path::new(".")); + + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_nanos()) + .unwrap_or(0); + + let temp_name = format!( + ".{}.{}.{}.tmp", + target + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("sourcemap"), + process::id(), + timestamp + ); + let temp_path = parent.join(&temp_name); + + // Write to temp file + let mut file = BufWriter::new( + File::create(&temp_path) + .with_context(|| format!("Failed to create temp file: {}", temp_path.display()))?, + ); + file.write_all(data) + .with_context(|| format!("Failed to write temp file: {}", temp_path.display()))?; + file.flush()?; + + // Ensure data is synced to disk before rename (important for crash safety) + file.into_inner()?.sync_all()?; + + // Atomic rename to target path + std::fs::rename(&temp_path, target).with_context(|| { + format!( + "Failed to rename {} to {}", + temp_path.display(), + target.display() + ) + })?; + + Ok(()) +}