Skip to content

Commit 191da4c

Browse files
committed
fix(v0.2.1): embed ui/build in binary via include_dir (fixes 404 on /test/ for installed builds)
1 parent ddf73c9 commit 191da4c

6 files changed

Lines changed: 78 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ All notable changes to this project are documented here.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
66
and this project adheres to [Semantic Versioning](https://semver.org/).
77

8+
## [0.2.1] — 2026-04-20
9+
10+
### Fixed
11+
12+
- Test view (`/test/...`) was returning 404 in installed builds because the
13+
LGS only served the UI from disk and the bundle didn't ship `ui/build/`
14+
alongside the exe. Release builds now **embed the UI inside the binary**
15+
via `include_dir!`. Dev builds keep the disk-based serving for hot reload.
16+
817
## [0.2.0] — 2026-04-20
918

1019
### Added

Cargo.lock

Lines changed: 23 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ members = [
66
]
77

88
[workspace.package]
9-
version = "0.2.0"
9+
version = "0.2.1"
1010
edition = "2024"
1111
authors = ["simnJS"]
1212
license = "MIT"
@@ -40,6 +40,8 @@ rustls = { version = "0.23", default-features = false, features = ["ring", "std"
4040
time = "0.3"
4141
dirs = "5.0"
4242
pem = "3.0"
43+
include_dir = "0.7"
44+
mime_guess = "2.0"
4345

4446
[profile.release]
4547
opt-level = 3

crates/desktop/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://schema.tauri.app/config/2",
33
"productName": "Stake Dev Tool",
4-
"version": "0.2.0",
4+
"version": "0.2.1",
55
"identifier": "bet.playandchill.stakedevtool",
66
"build": {
77
"beforeDevCommand": "pnpm --filter ui dev",

crates/lgs/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ time.workspace = true
3535
dirs.workspace = true
3636
pem.workspace = true
3737
uuid = { workspace = true }
38+
include_dir.workspace = true
39+
mime_guess.workspace = true
3840
tracing-subscriber.workspace = true
3941
anyhow.workspace = true
4042
thiserror.workspace = true

crates/lgs/src/lib.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
3239
pub 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+
5898
static CRYPTO_INIT: Once = Once::new();
5999

60100
fn init_crypto_provider() {

0 commit comments

Comments
 (0)