From 97cbd2df9f68ec56d4da2d8b276bc8197a66e730 Mon Sep 17 00:00:00 2001 From: Jane Chu <7559015+janechu@users.noreply.github.com> Date: Mon, 23 Feb 2026 14:05:07 -0800 Subject: [PATCH 1/2] chore: extract shared render, watcher, and HMR modules Move render.rs, watcher.rs, and HMR version logic into examples/shared/rust/ so both tiny_http and hyper integration examples reference a single copy via #[path] includes. Co-Authored-By: Claude Opus 4.6 --- examples/integration/hyper/src/main.rs | 4 + examples/integration/hyper/src/routes/hmr.rs | 21 +---- examples/integration/tiny_http/src/main.rs | 4 + examples/integration/tiny_http/src/render.rs | 61 ------------ .../integration/tiny_http/src/routes/hmr.rs | 28 +----- examples/integration/tiny_http/src/watcher.rs | 92 ------------------- examples/shared/rust/hmr.rs | 25 +++++ .../hyper/src => shared/rust}/render.rs | 0 .../hyper/src => shared/rust}/watcher.rs | 0 9 files changed, 40 insertions(+), 195 deletions(-) delete mode 100644 examples/integration/tiny_http/src/render.rs delete mode 100644 examples/integration/tiny_http/src/watcher.rs create mode 100644 examples/shared/rust/hmr.rs rename examples/{integration/hyper/src => shared/rust}/render.rs (100%) rename examples/{integration/hyper/src => shared/rust}/watcher.rs (100%) diff --git a/examples/integration/hyper/src/main.rs b/examples/integration/hyper/src/main.rs index 1e5492ce..ad3eea75 100644 --- a/examples/integration/hyper/src/main.rs +++ b/examples/integration/hyper/src/main.rs @@ -14,8 +14,11 @@ use tokio::net::TcpListener; #[path = "../../../../examples/shared/rust/config.rs"] mod config; +#[path = "../../../../examples/shared/rust/hmr.rs"] +mod hmr; #[path = "../../../../examples/shared/rust/output.rs"] mod output; +#[path = "../../../../examples/shared/rust/render.rs"] mod render; mod routes { pub mod assets; @@ -23,6 +26,7 @@ mod routes { pub mod index; pub mod not_found; } +#[path = "../../../../examples/shared/rust/watcher.rs"] mod watcher; use crate::config::AppPaths; diff --git a/examples/integration/hyper/src/routes/hmr.rs b/examples/integration/hyper/src/routes/hmr.rs index a41e304f..7d5715b3 100644 --- a/examples/integration/hyper/src/routes/hmr.rs +++ b/examples/integration/hyper/src/routes/hmr.rs @@ -1,31 +1,14 @@ -use std::fs; -use std::time::SystemTime; - use bytes::Bytes; use http_body_util::Full; use hyper::{Response, StatusCode}; use crate::config::AppPaths; +use crate::hmr::hmr_version; /// HMR endpoint returning a version derived from the latest modification time /// of the template or data file. The client polls this to detect changes. pub fn handle_hmr(paths: &AppPaths) -> Response> { - let template_mtime = fs::metadata(&paths.template) - .and_then(|m| m.modified()) - .ok(); - let data_mtime = fs::metadata(&paths.data).and_then(|m| m.modified()).ok(); - - let latest: Option = match (template_mtime, data_mtime) { - (Some(t), Some(d)) => Some(if t > d { t } else { d }), - (Some(t), None) => Some(t), - (None, Some(d)) => Some(d), - (None, None) => None, - }; - - let version_str = latest - .and_then(|t| t.duration_since(SystemTime::UNIX_EPOCH).ok()) - .map(|d| d.as_millis().to_string()) - .unwrap_or_else(|| "0".to_string()); + let version_str = hmr_version(&paths.template, &paths.data); Response::builder() .status(StatusCode::OK) diff --git a/examples/integration/tiny_http/src/main.rs b/examples/integration/tiny_http/src/main.rs index be15b5d0..46c7289d 100644 --- a/examples/integration/tiny_http/src/main.rs +++ b/examples/integration/tiny_http/src/main.rs @@ -7,8 +7,11 @@ use tiny_http::Server; #[path = "../../../../examples/shared/rust/config.rs"] mod config; +#[path = "../../../../examples/shared/rust/hmr.rs"] +mod hmr; #[path = "../../../../examples/shared/rust/output.rs"] mod output; +#[path = "../../../../examples/shared/rust/render.rs"] mod render; mod routes { pub mod assets; @@ -16,6 +19,7 @@ mod routes { pub mod index; pub mod not_found; } +#[path = "../../../../examples/shared/rust/watcher.rs"] mod watcher; use crate::config::AppPaths; diff --git a/examples/integration/tiny_http/src/render.rs b/examples/integration/tiny_http/src/render.rs deleted file mode 100644 index 3f4963b8..00000000 --- a/examples/integration/tiny_http/src/render.rs +++ /dev/null @@ -1,61 +0,0 @@ -use std::fs; - -use anyhow::{Context, Result}; -use serde_json::Value; -use webui_handler::{ResponseWriter, WebUIHandler}; -use webui_parser::HtmlParser; -use webui_protocol::WebUIProtocol; - -use crate::config::AppPaths; - -struct MemoryWriter { - content: String, -} - -impl MemoryWriter { - fn new() -> Self { - Self { - content: String::new(), - } - } -} - -impl ResponseWriter for MemoryWriter { - fn write(&mut self, content: &str) -> webui_handler::Result<()> { - self.content.push_str(content); - Ok(()) - } - - fn end(&mut self) -> webui_handler::Result<()> { - Ok(()) - } -} - -pub fn render_to_index_html(paths: &AppPaths) -> Result<()> { - // Load and parse the template into a WebUI protocol - let template = fs::read_to_string(&paths.template) - .with_context(|| format!("Failed to read template: {}", paths.template.display()))?; - let mut parser = HtmlParser::new(); - parser.parse("index.html", &template)?; - let fragments = parser.into_fragment_records(); - let protocol = WebUIProtocol { fragments }; - - // Load the state from state.json - let state_json = fs::read_to_string(&paths.data) - .with_context(|| format!("Failed to read state: {}", paths.data.display()))?; - let state: Value = serde_json::from_str(&state_json).context("Failed to parse state JSON")?; - - // Render into an in-memory buffer - let mut writer = MemoryWriter::new(); - let handler = WebUIHandler::new(); - handler.handle(&protocol, &state, &mut writer)?; - - // Ensure the dist directory exists and write to dist/index.html - fs::create_dir_all(&paths.dist_dir) - .with_context(|| format!("Failed to create dist dir: {}", paths.dist_dir.display()))?; - let output_path = paths.dist_dir.join("index.html"); - fs::write(&output_path, &writer.content) - .with_context(|| format!("Failed to write {}", output_path.display()))?; - - Ok(()) -} diff --git a/examples/integration/tiny_http/src/routes/hmr.rs b/examples/integration/tiny_http/src/routes/hmr.rs index 6156576c..3f8be1a3 100644 --- a/examples/integration/tiny_http/src/routes/hmr.rs +++ b/examples/integration/tiny_http/src/routes/hmr.rs @@ -1,31 +1,13 @@ -use std::fs; -use std::time::SystemTime; - use tiny_http::{Request, Response, StatusCode}; use crate::config::AppPaths; +use crate::hmr::hmr_version; -// HMR route which will refresh when the template or data file changes -// Returns a version derived from the latest modification time of -// template.html or data.json, so no shared counter is needed. +/// HMR route which will refresh when the template or data file changes. +/// Returns a version derived from the latest modification time of +/// template.html or data.json, so no shared counter is needed. pub fn handle_hmr(request: Request, paths: &AppPaths) { - let template_mtime = fs::metadata(&paths.template) - .and_then(|m| m.modified()) - .ok(); - let data_mtime = fs::metadata(&paths.data).and_then(|m| m.modified()).ok(); - - let latest: Option = match (template_mtime, data_mtime) { - (Some(t), Some(d)) => Some(if t > d { t } else { d }), - (Some(t), None) => Some(t), - (None, Some(d)) => Some(d), - (None, None) => None, - }; - - let version_str = latest - .and_then(|t| t.duration_since(SystemTime::UNIX_EPOCH).ok()) - .map(|d| d.as_millis().to_string()) - .unwrap_or_else(|| "0".to_string()); - + let version_str = hmr_version(&paths.template, &paths.data); let response = Response::from_string(version_str).with_status_code(StatusCode(200)); let _ = request.respond(response); } diff --git a/examples/integration/tiny_http/src/watcher.rs b/examples/integration/tiny_http/src/watcher.rs deleted file mode 100644 index dfb65257..00000000 --- a/examples/integration/tiny_http/src/watcher.rs +++ /dev/null @@ -1,92 +0,0 @@ -use std::collections::HashMap; -use std::fs; -use std::path::{Path, PathBuf}; -use std::thread; -use std::time::{Duration, SystemTime}; - -use crate::config::AppPaths; -use crate::render::render_to_index_html; - -fn collect_file_times(dir_path: &Path) -> HashMap { - let mut file_times = HashMap::new(); - - if let Ok(entries) = fs::read_dir(dir_path) { - for entry in entries.flatten() { - let path = entry.path(); - if path.is_file() { - if let Ok(metadata) = fs::metadata(&path) { - if let Ok(modified) = metadata.modified() { - file_times.insert(path, modified); - } - } - } else if path.is_dir() { - // Recursively collect file times from subdirectories - file_times.extend(collect_file_times(&path)); - } - } - } - - file_times -} - -pub fn start_file_watcher(paths: AppPaths) { - thread::spawn(move || { - let watch_dirs = paths.watch_dirs(); - let mut last_file_times: HashMap = HashMap::new(); - - // Initialize file times - for dir in &watch_dirs { - if dir.exists() { - last_file_times.extend(collect_file_times(dir)); - } - } - - loop { - let mut current_file_times: HashMap = HashMap::new(); - - // Collect current file modification times - for dir in &watch_dirs { - if dir.exists() { - current_file_times.extend(collect_file_times(dir)); - } - } - - // Check for changes - let mut files_changed = false; - - // Check for modified or new files - for (path, current_time) in ¤t_file_times { - if let Some(&last_time) = last_file_times.get(path) { - if *current_time != last_time { - files_changed = true; - break; - } - } else { - // New file - files_changed = true; - break; - } - } - - // Check for deleted files - if !files_changed { - for path in last_file_times.keys() { - if !current_file_times.contains_key(path) { - files_changed = true; - break; - } - } - } - - if files_changed { - if let Err(err) = render_to_index_html(&paths) { - eprintln!("Failed to re-render index.html: {err}"); - } - - last_file_times = current_file_times; - } - - thread::sleep(Duration::from_millis(500)); - } - }); -} diff --git a/examples/shared/rust/hmr.rs b/examples/shared/rust/hmr.rs new file mode 100644 index 00000000..87a4ae52 --- /dev/null +++ b/examples/shared/rust/hmr.rs @@ -0,0 +1,25 @@ +use std::fs; +use std::path::Path; +use std::time::SystemTime; + +/// Compute an HMR version string from the latest modification time +/// of the template and data files. Returns a millisecond timestamp +/// or `"0"` if neither file has a retrievable modification time. +pub fn hmr_version(template: &Path, data: &Path) -> String { + let template_mtime = fs::metadata(template) + .and_then(|m| m.modified()) + .ok(); + let data_mtime = fs::metadata(data).and_then(|m| m.modified()).ok(); + + let latest: Option = match (template_mtime, data_mtime) { + (Some(t), Some(d)) => Some(if t > d { t } else { d }), + (Some(t), None) => Some(t), + (None, Some(d)) => Some(d), + (None, None) => None, + }; + + latest + .and_then(|t| t.duration_since(SystemTime::UNIX_EPOCH).ok()) + .map(|d| d.as_millis().to_string()) + .unwrap_or_else(|| "0".to_string()) +} diff --git a/examples/integration/hyper/src/render.rs b/examples/shared/rust/render.rs similarity index 100% rename from examples/integration/hyper/src/render.rs rename to examples/shared/rust/render.rs diff --git a/examples/integration/hyper/src/watcher.rs b/examples/shared/rust/watcher.rs similarity index 100% rename from examples/integration/hyper/src/watcher.rs rename to examples/shared/rust/watcher.rs From 2bc942dce2b64b87102d5b601c05f398a4eb6f90 Mon Sep 17 00:00:00 2001 From: Jane Chu <7559015+janechu@users.noreply.github.com> Date: Mon, 23 Feb 2026 15:04:53 -0800 Subject: [PATCH 2/2] allow http/1.1 on hyper to be assigned automatically for local development --- examples/integration/hyper/Cargo.lock | 16 ++++++++++++++++ examples/integration/hyper/Cargo.toml | 4 ++-- examples/integration/hyper/README.md | 2 +- examples/integration/hyper/src/main.rs | 4 ++-- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/examples/integration/hyper/Cargo.lock b/examples/integration/hyper/Cargo.lock index 57c52df0..a7b0c801 100644 --- a/examples/integration/hyper/Cargo.lock +++ b/examples/integration/hyper/Cargo.lock @@ -334,6 +334,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + [[package]] name = "httpdate" version = "1.0.3" @@ -346,14 +352,18 @@ version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ + "atomic-waker", "bytes", "futures-channel", "futures-core", "h2", "http", "http-body", + "httparse", "httpdate", + "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", ] @@ -533,6 +543,12 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "prettyplease" version = "0.2.37" diff --git a/examples/integration/hyper/Cargo.toml b/examples/integration/hyper/Cargo.toml index 18cbaffa..0e32f374 100644 --- a/examples/integration/hyper/Cargo.toml +++ b/examples/integration/hyper/Cargo.toml @@ -12,8 +12,8 @@ serde_json = "1.0" webui-parser = { path = "../../../crates/webui-parser" } webui-protocol = { path = "../../../crates/webui-protocol" } webui-handler = { path = "../../../crates/webui-handler" } -hyper = { version = "1", features = ["http2", "server"] } -hyper-util = { version = "0.1", features = ["tokio"] } +hyper = { version = "1", features = ["http1", "http2", "server"] } +hyper-util = { version = "0.1", features = ["http1", "http2", "server-auto", "tokio"] } http-body-util = "0.1" tokio = { version = "1", features = ["full"] } bytes = "1" diff --git a/examples/integration/hyper/README.md b/examples/integration/hyper/README.md index 580ca5d4..33bea2d8 100644 --- a/examples/integration/hyper/README.md +++ b/examples/integration/hyper/README.md @@ -7,7 +7,7 @@ writes the result to `dist/index.html`, and serves it over HTTP at `http://127.0 ## Performance highlights -- **HTTP/2 only** — multiplexed streams, header compression, and binary framing via `hyper-util`'s `http2::Builder` +- **HTTP/1.1 + HTTP/2** — auto-negotiates per connection via `hyper-util`'s `auto::Builder`; browsers use HTTP/1.1 over plaintext while HTTP/2 clients can use h2c - **hyper 1.x** — zero-copy HTTP parsing with minimal allocations - **`Bytes` bodies** — reference-counted buffers avoid copying response data - **`Full`** — lightweight body type with no boxing overhead diff --git a/examples/integration/hyper/src/main.rs b/examples/integration/hyper/src/main.rs index ad3eea75..175e4734 100644 --- a/examples/integration/hyper/src/main.rs +++ b/examples/integration/hyper/src/main.rs @@ -6,10 +6,10 @@ use anyhow::{Context, Result}; use bytes::Bytes; use clap::Parser; use http_body_util::Full; -use hyper::server::conn::http2; use hyper::service::service_fn; use hyper::{Request, Response}; use hyper_util::rt::{TokioExecutor, TokioIo}; +use hyper_util::server::conn::auto; use tokio::net::TcpListener; #[path = "../../../../examples/shared/rust/config.rs"] @@ -99,7 +99,7 @@ async fn run(cli: &Cli) -> Result<()> { let paths = Arc::clone(&paths); tokio::task::spawn(async move { - if let Err(err) = http2::Builder::new(TokioExecutor::new()) + if let Err(err) = auto::Builder::new(TokioExecutor::new()) .serve_connection( io, service_fn(|req| handle_request(req, Arc::clone(&paths))),