//! 開発時のみ使用: フロントエンド静的ファイル配信用のパス解決とルーター。
//! モノリシック化で beforeDevCommand (Node) を廃止し、同一プロセスで 8474 を listen するために使う。
use std::path::{Path, PathBuf};
use axum::Router;
use tower_http::services::ServeDir;
/// 開発時用のフロントエンドルート(`src/frontend`)の絶対パスを返す。
/// テスト時は CARGO_MANIFEST_DIR、実行時は exe_dir から解決する。
fn resolve_dev_frontend_dir(exe_dir: Option<&Path>) -> Option<PathBuf> {
// テスト時: CARGO_MANIFEST_DIR が設定されている(src/backend の兄弟 = src/frontend)
if let Some(manifest_dir) = std::env::var_os("CARGO_MANIFEST_DIR")
.map(PathBuf::from)
.filter(|p| p.ends_with("backend"))
{
let parent = manifest_dir.parent()?;
let frontend = parent.join("frontend");
if frontend.join("index.html").exists() {
return Some(frontend);
}
}
// 実行時: バイナリは target/debug にあるので 2 階層上がプロジェクトルート
let exe_dir = exe_dir?;
let project_root = exe_dir.parent()?.parent()?;
let frontend = project_root.join("src").join("frontend");
if frontend.join("index.html").exists() {
Some(frontend)
} else {
None
}
}
/// 開発時用のフロントエンドルート(テスト用。CARGO_MANIFEST_DIR のみ使用)。
pub fn dev_frontend_dir() -> Option<PathBuf> {
resolve_dev_frontend_dir(None)
}
/// 実行時用: exe の親ディレクトリからプロジェクトルートを辿って frontend を解決する。
pub fn dev_frontend_dir_from_exe(exe_dir: &Path) -> Option<PathBuf> {
resolve_dev_frontend_dir(Some(exe_dir))
}
/// 開発用フロントルートが解決できる場合、静的配信用の Axum Router を返す。
/// exe_dir: 実行時は Some(バイナリの親ディレクトリ)。テスト時は None で CARGO_MANIFEST_DIR を使用。
pub fn dev_static_router(exe_dir: Option<&Path>) -> Option<Router> {
let dir = resolve_dev_frontend_dir(exe_dir)?;
let serve_dir = ServeDir::new(dir).append_index_html_on_directories(true);
Some(Router::new().fallback_service(serve_dir))
}
#[cfg(test)]
mod tests {
use super::*;
use axum::body::Body;
use axum::http::Request;
use tower::util::ServiceExt;
#[test]
fn dev_frontend_dir_resolves_to_path_with_index_html() {
let dir = dev_frontend_dir();
assert!(
dir.is_some(),
"dev_frontend_dir() should return Some when run from workspace (crate at src/backend)"
);
let path = dir.unwrap();
assert!(
path.join("index.html").exists(),
"resolved path must contain index.html: {:?}",
path
);
assert!(
path.to_string_lossy().contains("frontend"),
"path should contain 'frontend': {:?}",
path
);
}
#[tokio::test]
async fn dev_static_router_returns_200_for_root() {
let router = dev_static_router(None).expect("dev_static_router(None) should be Some when run from workspace");
let req = Request::get("/").body(Body::empty()).unwrap();
let response = router.oneshot(req).await.unwrap();
assert_eq!(response.status(), 200, "GET / should return 200 when serving dev frontend");
}
}