@@ -29,6 +29,13 @@ pub struct ServerHandle {
2929 pub join : tokio:: task:: JoinHandle < std:: io:: Result < ( ) > > ,
3030}
3131
32+ /// UI bundle embedded at compile time for release builds. In debug builds the
33+ /// UI is served from disk (see `ui_dir` fallback below) so `vite build` +
34+ /// `cargo run` picks up fresh UI without rebuilding the Rust binary.
35+ #[ cfg( not( debug_assertions) ) ]
36+ static EMBEDDED_UI : include_dir:: Dir < ' _ > =
37+ include_dir:: include_dir!( "$CARGO_MANIFEST_DIR/../../ui/build" ) ;
38+
3239pub fn build_router ( state : Arc < AppState > , ui_dir : Option < std:: path:: PathBuf > ) -> axum:: Router {
3340 let cors = CorsLayer :: new ( )
3441 . allow_origin ( AllowOrigin :: mirror_request ( ) )
@@ -46,6 +53,14 @@ pub fn build_router(state: Arc<AppState>, ui_dir: Option<std::path::PathBuf>) ->
4653 . merge ( devtool:: router ( state. clone ( ) ) )
4754 . merge ( replay:: router ( state) ) ;
4855
56+ // In release builds, always serve the embedded UI (no filesystem dep).
57+ // In debug builds, fall back to the on-disk ui/build/ for hot iteration.
58+ #[ cfg( not( debug_assertions) ) ]
59+ {
60+ let _ = ui_dir; // unused in release
61+ router = router. fallback ( embedded_ui_handler) ;
62+ }
63+ #[ cfg( debug_assertions) ]
4964 if let Some ( dir) = ui_dir {
5065 use tower_http:: services:: { ServeDir , ServeFile } ;
5166 let fallback = ServeFile :: new ( dir. join ( "index.html" ) ) ;
@@ -55,6 +70,31 @@ pub fn build_router(state: Arc<AppState>, ui_dir: Option<std::path::PathBuf>) ->
5570 router. layer ( cors) . layer ( TraceLayer :: new_for_http ( ) )
5671}
5772
73+ #[ cfg( not( debug_assertions) ) ]
74+ async fn embedded_ui_handler (
75+ req : axum:: http:: Request < axum:: body:: Body > ,
76+ ) -> axum:: response:: Response {
77+ use axum:: body:: Body ;
78+ use axum:: http:: { StatusCode , header} ;
79+ use axum:: response:: IntoResponse ;
80+
81+ let raw_path = req. uri ( ) . path ( ) ;
82+ let trimmed = raw_path. trim_start_matches ( '/' ) ;
83+ // Try direct match, directory index, then SPA fallback (index.html).
84+ let candidates: [ & str ; 3 ] = [ trimmed, & format ! ( "{trimmed}/index.html" ) , "index.html" ] ;
85+ // Re-alloc to satisfy lifetime — String → &str.
86+ let owned: Vec < String > = candidates. iter ( ) . map ( |s| s. to_string ( ) ) . collect ( ) ;
87+ for name in owned {
88+ if let Some ( file) = EMBEDDED_UI . get_file ( & name) {
89+ let mime = mime_guess:: from_path ( name)
90+ . first_or_octet_stream ( )
91+ . to_string ( ) ;
92+ return ( [ ( header:: CONTENT_TYPE , mime) ] , Body :: from ( file. contents ( ) ) ) . into_response ( ) ;
93+ }
94+ }
95+ ( StatusCode :: NOT_FOUND , "not found" ) . into_response ( )
96+ }
97+
5898static CRYPTO_INIT : Once = Once :: new ( ) ;
5999
60100fn init_crypto_provider ( ) {
0 commit comments