1use axum::http::StatusCode;
2use axum::response::{IntoResponse, Response};
3use axum::Router;
4use std::path::{Path, PathBuf};
5use tower_http::services::{ServeDir, ServeFile};
6
7pub fn locate(explicit: Option<PathBuf>) -> Option<PathBuf> {
13 if let Some(dir) = explicit {
14 return dir.join("index.html").is_file().then_some(dir);
15 }
16
17 if let Some(dir) = std::env::var_os("ECR_WEB_DIR").map(PathBuf::from) {
18 if dir.join("index.html").is_file() {
19 return Some(dir);
20 }
21 }
22
23 candidates()
24 .into_iter()
25 .find(|d| d.join("index.html").is_file())
26}
27
28fn candidates() -> Vec<PathBuf> {
29 let mut out = Vec::new();
30
31 if let Ok(cwd) = std::env::current_dir() {
32 out.push(cwd.join("web/dist"));
33 out.push(cwd.join("dist"));
34 }
35
36 if let Ok(exe) = std::env::current_exe() {
39 for ancestor in exe.ancestors().skip(1).take(4) {
40 out.push(ancestor.join("web/dist"));
41 out.push(ancestor.join("share/ecr/web"));
42 }
43 }
44
45 out
46}
47
48pub fn router(dir: &Path) -> Router {
51 let index = dir.join("index.html");
52
53 Router::new().fallback_service(
54 ServeDir::new(dir)
55 .append_index_html_on_directories(true)
56 .fallback(ServeFile::new(index)),
57 )
58}
59
60pub async fn missing() -> Response {
61 (
62 StatusCode::NOT_FOUND,
63 [(axum::http::header::CONTENT_TYPE, "text/html; charset=utf-8")],
64 MISSING_PAGE,
65 )
66 .into_response()
67}
68
69const MISSING_PAGE: &str = r#"<!doctype html>
70<meta charset="utf-8">
71<title>ecr — client not built</title>
72<style>
73 body { background:#1a1b26; color:#c0caf5; font-family:ui-monospace,monospace;
74 display:grid; place-items:center; height:100vh; margin:0; }
75 div { max-width:34rem; padding:2rem; }
76 h1 { color:#7aa2f7; font-size:1.1rem; }
77 code { background:#1f2335; padding:.15rem .4rem; border-radius:.2rem; color:#9ece6a; }
78 p { line-height:1.7; }
79</style>
80<div>
81 <h1>The web client has not been built</h1>
82 <p>The API is running here, but there is no <code>web/dist</code> to serve.</p>
83 <p>Build it with <code>just build-web</code>, then reload. For hot reload
84 during development run <code>just dev</code> and use
85 <code>http://localhost:1420</code> instead.</p>
86</div>
87"#;
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 #[test]
94 fn an_explicit_directory_without_an_index_is_rejected() {
95 let dir = tempfile::tempdir().unwrap();
96 assert_eq!(locate(Some(dir.path().to_path_buf())), None);
97 }
98
99 #[test]
100 fn an_explicit_directory_with_an_index_is_accepted() {
101 let dir = tempfile::tempdir().unwrap();
102 std::fs::write(dir.path().join("index.html"), "<html>").unwrap();
103
104 assert_eq!(
105 locate(Some(dir.path().to_path_buf())),
106 Some(dir.path().to_path_buf())
107 );
108 }
109
110 #[test]
111 fn candidates_include_the_workspace_layout() {
112 let paths = candidates();
113 assert!(paths.iter().any(|p| p.ends_with("web/dist")), "{paths:?}");
114 }
115
116 #[test]
117 fn the_missing_page_explains_how_to_fix_it() {
118 assert!(MISSING_PAGE.contains("just build-web"));
119 assert!(MISSING_PAGE.contains("just dev"));
120 }
121}