Rust library to generate static websites
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: try to make dev server even more stable (#50)

* fix: try to make dev server even more stable

* nit: try something with the lockfile

* fix: update ref

* fix: try things

* fix: cancel

* fix: evrything

* fix: improve port resuse

* fix: rework how we watch

authored by

Erika and committed by
GitHub
3c8326c4 fe799e8a

+596 -335
+86 -51
Cargo.lock
··· 52 52 53 53 [[package]] 54 54 name = "anstream" 55 - version = "0.6.20" 55 + version = "0.6.21" 56 56 source = "registry+https://github.com/rust-lang/crates.io-index" 57 - checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" 57 + checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" 58 58 dependencies = [ 59 59 "anstyle", 60 60 "anstyle-parse", ··· 215 215 216 216 [[package]] 217 217 name = "axum" 218 - version = "0.8.5" 218 + version = "0.8.6" 219 219 source = "registry+https://github.com/rust-lang/crates.io-index" 220 - checksum = "98e529aee37b5c8206bb4bf4c44797127566d72f76952c970bd3d1e85de8f4e2" 220 + checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871" 221 221 dependencies = [ 222 222 "axum-core", 223 223 "base64", ··· 251 251 252 252 [[package]] 253 253 name = "axum-core" 254 - version = "0.5.4" 254 + version = "0.5.5" 255 255 source = "registry+https://github.com/rust-lang/crates.io-index" 256 - checksum = "0ac7a6beb1182c7e30253ee75c3e918080bfb83f5a3023bcdf7209d85fd147e6" 256 + checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" 257 257 dependencies = [ 258 258 "bytes", 259 259 "futures-core", ··· 428 428 "bitflags 2.9.4", 429 429 "brk-notify-types", 430 430 "fsevent-sys", 431 - "inotify 0.11.0", 431 + "inotify", 432 432 "kqueue", 433 433 "libc", 434 434 "log", ··· 835 835 836 836 [[package]] 837 837 name = "bytemuck" 838 - version = "1.23.2" 838 + version = "1.24.0" 839 839 source = "registry+https://github.com/rust-lang/crates.io-index" 840 - checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" 840 + checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" 841 841 842 842 [[package]] 843 843 name = "byteorder" ··· 858 858 checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 859 859 860 860 [[package]] 861 + name = "camino" 862 + version = "1.2.1" 863 + source = "registry+https://github.com/rust-lang/crates.io-index" 864 + checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609" 865 + dependencies = [ 866 + "serde_core", 867 + ] 868 + 869 + [[package]] 870 + name = "cargo-platform" 871 + version = "0.3.1" 872 + source = "registry+https://github.com/rust-lang/crates.io-index" 873 + checksum = "122ec45a44b270afd1402f351b782c676b173e3c3fb28d86ff7ebfb4d86a4ee4" 874 + dependencies = [ 875 + "serde", 876 + ] 877 + 878 + [[package]] 879 + name = "cargo_metadata" 880 + version = "0.23.0" 881 + source = "registry+https://github.com/rust-lang/crates.io-index" 882 + checksum = "981a6f317983eec002839b90fae7411a85621410ae591a9cab2ecf5cb5744873" 883 + dependencies = [ 884 + "camino", 885 + "cargo-platform", 886 + "semver", 887 + "serde", 888 + "serde_json", 889 + "thiserror 2.0.17", 890 + ] 891 + 892 + [[package]] 861 893 name = "castaway" 862 894 version = "0.2.4" 863 895 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 868 900 869 901 [[package]] 870 902 name = "cc" 871 - version = "1.2.39" 903 + version = "1.2.40" 872 904 source = "registry+https://github.com/rust-lang/crates.io-index" 873 - checksum = "e1354349954c6fc9cb0deab020f27f783cf0b604e8bb754dc4658ecf0d29c35f" 905 + checksum = "e1d05d92f4b1fd76aad469d46cdd858ca761576082cd37df81416691e50199fb" 874 906 dependencies = [ 875 907 "find-msvc-tools", 876 908 "jobserver", ··· 1543 1575 1544 1576 [[package]] 1545 1577 name = "find-msvc-tools" 1546 - version = "0.1.2" 1578 + version = "0.1.3" 1547 1579 source = "registry+https://github.com/rust-lang/crates.io-index" 1548 - checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" 1580 + checksum = "0399f9d26e5191ce32c498bebd31e7a3ceabc2745f0ac54af3f335126c3f24b3" 1549 1581 1550 1582 [[package]] 1551 1583 name = "fixedbitset" ··· 2092 2124 checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" 2093 2125 dependencies = [ 2094 2126 "cfb", 2095 - ] 2096 - 2097 - [[package]] 2098 - name = "inotify" 2099 - version = "0.9.6" 2100 - source = "registry+https://github.com/rust-lang/crates.io-index" 2101 - checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" 2102 - dependencies = [ 2103 - "bitflags 1.3.2", 2104 - "inotify-sys", 2105 - "libc", 2106 2127 ] 2107 2128 2108 2129 [[package]] ··· 2375 2396 2376 2397 [[package]] 2377 2398 name = "lock_api" 2378 - version = "0.4.13" 2399 + version = "0.4.14" 2379 2400 source = "registry+https://github.com/rust-lang/crates.io-index" 2380 - checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" 2401 + checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" 2381 2402 dependencies = [ 2382 - "autocfg", 2383 2403 "scopeguard", 2384 2404 ] 2385 2405 ··· 2490 2510 dependencies = [ 2491 2511 "axum", 2492 2512 "brk_rolldown", 2513 + "cargo_metadata", 2493 2514 "chrono", 2494 2515 "clap", 2495 2516 "colored", ··· 2779 2800 2780 2801 [[package]] 2781 2802 name = "notify" 2782 - version = "6.1.1" 2803 + version = "8.2.0" 2783 2804 source = "registry+https://github.com/rust-lang/crates.io-index" 2784 - checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" 2805 + checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" 2785 2806 dependencies = [ 2786 2807 "bitflags 2.9.4", 2787 - "crossbeam-channel", 2788 - "filetime", 2789 2808 "fsevent-sys", 2790 - "inotify 0.9.6", 2809 + "inotify", 2791 2810 "kqueue", 2792 2811 "libc", 2793 2812 "log", 2794 - "mio 0.8.11", 2813 + "mio 1.0.4", 2814 + "notify-types", 2795 2815 "walkdir", 2796 - "windows-sys 0.48.0", 2816 + "windows-sys 0.60.2", 2797 2817 ] 2798 2818 2799 2819 [[package]] 2800 2820 name = "notify-debouncer-full" 2801 - version = "0.3.2" 2821 + version = "0.6.0" 2802 2822 source = "registry+https://github.com/rust-lang/crates.io-index" 2803 - checksum = "fb7fd166739789c9ff169e654dc1501373db9d80a4c3f972817c8a4d7cf8f34e" 2823 + checksum = "375bd3a138be7bfeff3480e4a623df4cbfb55b79df617c055cd810ba466fa078" 2804 2824 dependencies = [ 2805 - "crossbeam-channel", 2806 2825 "file-id", 2807 2826 "log", 2808 2827 "notify", 2809 - "parking_lot", 2828 + "notify-types", 2810 2829 "walkdir", 2811 2830 ] 2831 + 2832 + [[package]] 2833 + name = "notify-types" 2834 + version = "2.0.0" 2835 + source = "registry+https://github.com/rust-lang/crates.io-index" 2836 + checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" 2812 2837 2813 2838 [[package]] 2814 2839 name = "nu-ansi-term" ··· 3296 3321 3297 3322 [[package]] 3298 3323 name = "oxc_resolver" 3299 - version = "11.8.4" 3324 + version = "11.9.0" 3300 3325 source = "registry+https://github.com/rust-lang/crates.io-index" 3301 - checksum = "743c415f2237308d3a50d15d5ab5e432fd44c3b2c77042b01bbbd4e5e7d1ca0f" 3326 + checksum = "9bc696688fc6cbab56971f02badc233541f964f4705240c986abc02535a3728e" 3302 3327 dependencies = [ 3303 3328 "cfg-if", 3304 3329 "indexmap", ··· 3491 3516 3492 3517 [[package]] 3493 3518 name = "parking_lot" 3494 - version = "0.12.4" 3519 + version = "0.12.5" 3495 3520 source = "registry+https://github.com/rust-lang/crates.io-index" 3496 - checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" 3521 + checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" 3497 3522 dependencies = [ 3498 3523 "lock_api", 3499 3524 "parking_lot_core", ··· 3501 3526 3502 3527 [[package]] 3503 3528 name = "parking_lot_core" 3504 - version = "0.9.11" 3529 + version = "0.9.12" 3505 3530 source = "registry+https://github.com/rust-lang/crates.io-index" 3506 - checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" 3531 + checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" 3507 3532 dependencies = [ 3508 3533 "cfg-if", 3509 3534 "libc", 3510 3535 "redox_syscall", 3511 3536 "smallvec", 3512 - "windows-targets 0.52.6", 3537 + "windows-link", 3513 3538 ] 3514 3539 3515 3540 [[package]] ··· 3532 3557 3533 3558 [[package]] 3534 3559 name = "petgraph" 3535 - version = "0.8.2" 3560 + version = "0.8.3" 3536 3561 source = "registry+https://github.com/rust-lang/crates.io-index" 3537 - checksum = "54acf3a685220b533e437e264e4d932cfbdc4cc7ec0cd232ed73c08d03b8a7ca" 3562 + checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" 3538 3563 dependencies = [ 3539 3564 "fixedbitset", 3540 3565 "hashbrown 0.15.5", ··· 4244 4269 4245 4270 [[package]] 4246 4271 name = "rustls-webpki" 4247 - version = "0.103.6" 4272 + version = "0.103.7" 4248 4273 source = "registry+https://github.com/rust-lang/crates.io-index" 4249 - checksum = "8572f3c2cb9934231157b45499fc41e1f58c589fdfb81a844ba873265e80f8eb" 4274 + checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" 4250 4275 dependencies = [ 4251 4276 "ring", 4252 4277 "rustls-pki-types", ··· 4314 4339 version = "1.2.0" 4315 4340 source = "registry+https://github.com/rust-lang/crates.io-index" 4316 4341 checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749" 4342 + 4343 + [[package]] 4344 + name = "semver" 4345 + version = "1.0.27" 4346 + source = "registry+https://github.com/rust-lang/crates.io-index" 4347 + checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" 4348 + dependencies = [ 4349 + "serde", 4350 + "serde_core", 4351 + ] 4317 4352 4318 4353 [[package]] 4319 4354 name = "seq-macro" ··· 5145 5180 5146 5181 [[package]] 5147 5182 name = "typenum" 5148 - version = "1.18.0" 5183 + version = "1.19.0" 5149 5184 source = "registry+https://github.com/rust-lang/crates.io-index" 5150 - checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" 5185 + checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" 5151 5186 5152 5187 [[package]] 5153 5188 name = "unicase"
+11 -5
crates/maudit-cli/Cargo.toml
··· 13 13 chrono = "0.4.39" 14 14 colored = "2.2.0" 15 15 clap = { version = "4.5.23", features = ["derive"] } 16 - tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal"] } 16 + tokio = { version = "1", features = [ 17 + "macros", 18 + "rt-multi-thread", 19 + "signal", 20 + "process", 21 + ] } 17 22 axum = { version = "0.8.1", features = ["ws"] } 18 23 futures = "0.3" 19 24 tower-http = { version = "0.6.2", features = ["fs", "trace"] } 20 25 tracing = "0.1" 21 26 tracing-subscriber = { version = "=0.3.19", features = [ 22 - "env-filter", 23 - "chrono", 27 + "env-filter", 28 + "chrono", 24 29 ] } 25 - notify = "6.1.1" 26 - notify-debouncer-full = "0.3.1" 30 + notify = "8.2.0" 31 + notify-debouncer-full = "0.6.0" 27 32 inquire = "0.7.5" 28 33 rand = "0.9.0" 29 34 spinach = "3" ··· 35 40 quanta = "0.12.6" 36 41 serde_json = "1.0" 37 42 tokio-util = "0.7" 43 + cargo_metadata = "0.23.0" 38 44 39 45 [build-dependencies] 40 46 rolldown = { package = "brk_rolldown", version = "0.2.3" }
+190 -224
crates/maudit-cli/src/dev.rs
··· 1 1 pub(crate) mod server; 2 2 3 + mod build; 3 4 mod filterer; 4 5 5 - use colored::Colorize; 6 - use filterer::should_watch_path; 7 - use notify::{EventKind, RecursiveMode, Watcher, event::ModifyKind}; 6 + use notify::{ 7 + EventKind, RecursiveMode, 8 + event::{CreateKind, ModifyKind, RemoveKind}, 9 + }; 8 10 use notify_debouncer_full::{DebounceEventResult, DebouncedEvent, new_debouncer}; 9 11 use quanta::Instant; 10 - use server::{WebSocketMessage, update_status}; 11 - use std::path::Path; 12 - use std::sync::Arc; 13 - use tokio::sync::broadcast; 14 - use tokio_util::sync::CancellationToken; 15 - use tracing::{debug, error, info}; 12 + use server::WebSocketMessage; 13 + use std::{fs, path::Path}; 14 + use tokio::{ 15 + sync::{broadcast, mpsc::channel}, 16 + task::JoinHandle, 17 + }; 18 + use tracing::{error, info}; 16 19 17 - use crate::logging::{FormatElapsedTimeOptions, format_elapsed_time}; 18 - 19 - fn should_rebuild_for_event(event: &DebouncedEvent) -> bool { 20 - event.paths.iter().any(|path| { 21 - should_watch_path(path) 22 - && match event.kind { 23 - // Only rebuild on actual content modifications, not metadata changes 24 - EventKind::Modify(ModifyKind::Data(_)) => true, 25 - EventKind::Modify(ModifyKind::Name(_)) => true, 26 - EventKind::Modify(ModifyKind::Any) => true, 27 - EventKind::Modify(ModifyKind::Other) => true, 28 - // Skip metadata-only changes (permissions, timestamps, etc.) 29 - EventKind::Modify(ModifyKind::Metadata(_)) => false, 30 - // Include file creation and removal 31 - EventKind::Create(_) => true, 32 - EventKind::Remove(_) => true, 33 - // Skip other event types 34 - _ => false, 35 - } 36 - }) 37 - } 20 + use crate::dev::build::BuildManager; 38 21 39 22 pub async fn start_dev_env(cwd: &str, host: bool) -> Result<(), Box<dyn std::error::Error>> { 40 23 let start_time = Instant::now(); 41 24 info!(name: "dev", "Preparing dev environment…"); 42 25 43 - // Do initial sync build 44 - info!(name: "build", "Doing initial build…"); 45 - 46 - let child = std::process::Command::new("cargo") 47 - .args(["run", "--quiet"]) 48 - .envs([ 49 - ("MAUDIT_DEV", "true"), 50 - ("MAUDIT_QUIET", "true"), 51 - ("CARGO_TERM_COLOR", "always"), 52 - ("RUSTFLAGS", "-Awarnings"), 53 - ]) 54 - .stderr(std::process::Stdio::piped()) 55 - .spawn() 56 - .unwrap(); 57 - 58 - // Start a timer task to show warning after X seconds 59 - let warning_task = tokio::spawn(async { 60 - tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; // Adjust timeout as needed 61 - info!(name: "build", "{}", "This can take some time on the first run, or if there are uncached dependencies or assets..".dimmed()); 62 - }); 63 - 64 - // Wait for the command to finish 65 - let output = child.wait_with_output().unwrap(); 66 - 67 - // Cancel the warning task since the command finished 68 - warning_task.abort(); 69 - 70 - let stderr = String::from_utf8_lossy(&output.stderr); 71 - 72 - let duration = start_time.elapsed(); 73 - let formatted_elasped_time = 74 - format_elapsed_time(duration, &FormatElapsedTimeOptions::default_dev()); 75 - 76 - if output.status.success() { 77 - info!(name: "build", "Initial build finished {}", formatted_elasped_time); 78 - } else { 79 - error!(name: "build", "{}", stderr); 80 - error!(name: "build", "Initial build failed with errors {}", formatted_elasped_time); 81 - } 82 - 83 26 let (sender_websocket, _) = broadcast::channel::<WebSocketMessage>(100); 84 27 85 - // Create shared status state 86 - let current_status = Arc::new(tokio::sync::RwLock::new(if !output.status.success() { 87 - Some(stderr.to_string()) 88 - } else { 89 - None 90 - })); 28 + // Create build manager (it will create its own status state internally) 29 + let build_manager = BuildManager::new(sender_websocket.clone()); 91 30 92 - // Track the current build's cancellation token 93 - let current_build_cancel = 94 - Arc::new(tokio::sync::RwLock::new(Option::<CancellationToken>::None)); 31 + // Do initial build 32 + info!(name: "build", "Doing initial build…"); 33 + let initial_build_success = build_manager.do_initial_build().await?; 95 34 96 - let web_server_thread: tokio::task::JoinHandle<()> = 97 - tokio::spawn(server::start_dev_web_server( 98 - start_time, 99 - sender_websocket.clone(), 100 - host, 101 - if !output.status.success() { 102 - Some(stderr.to_string()) 103 - } else { 104 - None 105 - }, 106 - current_status.clone(), 107 - )); 35 + // Set up file watching with debouncer 36 + let (tx, mut rx) = channel::<DebounceEventResult>(1000); 108 37 109 - // Set up file watching with debouncer 110 - let (tx, mut rx) = tokio::sync::mpsc::channel::<DebounceEventResult>(100); 38 + let directories = fs::read_dir(cwd)? 39 + .filter_map(|entry| entry.ok()) 40 + .filter(|entry| entry.path().is_dir()) 41 + .filter(|entry| { 42 + let path = entry.path(); 43 + let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); 44 + !matches!(file_name, "target" | ".git" | "dist") 45 + }) 46 + .map(|entry| entry.path()) 47 + .collect::<Vec<_>>(); 111 48 112 49 let mut debouncer = new_debouncer( 113 50 std::time::Duration::from_millis(100), ··· 117 54 }, 118 55 )?; 119 56 120 - debouncer 121 - .watcher() 122 - .watch(Path::new(cwd), RecursiveMode::Recursive)?; 57 + // Watch the root directly both for changes to files like Cargo.toml and for new directories 58 + debouncer.watch(cwd, RecursiveMode::NonRecursive)?; 123 59 124 - // Handle file events 125 - tokio::spawn(async move { 126 - let browser_websocket = sender_websocket.clone(); 127 - let current_status = current_status.clone(); 128 - let build_cancel_ref = current_build_cancel.clone(); 60 + // It'd seems like it'd be much easier to just watch recursively from cwd, but the problem is that this ends up 61 + // watching a looooooot of files that we don't want to watch (target directory, dist directory, .git directory, etc.) which is causing about a million issues in Notify. 62 + // The fork of Notify we use has support for filtering while watching, but it doesn't seem to work super well in practice. 63 + // So instead we just watch the top-level directories (excluding known ones to ignore) and then add/remove watches for new/deleted directories as needed. 64 + for dir in &directories { 65 + debouncer.watch(dir, RecursiveMode::Recursive)?; 66 + } 129 67 130 - while let Some(result) = rx.recv().await { 131 - match result { 132 - Ok(events) => { 133 - // Filter events that should trigger a rebuild 134 - let triggering_events: Vec<_> = events 135 - .iter() 136 - .filter(|event| should_rebuild_for_event(event)) 137 - .collect(); 68 + let mut web_server_thread: Option<tokio::task::JoinHandle<()>> = None; 138 69 139 - if !triggering_events.is_empty() { 140 - debug!("File events: {} valid changes", triggering_events.len()); 141 - for event in &triggering_events { 142 - for path in &event.paths { 143 - debug!(" {:?}: {}", event.kind, path.display()); 144 - } 145 - } 70 + // If initial build succeeded, start web server immediately 71 + if initial_build_success { 72 + info!(name: "dev", "Starting web server..."); 73 + web_server_thread = Some(tokio::spawn(server::start_dev_web_server( 74 + start_time, 75 + sender_websocket.clone(), 76 + host, 77 + None, 78 + build_manager.current_status(), 79 + ))); 80 + } 146 81 147 - info!(name: "build", "Detected changes. Rebuilding…"); 82 + // Clone build manager for the file watcher task 83 + let build_manager_watcher = build_manager.clone(); 84 + let sender_websocket_watcher = sender_websocket.clone(); 148 85 149 - // Cancel any ongoing build 150 - { 151 - let mut current_cancel = build_cancel_ref.write().await; 152 - if let Some(cancel_token) = current_cancel.take() { 153 - cancel_token.cancel(); 154 - debug!("Cancelled previous build"); 155 - } 156 - } 86 + let file_watcher_task = tokio::spawn(async move { 87 + let mut dev_server_started = initial_build_success; 88 + let mut dev_server_handle: Option<JoinHandle<()>> = None; 157 89 158 - // Create new cancellation token for this build 159 - let new_cancel_token = CancellationToken::new(); 160 - { 161 - let mut current_cancel = build_cancel_ref.write().await; 162 - *current_cancel = Some(new_cancel_token.clone()); 163 - } 90 + loop { 91 + tokio::select! { 92 + // Handle file system events 93 + result = rx.recv() => { 94 + let Some(result) = result else { 95 + break; // Channel closed 96 + }; 164 97 165 - let start_time = Instant::now(); 166 - 167 - // Run the build command 168 - // TODO: Right now we always run `cargo run`, but for the sake of performance, we should detect in advance 169 - // if the change even needs a full rebuild (e.g. if only content files changed, we can skip rebuilding the Rust binary) 170 - // Perhaps this could be done by parsing the `.d` files that cargo generates. 171 - let child = std::process::Command::new("cargo") 172 - .args(["run", "--quiet"]) 173 - .envs([ 174 - ("MAUDIT_DEV", "true"), 175 - ("MAUDIT_QUIET", "true"), 176 - ("CARGO_TERM_COLOR", "always"), 177 - ("RUSTFLAGS", "-Awarnings"), 178 - ]) 179 - .stdout(std::process::Stdio::inherit()) 180 - .stderr(std::process::Stdio::piped()) 181 - .spawn(); 98 + match result { 99 + Ok(events) => { 100 + // TODO: Handle rescan events, I don't fully understand the implication of them yet 101 + // some issues: 102 + // - https://github.com/notify-rs/notify/issues/434 103 + // - https://github.com/notify-rs/notify/issues/412 182 104 183 - match child { 184 - Ok(child_process) => { 185 - // Spawn the build in a separate task so we can cancel it 186 - let build_task = tokio::task::spawn_blocking(move || { 187 - child_process.wait_with_output() 188 - }); 105 + let should_rebuild = events.iter().any(should_rebuild_for_event); 189 106 190 - // Wait for either process completion or cancellation 191 - let output_result = tokio::select! { 192 - output = build_task => { 193 - match output { 194 - Ok(result) => Some(result), 195 - Err(e) => { 196 - error!(name: "build", "Failed to join build task: {}", e); 197 - None 107 + // If new folder are created or removed, add/remove watches as needed 108 + for event in &events { 109 + if let EventKind::Create(CreateKind::Folder) = event.kind { 110 + for path in &event.paths { 111 + if should_watch_path(path) { 112 + if let Err(e) = debouncer.watch(path, RecursiveMode::Recursive) { 113 + error!(name: "watch", "Failed to add watch for new directory {:?}: {}", path, e); 114 + } else { 115 + info!(name: "watch", "Added watch for new directory {:?}", path); 198 116 } 199 117 } 200 118 } 201 - _ = new_cancel_token.cancelled() => { 202 - debug!("Build was cancelled by new file changes"); 203 - None 204 - } 205 - }; 206 - 207 - // Clear the cancellation token since build is done/cancelled 208 - { 209 - let mut current_cancel = build_cancel_ref.write().await; 210 - *current_cancel = None; 211 119 } 212 120 213 - if let Some(output) = output_result { 214 - match output { 215 - Ok(output) => { 216 - let duration = start_time.elapsed(); 217 - let formatted_elapsed_time = format_elapsed_time( 218 - duration, 219 - &FormatElapsedTimeOptions::default_dev(), 220 - ); 121 + // TODO: This doesn't seem to always work, sometimes removed folders are considered renames (maybe because of trash?), but it's fine I think 122 + if let EventKind::Remove(RemoveKind::Folder) = event.kind { 123 + for path in &event.paths { 124 + if should_watch_path(path) { 125 + if let Err(e) = debouncer.unwatch(path) { 126 + error!(name: "watch", "Failed to remove watch for directory {:?}: {}", path, e); 127 + } else { 128 + info!(name: "watch", "Removed watch for directory {:?}", path); 129 + } 130 + } 131 + } 132 + } 133 + } 221 134 222 - if output.status.success() { 223 - info!(name: "build", "Rebuild finished {}", formatted_elapsed_time); 135 + if should_rebuild { 136 + if !dev_server_started { 137 + // Initial build failed, retry it 138 + info!(name: "watch", "Files changed, retrying initial build..."); 139 + let start_time = Instant::now(); 140 + match build_manager_watcher.do_initial_build().await { 141 + Ok(true) => { 142 + info!(name: "build", "Initial build succeeded! Starting web server..."); 143 + dev_server_started = true; 224 144 225 - // Update status and send success message to browser 226 - let websocket = browser_websocket.clone(); 227 - let status = current_status.clone(); 228 - tokio::spawn(async move { 229 - update_status( 230 - &websocket, status, "success", "", 231 - ) 232 - .await; 233 - }); 234 - } else { 235 - // TODO: It'd be great to somehow be able to get structured errors here (and in the initial build) 236 - // You can get some sort of structured errors from cargo with `--message-format=json`, but: 237 - // - You get an absurd amount of output, including non-error messages, at least when running `cargo run` 238 - // - You don't get the normal human-friendly output anymore, which would be great to have still 239 - // - You can print the rendered output to the console from the JSON, but then you don't have colors 240 - // - It'd only work for rustc errors, not sure how we'd make it work with runtime errors. 241 - // ... So until then, we just send the raw stderr output and hopefully the user can make sense of it. 242 - let stderr = 243 - String::from_utf8_lossy(&output.stderr) 244 - .to_string(); 245 - error!(name: "build", "{}", stderr); 246 - error!(name: "build", "Rebuild failed with errors {}", formatted_elapsed_time); 247 - 248 - // Update status and send error message to browser 249 - let websocket = browser_websocket.clone(); 250 - let status = current_status.clone(); 251 - tokio::spawn(async move { 252 - update_status( 253 - &websocket, status, "error", &stderr, 254 - ) 255 - .await; 256 - }); 257 - } 145 + dev_server_handle = 146 + Some(tokio::spawn(server::start_dev_web_server( 147 + start_time, 148 + sender_websocket_watcher.clone(), 149 + host, 150 + None, 151 + build_manager_watcher.current_status(), 152 + ))); 153 + } 154 + Ok(false) => { 155 + // Still failing, continue waiting 258 156 } 259 157 Err(e) => { 260 - error!(name: "build", "Failed to wait for build process: {}", e); 158 + error!(name: "build", "Failed to retry initial build: {}", e); 261 159 } 262 160 } 161 + } else { 162 + // Normal rebuild - spawn in background so file watcher can continue 163 + info!(name: "watch", "Files changed, rebuilding..."); 164 + let build_manager_clone = build_manager_watcher.clone(); 165 + tokio::spawn(async move { 166 + match build_manager_clone.start_build().await { 167 + Ok(_) => { 168 + // Build completed (success or failure already logged) 169 + } 170 + Err(e) => { 171 + error!(name: "build", "Failed to start build: {}", e); 172 + } 173 + } 174 + }); 263 175 } 264 176 } 265 - Err(e) => { 266 - error!(name: "build", "Failed to spawn build process: {}", e); 177 + } 178 + Err(errors) => { 179 + for error in errors { 180 + error!(name: "watch", "Watch error: {}", error); 267 181 } 268 182 } 269 183 } 270 184 } 271 - Err(errors) => { 272 - for error in errors { 273 - error!("File watch error: {:?}", error); 185 + // Monitor dev server - if it ends, file watcher ends too 186 + _ = async { 187 + if let Some(handle) = &mut dev_server_handle { 188 + handle.await 189 + } else { 190 + std::future::pending().await // Never resolves if no dev server 274 191 } 192 + } => { 193 + break; 275 194 } 276 195 } 277 196 } 278 197 }); 279 198 280 - // Wait for the web server to finish (this will run indefinitely) 281 - web_server_thread.await.unwrap(); 199 + // Wait for either the web server or the file watcher to finish 200 + if let Some(web_server) = web_server_thread { 201 + tokio::select! { 202 + _ = web_server => {}, 203 + _ = file_watcher_task => {}, 204 + } 205 + } else { 206 + // No web server started yet, just wait for file watcher 207 + // If it started the web server, it'll also close itself if the web server ends 208 + file_watcher_task.await.unwrap(); 209 + } 210 + Ok(()) 211 + } 212 + 213 + fn should_rebuild_for_event(event: &DebouncedEvent) -> bool { 214 + event.paths.iter().any(|path| { 215 + should_watch_path(path) 216 + && match event.kind { 217 + // Only rebuild on actual content modifications, not metadata changes 218 + EventKind::Modify(ModifyKind::Data(_)) => true, 219 + EventKind::Modify(ModifyKind::Name(_)) => true, 220 + EventKind::Modify(ModifyKind::Any) => true, 221 + EventKind::Modify(ModifyKind::Other) => true, 222 + // Skip metadata-only changes (permissions, timestamps, etc.) 223 + EventKind::Modify(ModifyKind::Metadata(_)) => false, 224 + // Include file creation and removal 225 + EventKind::Create(_) => true, 226 + EventKind::Remove(_) => true, 227 + // Skip other event types 228 + _ => false, 229 + } 230 + }) 231 + } 232 + 233 + fn should_watch_path(path: &Path) -> bool { 234 + // Skip .DS_Store files 235 + if let Some(file_name) = path.file_name() 236 + && file_name == ".DS_Store" 237 + { 238 + return false; 239 + } 282 240 283 - Ok(()) 241 + // Skip dist and target directories, normally ignored by the watcher, but just in case 242 + if path 243 + .ancestors() 244 + .any(|p| p.ends_with("dist") || p.ends_with("target") || p.ends_with(".git")) 245 + { 246 + return false; 247 + } 248 + 249 + true 284 250 }
+216
crates/maudit-cli/src/dev/build.rs
··· 1 + use cargo_metadata::Message; 2 + use quanta::Instant; 3 + use server::{StatusType, WebSocketMessage, update_status}; 4 + use std::sync::Arc; 5 + use tokio::process::Command; 6 + use tokio::sync::broadcast; 7 + use tokio_util::sync::CancellationToken; 8 + use tracing::{debug, error, info}; 9 + 10 + use crate::{ 11 + dev::server, 12 + logging::{FormatElapsedTimeOptions, format_elapsed_time}, 13 + }; 14 + 15 + #[derive(Clone)] 16 + pub struct BuildManager { 17 + current_cancel: Arc<tokio::sync::RwLock<Option<CancellationToken>>>, 18 + build_semaphore: Arc<tokio::sync::Semaphore>, 19 + websocket_tx: broadcast::Sender<WebSocketMessage>, 20 + current_status: Arc<tokio::sync::RwLock<Option<server::PersistentStatus>>>, 21 + } 22 + 23 + impl BuildManager { 24 + pub fn new(websocket_tx: broadcast::Sender<WebSocketMessage>) -> Self { 25 + Self { 26 + current_cancel: Arc::new(tokio::sync::RwLock::new(None)), 27 + build_semaphore: Arc::new(tokio::sync::Semaphore::new(1)), // Only one build at a time 28 + websocket_tx, 29 + current_status: Arc::new(tokio::sync::RwLock::new(None)), 30 + } 31 + } 32 + 33 + /// Get a reference to the current status for use with the web server 34 + pub fn current_status(&self) -> Arc<tokio::sync::RwLock<Option<server::PersistentStatus>>> { 35 + self.current_status.clone() 36 + } 37 + 38 + /// Do initial build that can be cancelled (but isn't stored as current build) 39 + pub async fn do_initial_build(&self) -> Result<bool, Box<dyn std::error::Error>> { 40 + self.internal_build(true).await 41 + } 42 + 43 + /// Start a new build, cancelling any previous one 44 + pub async fn start_build(&self) -> Result<bool, Box<dyn std::error::Error>> { 45 + self.internal_build(false).await 46 + } 47 + 48 + /// Internal build method that handles both initial and regular builds 49 + async fn internal_build(&self, is_initial: bool) -> Result<bool, Box<dyn std::error::Error>> { 50 + // Cancel any existing build immediately 51 + let cancel = CancellationToken::new(); 52 + { 53 + let mut current_cancel = self.current_cancel.write().await; 54 + if let Some(old_cancel) = current_cancel.replace(cancel.clone()) { 55 + old_cancel.cancel(); 56 + } 57 + } 58 + 59 + // Acquire semaphore to ensure only one build runs at a time 60 + // This prevents resource conflicts if cancellation fails 61 + let _ = self.build_semaphore.acquire().await?; 62 + 63 + // Notify that build is starting 64 + update_status( 65 + &self.websocket_tx, 66 + self.current_status.clone(), 67 + StatusType::Info, 68 + "Building...", 69 + ) 70 + .await; 71 + 72 + let mut child = Command::new("cargo") 73 + .args([ 74 + "run", 75 + "--quiet", 76 + "--message-format", 77 + "json-diagnostic-rendered-ansi", 78 + ]) 79 + .envs([ 80 + ("MAUDIT_DEV", "true"), 81 + ("MAUDIT_QUIET", "true"), 82 + ("CARGO_TERM_COLOR", "always"), 83 + ]) 84 + .stdout(std::process::Stdio::piped()) 85 + .stderr(std::process::Stdio::piped()) 86 + .spawn()?; 87 + 88 + // Take the stderr stream for manual handling 89 + let mut stdout = child.stdout.take().unwrap(); 90 + let mut stderr = child.stderr.take().unwrap(); 91 + 92 + let websocket_tx = self.websocket_tx.clone(); 93 + let current_status = self.current_status.clone(); 94 + let build_start_time = Instant::now(); 95 + 96 + // Create a channel to get the build result back 97 + let (result_tx, mut result_rx) = tokio::sync::mpsc::channel::<bool>(1); 98 + 99 + // Spawn watcher task to monitor the child process 100 + tokio::spawn(async move { 101 + let output_future = async { 102 + // Read stdout concurrently with waiting for process to finish 103 + let stdout_task = tokio::spawn(async move { 104 + let mut out = Vec::new(); 105 + tokio::io::copy(&mut stdout, &mut out).await.unwrap_or(0); 106 + 107 + let mut rendered_messages: Vec<String> = Vec::new(); 108 + 109 + // Ideally we'd stream things as they come, but I can't figure it out 110 + for message in cargo_metadata::Message::parse_stream( 111 + String::from_utf8_lossy(&out).to_string().as_bytes(), 112 + ) { 113 + match message { 114 + Err(e) => { 115 + error!(name: "build", "Failed to parse cargo message: {}", e); 116 + continue; 117 + } 118 + Ok(message) => { 119 + match message { 120 + // Compiler wants to tell us something 121 + Message::CompilerMessage(msg) => { 122 + // TODO: For now, just send through the rendered messages, but in the future let's send 123 + // structured messages to the frontend so we can do better formatting 124 + if let Some(rendered) = &msg.message.rendered { 125 + info!("{}", rendered); 126 + rendered_messages.push(rendered.to_string()); 127 + } 128 + } 129 + // Random text came in, just log it 130 + Message::TextLine(msg) => { 131 + info!("{}", msg); 132 + } 133 + _ => {} 134 + } 135 + } 136 + } 137 + } 138 + 139 + (out, rendered_messages) 140 + }); 141 + 142 + let stderr_task = tokio::spawn(async move { 143 + let mut err = Vec::new(); 144 + tokio::io::copy(&mut stderr, &mut err).await.unwrap_or(0); 145 + 146 + err 147 + }); 148 + 149 + let status = child.wait().await?; 150 + let stdout_data = stdout_task.await.unwrap_or_default(); 151 + let stderr_data = stderr_task.await.unwrap_or_default(); 152 + 153 + Ok::<(std::process::Output, Vec<String>), Box<dyn std::error::Error + Send + Sync>>( 154 + ( 155 + std::process::Output { 156 + status, 157 + stdout: stdout_data.0, 158 + stderr: stderr_data, 159 + }, 160 + stdout_data.1, 161 + ), 162 + ) 163 + }; 164 + 165 + tokio::select! { 166 + _ = cancel.cancelled() => { 167 + debug!(name: "build", "Build cancelled"); 168 + let _ = child.kill().await; 169 + update_status(&websocket_tx, current_status, StatusType::Info, "Build cancelled").await; 170 + let _ = result_tx.send(false).await; // Build failed due to cancellation 171 + } 172 + res = output_future => { 173 + let duration = build_start_time.elapsed(); 174 + let formatted_elapsed_time = format_elapsed_time( 175 + duration, 176 + &FormatElapsedTimeOptions::default_dev(), 177 + ); 178 + 179 + let success = match res { 180 + Ok(output) => { 181 + let (output, rendered_messages) = output; 182 + if output.status.success() { 183 + let build_type = if is_initial { "Initial build" } else { "Rebuild" }; 184 + info!(name: "build", "{} finished {}", build_type, formatted_elapsed_time); 185 + update_status(&websocket_tx, current_status, StatusType::Success, "Build finished successfully").await; 186 + true 187 + } else { 188 + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); 189 + println!("{}", stderr); // Raw stderr sometimes has something to say whenever cargo fails, even if the errors messages are actually in stdout 190 + let build_type = if is_initial { "Initial build" } else { "Rebuild" }; 191 + error!(name: "build", "{} failed with errors {}", build_type, formatted_elapsed_time); 192 + if is_initial { 193 + error!(name: "build", "Initial build needs to succeed before we can start the dev server"); 194 + update_status(&websocket_tx, current_status, StatusType::Error, "Initial build failed - fix errors and save to retry").await; 195 + } else { 196 + update_status(&websocket_tx, current_status, StatusType::Error, &rendered_messages.join("\n")).await; 197 + } 198 + false 199 + } 200 + } 201 + Err(e) => { 202 + error!(name: "build", "Failed to wait for build: {}", e); 203 + update_status(&websocket_tx, current_status, StatusType::Error, &format!("Failed to wait for build: {}", e)).await; 204 + false 205 + } 206 + }; 207 + let _ = result_tx.send(success).await; 208 + } 209 + } 210 + }); 211 + 212 + // Wait for the build result 213 + let success = result_rx.recv().await.unwrap_or(false); 214 + Ok(success) 215 + } 216 + }
-21
crates/maudit-cli/src/dev/filterer.rs
··· 1 - use std::path::Path; 2 - 3 - /// Simple file path filter for the dev server 4 - pub fn should_watch_path(path: &Path) -> bool { 5 - // Skip .DS_Store files 6 - if let Some(file_name) = path.file_name() 7 - && file_name == ".DS_Store" 8 - { 9 - return false; 10 - } 11 - 12 - // Skip dist and target directories 13 - if path 14 - .ancestors() 15 - .any(|p| p.ends_with("dist") || p.ends_with("target")) 16 - { 17 - return false; 18 - } 19 - 20 - true 21 - }
+89 -32
crates/maudit-cli/src/dev/server.rs
··· 5 5 Request, State, 6 6 ws::{Message, WebSocket, WebSocketUpgrade}, 7 7 }, 8 - handler::HandlerWithoutStateExt, 9 - http::{HeaderValue, StatusCode}, 8 + http::{HeaderValue, StatusCode, Uri}, 10 9 middleware::{self, Next}, 11 10 response::{IntoResponse, Response}, 12 11 routing::get, 13 12 }; 14 13 use quanta::Instant; 15 14 use serde_json::json; 16 - use tokio::{net::TcpSocket, signal, sync::broadcast}; 17 - use tracing::{Level, debug}; 15 + use tokio::{ 16 + net::TcpSocket, 17 + signal, 18 + sync::{RwLock, broadcast}, 19 + }; 20 + use tracing::{Level, debug, warn}; 18 21 19 22 use std::net::{IpAddr, SocketAddr}; 20 23 use std::sync::Arc; ··· 36 39 pub data: String, 37 40 } 38 41 42 + #[derive(Clone, Debug)] 43 + pub enum StatusType { 44 + Success, 45 + Info, 46 + Error, 47 + } 48 + 49 + impl std::fmt::Display for StatusType { 50 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 51 + match self { 52 + StatusType::Success => write!(f, "success"), 53 + StatusType::Info => write!(f, "info"), 54 + StatusType::Error => write!(f, "error"), 55 + } 56 + } 57 + } 58 + 59 + // Persistent state for new connections 60 + #[derive(Clone, Debug)] 61 + pub struct PersistentStatus { 62 + pub status_type: StatusType, // Only Success or Error 63 + pub message: String, 64 + } 65 + 39 66 #[derive(Clone)] 40 67 struct AppState { 41 68 tx: broadcast::Sender<WebSocketMessage>, 42 - current_status: Arc<tokio::sync::RwLock<Option<String>>>, 69 + current_status: Arc<RwLock<Option<PersistentStatus>>>, 43 70 } 44 71 45 - fn inject_live_reload_script(html_content: &str, socket_addr: SocketAddr, host: bool) -> String { 46 - let mut content = html_content.to_string(); 47 - 72 + fn inject_live_reload_script( 73 + uri: &Uri, 74 + html_content: &str, 75 + socket_addr: SocketAddr, 76 + host: bool, 77 + ) -> String { 48 78 let script_content = include_str!(concat!(env!("OUT_DIR"), "/js/client.js")).replace( 49 79 "{SERVER_ADDRESS}", 50 80 &format!( ··· 58 88 ), 59 89 ); 60 90 61 - content.push_str(&format!("<script>{script_content}</script>")); 62 - content 91 + // Only inject script if content looks like proper HTML 92 + if html_content.trim_start().starts_with("<!DOCTYPE") 93 + || html_content.trim_start().starts_with("<html") 94 + { 95 + format!("{}<script>{}</script>", html_content, script_content) 96 + } else { 97 + warn!( 98 + "{} matched an HTML response, but it does not look like proper HTML, live-reload won't work. Make sure your HTML has a proper <!DOCTYPE> or <html> tag.", 99 + uri 100 + ); 101 + // Not proper HTML, return content unchanged 102 + html_content.to_string() 103 + } 63 104 } 64 105 65 106 pub async fn start_dev_web_server( ··· 67 108 tx: broadcast::Sender<WebSocketMessage>, 68 109 host: bool, 69 110 initial_error: Option<String>, 70 - current_status: Arc<tokio::sync::RwLock<Option<String>>>, 111 + current_status: Arc<RwLock<Option<PersistentStatus>>>, 71 112 ) { 72 113 // TODO: The dist dir should be configurable 73 114 let dist_dir = "dist"; ··· 76 117 if let Some(error) = initial_error { 77 118 let _ = tx.send(WebSocketMessage { 78 119 data: json!({ 79 - "type": "error", 120 + "type": StatusType::Error.to_string(), 80 121 "message": error 81 122 }) 82 123 .to_string(), 83 124 }); 84 125 } 85 126 86 - async fn handle_404(socket_addr: SocketAddr, host: bool, dist_dir: &str) -> impl IntoResponse { 127 + async fn handle_404( 128 + uri: Uri, 129 + socket_addr: SocketAddr, 130 + host: bool, 131 + dist_dir: &str, 132 + ) -> impl IntoResponse { 87 133 let content = match fs::read_to_string(format!("{}/404.html", dist_dir)).await { 88 134 Ok(custom_content) => custom_content, 89 135 Err(_) => include_str!("./404.html").to_string(), ··· 92 138 ( 93 139 StatusCode::NOT_FOUND, 94 140 [(header::CONTENT_TYPE, "text/html; charset=utf-8")], 95 - inject_live_reload_script(&content, socket_addr, host), 141 + inject_live_reload_script(&uri, &content, socket_addr, host), 96 142 ) 97 143 .into_response() 98 144 } ··· 105 151 }; 106 152 let port = find_open_port(&addr, 1864).await; 107 153 let socket = TcpSocket::new_v4().unwrap(); 154 + let _ = socket.set_reuseaddr(true); 155 + let _ = socket.set_reuseport(true); 108 156 109 157 let socket_addr = SocketAddr::new(addr, port); 110 158 socket.bind(socket_addr).unwrap(); ··· 113 161 114 162 debug!("listening on {}", listener.local_addr().unwrap()); 115 163 116 - let service = (move || handle_404(socket_addr, host, dist_dir)).into_service(); 117 - let serve_dir = ServeDir::new(dist_dir).not_found_service(service); 164 + let serve_dir = 165 + ServeDir::new(dist_dir).not_found_service(axum::routing::any(move |uri: Uri| async move { 166 + handle_404(uri, socket_addr, host, dist_dir).await 167 + })); 118 168 119 169 // TODO: Return a `.well-known/appspecific/com.chrome.devtools.json` for Chrome 120 170 ··· 156 206 157 207 pub async fn update_status( 158 208 tx: &broadcast::Sender<WebSocketMessage>, 159 - current_status: Arc<tokio::sync::RwLock<Option<String>>>, 160 - status_type: &str, 209 + current_status: Arc<RwLock<Option<PersistentStatus>>>, 210 + status_type: StatusType, 161 211 message: &str, 162 212 ) { 163 - let status_message = if status_type == "success" { 164 - None // Clear the status on success 165 - } else { 166 - Some(message.to_string()) 213 + // Only store persistent states (Success clears errors, Error stores the error) 214 + let persistent_status = match status_type { 215 + StatusType::Success => None, // Clear any error state 216 + StatusType::Error => Some(PersistentStatus { 217 + status_type: StatusType::Error, 218 + message: message.to_string(), 219 + }), 220 + // Everything else just keeps the current state 221 + _ => { 222 + let status = current_status.read().await; 223 + status.clone() // Keep existing persistent state 224 + } 167 225 }; 168 226 169 227 // Update the stored status 170 228 { 171 229 let mut status = current_status.write().await; 172 - *status = status_message; 230 + *status = persistent_status; 173 231 } 174 232 175 - // Send the message 233 + // Send the message to all connected clients 176 234 let _ = tx.send(WebSocketMessage { 177 235 data: json!({ 178 - "type": status_type, 236 + "type": status_type.to_string(), 179 237 "message": message 180 238 }) 181 239 .to_string(), ··· 201 259 202 260 let body = String::from_utf8_lossy(&bytes).into_owned(); 203 261 204 - // TODO: Handle HTML documents with no tags, e.g. `"Hello, world"`. Appending a raw script tag will cause it to show up as text. 205 - let body_with_script = inject_live_reload_script(&body, socket_addr, host); 262 + let body_with_script = inject_live_reload_script(&uri, &body, socket_addr, host); 206 263 207 264 // Copy the headers from the original response 208 265 let mut res = Response::new(body_with_script.into()); ··· 231 288 socket: WebSocket, 232 289 who: SocketAddr, 233 290 tx: broadcast::Sender<WebSocketMessage>, 234 - current_status: Arc<tokio::sync::RwLock<Option<String>>>, 291 + current_status: Arc<RwLock<Option<PersistentStatus>>>, 235 292 ) { 236 293 let (mut sender, mut receiver) = socket.split(); 237 294 238 - // Send current status to new connection if there is one 295 + // Send current persistent status to new connection if there is one 239 296 { 240 297 let status = current_status.read().await; 241 - if let Some(error_message) = status.as_ref() { 298 + if let Some(persistent_status) = status.as_ref() { 242 299 let _ = sender 243 300 .send(Message::Text( 244 301 json!({ 245 - "type": "error", 246 - "message": error_message 302 + "type": persistent_status.status_type.to_string(), 303 + "message": persistent_status.message 247 304 }) 248 305 .to_string() 249 306 .into(),
+3 -1
crates/maudit-cli/src/preview/server.rs
··· 42 42 IpAddr::from([127, 0, 0, 1]) 43 43 }; 44 44 45 - let port = find_open_port(&addr, 3000).await; 45 + let port = find_open_port(&addr, 1864).await; 46 46 let socket = TcpSocket::new_v4().unwrap(); 47 + let _ = socket.set_reuseaddr(true); 48 + let _ = socket.set_reuseport(true); 47 49 48 50 let socket_addr = SocketAddr::new(addr, port); 49 51 socket.bind(socket_addr).unwrap();
+1 -1
crates/maudit/Cargo.toml
··· 46 46 thiserror = "2.0.9" 47 47 oxc_sourcemap = "4.1.0" 48 48 rayon = "1.11.0" 49 - xxhash-rust = "0.8.15" 49 + xxhash-rust = "0.8.15"