From bbdb352f7bf1516f6fa051b5214dd34b5bb106f5 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 11:59:40 +0000 Subject: [PATCH] fix: address PR review comments for dev server initialization - Wrapped `filter_map` closure in a future returning construct for tokio streams. - Explicitly handle `serde_json::to_string` serialization errors without returning empty payload strings. - Replaced the watcher `blocking_send` logic with non-blocking `try_send`. - Expanded watcher target directories to include `app/` and `public/` directories from the standard structure scaffold. - Bound server configuration host to `127.0.0.1` by default and added CLI argument. - Extracted `tokio-stream` to workspace global dependencies. Co-authored-by: Theaxiom <57013+Theaxiom@users.noreply.github.com> --- Cargo.lock | 13 ++- Cargo.toml | 1 + cli/src/commands/dev.rs | 26 ++++- foundry/client/src/migrate/analyzer.rs | 21 ++--- runtime/Cargo.toml | 4 +- runtime/src/dev/dev_server.rs | 125 ++++++++++++++++++++++++- runtime/src/dev/hot_reload.rs | 83 ++++++++++++++++ 7 files changed, 251 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8ae8cfa..667be77 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -831,7 +831,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -977,12 +977,14 @@ dependencies = [ "forge-compiler", "forge-shared", "futures", + "futures-util", "hyper", "notify", "serde", "serde_json", "thiserror", "tokio", + "tokio-stream", "tokio-util", "tower 0.4.13", "tower-http", @@ -1979,7 +1981,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2779,7 +2781,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3480,7 +3482,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3605,6 +3607,7 @@ dependencies = [ "futures-core", "pin-project-lite", "tokio", + "tokio-util", ] [[package]] @@ -4205,7 +4208,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index bce0cb8..742cad0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,6 +68,7 @@ chrono = { version = "0.4", features = ["serde"] } # Utilities bytes = "1" futures = "0.3" +tokio-stream = { version = "0.1.18", features = ["sync"] } async-trait = "0.1" dashmap = "6" indexmap = { version = "2", features = ["serde"] } diff --git a/cli/src/commands/dev.rs b/cli/src/commands/dev.rs index 38c5a48..b971de7 100644 --- a/cli/src/commands/dev.rs +++ b/cli/src/commands/dev.rs @@ -1,10 +1,15 @@ //! `forge dev` — Start the development server and Forge Studio. use anyhow::Result; +use camino::Utf8PathBuf; use clap::Args; +use forge_runtime::dev::dev_server::{start_dev_server, DevServerConfig}; #[derive(Debug, Args)] pub struct DevArgs { + /// Host to bind to (default: 127.0.0.1) + #[arg(long, default_value = "127.0.0.1")] + pub host: String, /// Port for the dev server (default: 3000) #[arg(long, default_value = "3000")] pub port: u16, @@ -14,8 +19,23 @@ pub struct DevArgs { } pub async fn run(args: DevArgs) -> Result<()> { - crate::output::info(&format!("Starting dev server on :{}", args.port)); - crate::output::info(&format!("Forge Studio on :{}", args.studio_port)); - // TODO: Delegate to forge-runtime dev server + crate::output::info(&format!( + "Starting dev server on {}:{}", + args.host, args.port + )); + crate::output::info(&format!( + "Forge Studio on {}:{}", + args.host, args.studio_port + )); + + let config = DevServerConfig { + host: args.host, + port: args.port, + studio_port: args.studio_port, + project_root: Utf8PathBuf::from("."), + }; + + start_dev_server(config).await?; + Ok(()) } diff --git a/foundry/client/src/migrate/analyzer.rs b/foundry/client/src/migrate/analyzer.rs index 9d59fed..a6229a4 100644 --- a/foundry/client/src/migrate/analyzer.rs +++ b/foundry/client/src/migrate/analyzer.rs @@ -218,21 +218,20 @@ fn check_expression(expr: &Expression<'_>, source: &str, detected: &mut Vec { - if member.property.name == "env" { - if let Expression::Identifier(obj) = &member.object { - if obj.name == "process" { - let line = line_number_at_offset(source, member.span.start); - detected.push(DetectedApi { - pattern: "process.env".into(), - line, - compatibility: Compatibility::Shimmable, - }); - } + Expression::StaticMemberExpression(member) if member.property.name == "env" => { + if let Expression::Identifier(obj) = &member.object { + if obj.name == "process" { + let line = line_number_at_offset(source, member.span.start); + detected.push(DetectedApi { + pattern: "process.env".into(), + line, + compatibility: Compatibility::Shimmable, + }); } } // __dirname, __filename as member access targets are covered below } + Expression::StaticMemberExpression(_) => {} // Standalone identifiers: __dirname, __filename, Buffer (as global) Expression::Identifier(ident) => { diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 8387166..d5309fa 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -23,7 +23,9 @@ anyhow = { workspace = true } tracing = { workspace = true } camino = { workspace = true } bytes = { workspace = true } -futures = { workspace = true } +futures.workspace = true notify = { workspace = true } uuid = { workspace = true } deno_core = { workspace = true } +tokio-stream = { workspace = true } +futures-util = "0.3.32" diff --git a/runtime/src/dev/dev_server.rs b/runtime/src/dev/dev_server.rs index aa86ae2..fb16b4e 100644 --- a/runtime/src/dev/dev_server.rs +++ b/runtime/src/dev/dev_server.rs @@ -3,10 +3,131 @@ //! Starts the dev server with HMR and Forge Studio. //! Listens on port 3000 (app) and port 3001 (Studio) by default. +use crate::dev::hot_reload::{hmr_router, HmrMessage, HmrState}; use crate::error::RuntimeError; +use axum::Router; +use camino::Utf8PathBuf; +use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher}; +use tokio::net::TcpListener; +use tokio::sync::mpsc; +use tracing::{error, info}; + +/// Configuration for the development server. +#[derive(Debug, Clone)] +pub struct DevServerConfig { + /// Host to bind to (default: 127.0.0.1) + pub host: String, + /// Port for the dev server (default: 3000) + pub port: u16, + /// Port for Forge Studio (default: 3001) + pub studio_port: u16, + /// The project root directory + pub project_root: Utf8PathBuf, +} + +impl Default for DevServerConfig { + fn default() -> Self { + Self { + host: "127.0.0.1".to_string(), + port: 3000, + studio_port: 3001, + project_root: Utf8PathBuf::from("."), + } + } +} /// Start the development server. -pub async fn start_dev_server() -> Result<(), RuntimeError> { - // TODO: Initialize file watcher, incremental compiler, HMR, and Studio +pub async fn start_dev_server(config: DevServerConfig) -> Result<(), RuntimeError> { + let hmr_state = HmrState::new(); + + // 1. Setup file watcher + let (tx, mut rx) = mpsc::channel(100); + + let mut watcher = RecommendedWatcher::new( + move |res| { + if let Err(e) = tx.try_send(res) { + tracing::warn!("Failed to send watcher event: {}", e); + } + }, + Config::default(), + ) + .map_err(|e| RuntimeError::Internal(format!("Failed to initialize watcher: {}", e)))?; + + // Watch relevant directories + let watch_dirs = ["app", "public", "src"]; + let mut watching_any = false; + for dir in watch_dirs { + let path = config.project_root.join(dir); + if path.exists() { + watcher + .watch(path.as_std_path(), RecursiveMode::Recursive) + .map_err(|e| { + RuntimeError::Internal(format!("Failed to watch {} directory: {}", dir, e)) + })?; + info!("Watching {} for changes", path); + watching_any = true; + } + } + + if !watching_any { + error!( + "No recognizable source directories (app, public, src) found in {}", + config.project_root + ); + } + + // Spawn a background task to process file watcher events and trigger HMR + let hmr_state_clone = hmr_state.clone(); + tokio::spawn(async move { + while let Some(res) = rx.recv().await { + match res { + Ok(event) => { + // For now, we broadcast a reload on any change. + // Later, this will trigger the incremental compiler and only push updates for changed modules. + if event.kind.is_modify() || event.kind.is_create() || event.kind.is_remove() { + info!("File changed, triggering HMR reload"); + hmr_state_clone.broadcast(HmrMessage::Reload); + } + } + Err(e) => error!("Watch error: {:?}", e), + } + } + // Keep watcher alive by moving it into this task + let _watcher = watcher; + }); + + // 2. Main App Server (incorporates HMR router) + // TODO: Combine with the actual app router + let app = hmr_router(hmr_state.clone()); + let addr = format!("{}:{}", config.host, config.port); + let listener = TcpListener::bind(&addr).await.map_err(RuntimeError::Io)?; + + // 3. Studio Server + let studio_app = Router::new().route( + "/", + axum::routing::get(|| async { "Forge Studio (Coming soon)" }), + ); + let studio_addr = format!("{}:{}", config.host, config.studio_port); + let studio_listener = TcpListener::bind(&studio_addr) + .await + .map_err(RuntimeError::Io)?; + + info!("Dev server listening on {}", addr); + info!("Forge Studio listening on {}", studio_addr); + + // Run both servers concurrently + tokio::try_join!( + async { + axum::serve(listener, app) + .await + .map_err(|e| RuntimeError::Http(e.to_string())) + }, + async { + axum::serve(studio_listener, studio_app) + .await + .map_err(|e| RuntimeError::Http(e.to_string())) + } + )?; + Ok(()) } diff --git a/runtime/src/dev/hot_reload.rs b/runtime/src/dev/hot_reload.rs index 8caed71..907a829 100644 --- a/runtime/src/dev/hot_reload.rs +++ b/runtime/src/dev/hot_reload.rs @@ -5,3 +5,86 @@ //! 2. Pushes the new module source to connected browsers via a SSE stream //! 3. The browser's HMR runtime replaces the module in-place if possible, //! or triggers a full reload if the module graph changed structurally + +use axum::{ + extract::State, + response::sse::{Event, Sse}, + routing::get, + Router, +}; +use futures::stream::Stream; +use std::convert::Infallible; +use tokio::sync::broadcast; +use tokio_stream::wrappers::BroadcastStream; + +/// Message sent over the HMR broadcast channel. +#[derive(Debug, Clone, serde::Serialize)] +#[serde(tag = "type", content = "payload")] +pub enum HmrMessage { + /// A module was updated and should be re-evaluated. + Update { + /// The path of the module that was updated. + path: String, + /// The new module source code. + code: String, + }, + /// The application needs a full reload (e.g. structural change). + Reload, +} + +/// State shared across HMR SSE connections. +#[derive(Clone)] +pub struct HmrState { + /// Broadcast channel for pushing updates to connected clients. + pub tx: broadcast::Sender, +} + +impl Default for HmrState { + fn default() -> Self { + Self::new() + } +} + +impl HmrState { + /// Create a new HmrState. + pub fn new() -> Self { + let (tx, _) = broadcast::channel(100); + Self { tx } + } + + /// Broadcast an update to all connected clients. + pub fn broadcast(&self, message: HmrMessage) { + // Ignore send errors (happens when no clients are connected) + let _ = self.tx.send(message); + } +} + +/// Create the axum router for the HMR endpoint. +pub fn hmr_router(state: HmrState) -> Router { + Router::new() + .route("/_forge/hmr", get(hmr_endpoint)) + .with_state(state) +} + +/// SSE endpoint handler for HMR connections. +async fn hmr_endpoint( + State(state): State, +) -> Sse>> { + let rx = state.tx.subscribe(); + let stream = futures::StreamExt::filter_map(BroadcastStream::new(rx), |msg| { + let res = match msg { + Ok(msg) => match serde_json::to_string(&msg) { + Ok(json) => Some(Ok(Event::default().data(json))), + Err(e) => { + tracing::error!("Failed to serialize HMR message: {}", e); + None + } + }, + // Ignore lag errors + Err(_) => None, + }; + std::future::ready(res) + }); + + Sse::new(stream).keep_alive(axum::response::sse::KeepAlive::new()) +}