From 7b6302b3b2e24347ebb60db2627c4785ed1fc93b Mon Sep 17 00:00:00 2001 From: Nathan Gill Date: Fri, 16 Jan 2026 14:11:26 +0000 Subject: [PATCH] feat(serve): add simple automatic reload using server sent events --- Cargo.lock | 23 +++++++++++++++++++++++ Cargo.toml | 1 + src/build.rs | 10 +++++++--- src/main.rs | 4 ++-- src/serve.rs | 35 ++++++++++++++++++++++++++++++++--- 5 files changed, 65 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 322f871..37a02f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,6 +67,28 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -994,6 +1016,7 @@ name = "tars-bin" version = "0.4.0" dependencies = [ "anyhow", + "async-stream", "chrono", "clap", "fs_extra", diff --git a/Cargo.toml b/Cargo.toml index ca0a265..12a01e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ path = "src/main.rs" [dependencies] anyhow = "1.0.100" +async-stream = "0.3.6" chrono = { version = "0.4.42", features = [ "serde" ] } clap = "4.5.51" fs_extra = "1.3.0" diff --git a/src/build.rs b/src/build.rs index 1f1b472..2e499c6 100644 --- a/src/build.rs +++ b/src/build.rs @@ -28,10 +28,11 @@ pub struct Builder<'a> { tmp_dir: Option, built: bool, no_verify: bool, + extra_content: Option<&'a str>, } impl<'a> Builder<'a> { - pub fn new(config: &'a Config, no_verify: bool) -> Self { + pub fn new(config: &'a Config, no_verify: bool, extra_content: Option<&'a str>) -> Self { Self { template_env: TemplateEnvironment::new(), pages: Vec::new(), @@ -44,6 +45,7 @@ impl<'a> Builder<'a> { tmp_dir: None, built: false, no_verify: no_verify | config.build.no_verify, + extra_content, } } @@ -156,11 +158,13 @@ impl<'a> Builder<'a> { let mut dst_path = self.build_root.join(&page.rel_path); dst_path.set_extension("html"); + let extra_str = self.extra_content.unwrap_or(""); + if let Some(tmpl_name) = &page.meta.template { let render_str = self.template_env.render_template(&ctx, tmpl_name)?; - write(&dst_path, render_str)?; + write(&dst_path, format!("{}{}", render_str, extra_str))?; } else { - write(&dst_path, &page.content)?; + write(&dst_path, format!("{}{}", &page.content, extra_str))?; } println!("Generated {}", &page.rel_path.display()); diff --git a/src/main.rs b/src/main.rs index cf0d22b..b6a094c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -79,7 +79,7 @@ async fn main() { } TarsSubcommand::Build(args) => { let config = load_config(&args.config); - let mut builder = Builder::new(&config, args.no_verify); + let mut builder = Builder::new(&config, args.no_verify, None); if let Err(e) = builder.build() { println!("{e}"); @@ -88,7 +88,7 @@ async fn main() { } TarsSubcommand::Clean(args) => { let config = load_config(&args.config); - let builder = Builder::new(&config, false); + let builder = Builder::new(&config, false, None); if let Err(e) = builder.clean() { println!("{e}"); diff --git a/src/serve.rs b/src/serve.rs index 1bfcb36..4e40d7a 100644 --- a/src/serve.rs +++ b/src/serve.rs @@ -1,16 +1,27 @@ use anyhow::Result; use notify::{Event, EventKind, RecursiveMode, Watcher}; +use std::convert::Infallible; use std::net::SocketAddr; use std::sync::Arc; use std::{path::Path, sync::mpsc}; +use warp::Filter; use crate::{build::Builder, config::Config}; +const RELOAD_JS: &str = " + +"; + pub async fn run_server(config: Arc) -> Result<()> { let socket_str = format!("{}:{}", config.serve.host, config.serve.port); let socket_addr: SocketAddr = socket_str.parse()?; let build_dir = config.build.build_dir.clone(); + let (refresh_tx, _) = tokio::sync::broadcast::channel::<()>(16); + let refresh_sse = refresh_tx.clone(); let (notify_tx, notify_rx) = mpsc::channel::>(); let mut watcher = notify::recommended_watcher(notify_tx)?; watcher.watch( @@ -26,8 +37,8 @@ pub async fn run_server(config: Arc) -> Result<()> { RecursiveMode::Recursive, )?; - tokio::spawn(async move { - let mut builder = Builder::new(&config, false); + std::thread::spawn(move || { + let mut builder = Builder::new(&config, false, Some(RELOAD_JS)); println!("Building..."); if let Err(e) = builder.build() { println!("Build error: {e}"); @@ -41,6 +52,7 @@ pub async fn run_server(config: Arc) -> Result<()> { if let Err(e) = builder.rebuild() { println!("Build error: {e}"); } + let _ = refresh_tx.send(()); } _ => {} }, @@ -53,7 +65,24 @@ pub async fn run_server(config: Arc) -> Result<()> { println!("Running server on http://{socket_addr}"); - warp::serve(warp::fs::dir(build_dir)).run(socket_addr).await; + let sse = warp::path("__tars_reload__").and(warp::get()).map(move || { + let mut rx = refresh_sse.subscribe(); + + let stream = async_stream::stream! { + loop { + if rx.recv().await.is_ok() { + yield Ok::<_, Infallible>( + warp::sse::Event::default().data("reload") + ); + } + } + }; + + warp::sse::reply(warp::sse::keep_alive().stream(stream)) + }); + + let routes = warp::fs::dir(build_dir).or(sse); + warp::serve(routes).run(socket_addr).await; Ok(()) }