Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
9af9c49
feat: add nix shell
fahdfady Feb 13, 2025
48f1903
feat: add a simple watcher
fahdfady Feb 16, 2025
16142af
add dev to cli
fahdfady Feb 17, 2025
73c4d33
feat: add dev to cli
fahdfady Feb 18, 2025
32f8823
fix watching events nad logging them
fahdfady Feb 18, 2025
1a32395
feat: add crate "metassr-watcher
fahdfady Feb 19, 2025
ceb3712
add a dummy rebuilder module
fahdfady Feb 19, 2025
02d95c4
logging out rebuilds
fahdfady Feb 19, 2025
96891e9
fix typo in path binding
fahdfady Feb 20, 2025
cedb5ea
feat: dummy single-page rebuilder implementation
fahdfady Feb 20, 2025
058e782
integrate rebuilder in dev server mode
fahdfady Feb 20, 2025
0c3422d
add watching for both src and static directories
fahdfady Feb 20, 2025
1b22a4b
abstract the start server in dev server mode
fahdfady Feb 20, 2025
9f4df0e
feat(devserver): add file change handling
fahdfady Feb 20, 2025
1cf2b79
add debug output for rebuild paths and dev command
fahdfady Feb 23, 2025
77225ff
fix(rebuilder): correct error handling for irrelevant events
fahdfady Feb 23, 2025
8cf3172
fix(main cli): use current directory for server path in dev mode
fahdfady Feb 23, 2025
20623bd
fix(rebuilder): start building on changes
fahdfady Feb 23, 2025
dc587fb
fix(rebuilder): update output directory handling and improve logging
fahdfady Feb 23, 2025
9107fe9
metassr-server(deps): add tokio-tungstenite, futures-util, serde
fahdfady Feb 26, 2025
b63d0c3
feat: implement LiveReload, the server-side of websocket in devmode
fahdfady Feb 26, 2025
5f31a39
feat(notworking): implement LiveReload, the client-side of websocket …
fahdfady Feb 26, 2025
ed3053e
bump to metacall 4.4 and solve some of the breaking changes of the ne…
fahdfady Sep 8, 2025
fe5eba6
Update Cargo.toml
viferga Sep 8, 2025
676a645
add metacall-sys and build.rs to find metcall using cargo
fahdfady Sep 11, 2025
c071f77
chore(test web-app): add @rspack/core
fahdfady Sep 11, 2025
f3df7bc
trying to fix breaking metacall 0.5.1 changes
fahdfady Sep 11, 2025
990861d
deps: make sure all crates have metacall 0.5.1 and all working with n…
fahdfady Sep 11, 2025
ac3e759
add metacall-sys 0.1.0 and build scripts for all crates
fahdfady Sep 16, 2025
ace5aef
Update Cargo.toml
viferga Sep 16, 2025
c654a34
Update builder.rs
viferga Sep 16, 2025
ceda0ca
metacall_sys 0.1.1 for all crates
fahdfady Sep 17, 2025
c074c4a
Merge branch 'master' of https://github.com/metacall/metassr into dev…
fahdfady Sep 17, 2025
53348dc
merge changes from bump
fahdfady Sep 17, 2025
2f9708e
Merge branch 'development-mode' of https://github.com/fahdfady/metass…
fahdfady Sep 17, 2025
c9aadb6
silence tracing and logs from notify crate by filtering
fahdfady Sep 17, 2025
2753a8b
add trace info instead of println. remove package-lock.json
fahdfady Sep 17, 2025
1391bc2
init a working rust nix flake
fahdfady Sep 10, 2025
dba7ecc
update flake
fahdfady Sep 10, 2025
e723295
try to define metacall in flake
fahdfady Sep 10, 2025
1a26786
refactor: streamline MetaCall installation in dev shell
fahdfady Sep 10, 2025
20ef0cf
enhance flake.nix with important pkgs and lib paths
fahdfady Sep 11, 2025
94448a3
fix(flake): edit lib paths
fahdfady Sep 11, 2025
5a95467
remove nix shell
fahdfady Sep 23, 2025
cdadeea
flake: add yarn
fahdfady Sep 24, 2025
bdc2944
feat: inject live-reload.js in dev mode
fahdfady Sep 24, 2025
178f1d4
feat: update dependencies and enhance live reload functionality
fahdfady Sep 24, 2025
18c5bbe
feat: minimal working live reload server updated on file change via w…
fahdfady Sep 28, 2025
e7309c4
feat: enhance live reload functionality with notify-debouncer-full an…
fahdfady Oct 5, 2025
4e27164
some edits
fahdfady Oct 5, 2025
75da799
fix: refactor rebuilder Arc
fahdfady Oct 5, 2025
753394b
fix: multiple refreshes due to multiple websocket messages sent
fahdfady Oct 5, 2025
2584fd9
add is_rebuilding to avoid race conditions between threads
fahdfady Oct 8, 2025
6b617ea
refacor: js close old websocket connections
fahdfady Oct 8, 2025
acf6918
watcher and dev mode increase channel size
fahdfady Oct 8, 2025
ae24900
remove flakes
fahdfady Oct 8, 2025
e1599c6
some PR edits
fahdfady Oct 9, 2025
f746a8a
uncomment code
fahdfady Oct 9, 2025
bd30858
server mode: implement display
fahdfady Oct 9, 2025
240410c
move rebuilder to ServerConfigs Struct
fahdfady Oct 9, 2025
835d767
improve debug messages
fahdfady Oct 9, 2025
c834b50
solve conflicts
fahdfady Oct 9, 2025
af7b841
Merge branch 'master' into development-mode
fahdfady Oct 9, 2025
a5eefca
livereload script remove validation
fahdfady Oct 9, 2025
f218d8f
change listening message
fahdfady Oct 9, 2025
b92b87a
implement as_message for RebuildType
fahdfady Oct 9, 2025
d512200
chore(deps): remove axum ws feature
fahdfady Oct 9, 2025
b7cf573
chore: remove import comment
hulxv Oct 16, 2025
1412530
Merge branch 'master' into development-mode
hulxv Oct 17, 2025
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
991 changes: 860 additions & 131 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ tower-service = "0.3.3"
metassr-create = { path = "crates/metassr-create" }
metassr-bundler = { path = "crates/metassr-bundler" }
metassr-fs-analyzer = { path = "crates/metassr-fs-analyzer" }
metassr-watcher = { path = "crates/metassr-watcher" }

[build-dependencies]
metacall-sys = "0.1.1"
Expand All @@ -54,6 +55,7 @@ members = [
"crates/metassr-create",
"crates/metassr-bundler",
"crates/metassr-fs-analyzer",
"crates/metassr-watcher",
]

[[bin]]
Expand Down
4 changes: 4 additions & 0 deletions crates/metassr-build/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ lazy_static = "1.5.0"
serde = { version = "1.0.207", features = ["derive"] }
metassr-bundler = { path = "../metassr-bundler" }
metassr-fs-analyzer = { path = "../metassr-fs-analyzer" }


[build-dependencies]
metacall-sys = "0.1.1"
3 changes: 3 additions & 0 deletions crates/metassr-build/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fn main() {
metacall_sys::build();
}
4 changes: 4 additions & 0 deletions crates/metassr-bundler/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,7 @@ metacall = "0.5.1"
metassr-utils = { path = "../metassr-utils" }
serde_json = "1.0.128"
tracing = "0.1.40"


[build-dependencies]
metacall-sys = "0.1.1"
3 changes: 3 additions & 0 deletions crates/metassr-bundler/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fn main() {
metacall_sys::build();
}
8 changes: 8 additions & 0 deletions crates/metassr-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,20 @@ edition = "2021"
anyhow = "1.0.82"
axum = "0.7.5"
chrono = "0.4.38"
metacall = "0.4.1"
metassr-build = { path = "../metassr-build" }
metassr-fs-analyzer = { path = "../metassr-fs-analyzer" }
metassr-utils = { path = "../metassr-utils" }
metassr-watcher = { path = "../metassr-watcher" }
serde_json = "1.0.122"
tokio = { version = "1.36.0", features = ["full"] }
tokio-tungstenite = "0.26.2"
futures-util = "0.3.31"
serde = "1.0.218"
tower-http = { version = "0.5.2", features = ["trace", "fs"] }
tower-layer = "0.3.3"
tower-service = "0.3.3"
tracing = "0.1.40"
notify = { version = "8.0.0", features = ["serde"] }
notify-debouncer-full = "0.6.0"
walkdir = "2"
105 changes: 94 additions & 11 deletions crates/metassr-server/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,30 +1,67 @@
mod fallback;
mod handler;
mod layers;
pub mod live_reload;
pub mod rebuilder;
mod router;

use fallback::Fallback;
use handler::PagesHandler;
use layers::tracing::{LayerSetup, TracingLayer, TracingLayerOptions};

use anyhow::Result;
use axum::routing::get;
use axum::{http::StatusCode, response::Redirect, Router};
use live_reload::LiveReloadServer;
use rebuilder::Rebuilder;
use router::RouterMut;
use std::path::{Path, PathBuf};
use std::{
path::{Path, PathBuf},
sync::Arc,
};
use tokio::net::TcpListener;
use tower_http::services::ServeDir;
use tracing::info;
use tracing::{debug, info};

use crate::live_reload::inject_live_reload_script;

#[derive(Debug, Clone, Copy)]
pub enum ServerMode {
Development,
Production,
}

impl std::fmt::Display for ServerMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match *self {
Self::Development => write!(f, "development"),
Self::Production => write!(f, "production"),
}
}
}

#[derive(Debug, Clone, Copy)]
pub enum RunningType {
StaticSiteGeneration,
ServerSideRendering,
}

impl std::fmt::Display for RunningType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::StaticSiteGeneration => write!(f, "SSG"),
Self::ServerSideRendering => write!(f, "SSR"),
}
}
}

pub struct ServerConfigs {
pub port: u16,
pub _enable_http_logging: bool,
pub root_path: PathBuf,
pub running_type: RunningType,
pub mode: ServerMode,
pub rebuilder: Option<Arc<Rebuilder>>,
}

pub struct Server {
Expand All @@ -47,11 +84,46 @@ impl Server {
self.configs.root_path.to_str().unwrap()
));

let mut app = RouterMut::from(
Router::new()
.nest_service("/static", ServeDir::new(&static_dir))
.nest_service("/dist", ServeDir::new(&dist_dir)),
);
let mut base_router = Router::new()
.nest_service("/static", ServeDir::new(&static_dir))
.nest_service("/dist", ServeDir::new(&dist_dir));

if let ServerMode::Development = self.configs.mode {
info!("Configuring server for development mode");
let live_reload_script = include_str!("scripts/live-reload.js");
base_router = base_router.route(
"/livereload/script.js",
get(|| async {
info!("Serving live-reload.js");
axum::response::Response::builder()
.header("Content-Type", "application/javascript")
.body(live_reload_script.to_string())
.unwrap()
}),
);
// Apply live reload middleware
base_router = base_router.layer(axum::middleware::from_fn(inject_live_reload_script));

// Start the WebSocket server for live reload
let ws_listener = TcpListener::bind("127.0.0.1:3001")
.await
.map_err(|e| anyhow::anyhow!("WebSocket bind error: {}", e))?;
debug!(
"WebSocket server listening on {:?}",
ws_listener.local_addr()?
);
if let Some(rebuilder) = self.configs.rebuilder.clone() {
tokio::spawn(async move {
while let Ok((stream, addr)) = ws_listener.accept().await {
let live_reload = LiveReloadServer::new(rebuilder.subscribe());
// live_reload.handle_connection(socket).await;
tokio::spawn(live_reload.handle_connection(stream, addr));
}
});
}
}

let mut app = RouterMut::from(base_router);

match self.configs.running_type {
RunningType::StaticSiteGeneration => {
Expand All @@ -65,7 +137,7 @@ impl Server {
.to_html(),
)
};
app.fallback(fallback)
app.fallback(fallback);
}
RunningType::ServerSideRendering => {
app.fallback(|| async { Redirect::to("/_notfound") })
Expand All @@ -74,17 +146,28 @@ impl Server {

PagesHandler::new(&mut app, &dist_dir, self.configs.running_type)?.build()?;

// **Setting up layers**
// Apply middleware again after PagesHandler to catch dynamic HTML
if let ServerMode::Development = self.configs.mode {
debug!("Applying live reload middleware after PagesHandler");
app = RouterMut::from(
app.app()
.layer(axum::middleware::from_fn(inject_live_reload_script)),
);
}

// Tracing layer
TracingLayer::setup(
TracingLayerOptions {
enable_http_logging: self.configs._enable_http_logging,
// mode: self.configs.mode,
},
&mut app,
);

info!("Listening on http://{}", listener.local_addr()?);
info!(
message = format!("Listening on http://{}", listener.local_addr()?),
mode = self.configs.mode.to_string()
);

axum::serve(listener, app.app()).await?;
Ok(())
}
Expand Down
105 changes: 105 additions & 0 deletions crates/metassr-server/src/live_reload.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
use crate::rebuilder::RebuildType;
use axum::{
body::Body,
http::{header, Request, Response, StatusCode},
middleware::Next,
};
use futures_util::{SinkExt, StreamExt};
use serde::Serialize;
use tokio_tungstenite::tungstenite::Message;

use tokio::{net::TcpStream, sync::broadcast::Receiver};
use tracing::info;

#[derive(Debug, Serialize)]
struct LiveReloadMessage {
#[serde(rename = "type")]
type_: String,
path: Option<String>,
}

impl RebuildType {
fn as_message(&self) -> LiveReloadMessage {
let (type_, path) = match self {
RebuildType::Page(path) => {
("page".to_string(), Some(path.to_string_lossy().to_string()))
}
_ => (self.to_string(), None),
};

LiveReloadMessage { type_, path }
}
}

pub struct LiveReloadServer {
receiver: Receiver<RebuildType>,
}

impl LiveReloadServer {
pub fn new(receiver: Receiver<RebuildType>) -> Self {
Self { receiver }
}

pub async fn handle_connection(mut self, stream: TcpStream, addr: std::net::SocketAddr) {
let ws_stream = tokio_tungstenite::accept_async(stream)
.await
.expect("Error during websocket handshake");

let (mut ws_sender, _) = ws_stream.split();

while let Ok(rebuild_type) = self.receiver.recv().await {
let message = rebuild_type.as_message();
let message_json = serde_json::to_string(&message).unwrap();

if let Err(e) = ws_sender.send(Message::Text(message_json.into())).await {
tracing::error!("Failed to send LiveReload message: {}", e);
break;
}
}
}
}

/// middlware to indject the live-reload.js script
pub async fn inject_live_reload_script(
req: Request<Body>,
next: Next,
) -> Result<Response<Body>, StatusCode> {
let response = next.run(req).await;

// Check if the response is HTML
let is_html: bool = response
.headers()
.get(header::CONTENT_TYPE)
.map(|v| {
v.to_str()
.unwrap_or("")
.to_lowercase()
.contains("text/html")
})
.unwrap_or(false);

if is_html {
let (parts, body) = response.into_parts();

let body_bytes = axum::body::to_bytes(body, usize::MAX).await.map_err(|e| {
info!("Failed to read response body: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;
let body_str = String::from_utf8_lossy(&body_bytes).to_string();

// Inject script before </body> or append if </body> is missing
let modified_body = body_str.replace(
"</body>",
r#"<script src="/livereload/script.js"></script></body>"#,
);

return Ok(Response::builder()
.status(parts.status)
.header(header::CONTENT_TYPE, "text/html")
.header(header::CACHE_CONTROL, "no-cache") // Prevent caching in dev
.body(Body::from(modified_body))
.unwrap());
}

Ok(response)
}
Loading
Loading