Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,4 @@ rust_guardian.*
# Test artifacts
*.profraw
lcov.info
update_analyzer.sh
10 changes: 5 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
14 changes: 12 additions & 2 deletions cli/src/commands/dev.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ 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,
Expand All @@ -16,10 +19,17 @@ 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));
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("."),
Expand Down
41 changes: 29 additions & 12 deletions runtime/src/dev/dev_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ 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)
Expand All @@ -26,6 +28,7 @@ pub struct DevServerConfig {
impl Default for DevServerConfig {
fn default() -> Self {
Self {
host: "127.0.0.1".to_string(),
port: 3000,
studio_port: 3001,
project_root: Utf8PathBuf::from("."),
Expand All @@ -42,21 +45,35 @@ pub async fn start_dev_server(config: DevServerConfig) -> Result<(), RuntimeErro

let mut watcher = RecommendedWatcher::new(
move |res| {
let _ = tx.blocking_send(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 the src directory
let src_dir = config.project_root.join("src");
if src_dir.exists() {
watcher
.watch(src_dir.as_std_path(), RecursiveMode::Recursive)
.map_err(|e| RuntimeError::Internal(format!("Failed to watch src directory: {}", e)))?;
info!("Watching {} for changes", src_dir);
} else {
error!("src directory not found in {}", config.project_root);
// 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
Expand All @@ -82,15 +99,15 @@ pub async fn start_dev_server(config: DevServerConfig) -> Result<(), RuntimeErro
// 2. Main App Server (incorporates HMR router)
// TODO: Combine with the actual app router
let app = hmr_router(hmr_state.clone());
let addr = format!("0.0.0.0:{}", config.port);
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!("0.0.0.0:{}", config.studio_port);
let studio_addr = format!("{}:{}", config.host, config.studio_port);
let studio_listener = TcpListener::bind(&studio_addr)
.await
.map_err(RuntimeError::Io)?;
Expand Down
13 changes: 8 additions & 5 deletions runtime/src/dev/hot_reload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ impl HmrState {
}

/// Create the axum router for the HMR endpoint.
pub fn hmr_router(state: HmrState) -> Router {
pub fn hmr_router(state: HmrState) -> Router<()> {
Router::new()
.route("/_forge/hmr", get(hmr_endpoint))
.with_state(state)
Expand All @@ -74,10 +74,13 @@ async fn hmr_endpoint(
let rx = state.tx.subscribe();
let stream = BroadcastStream::new(rx).filter_map(|msg| {
match msg {
Ok(msg) => {
let json = serde_json::to_string(&msg).unwrap_or_default();
Some(Ok(Event::default().data(json)))
}
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,
}
Expand Down