create_rust_app/util/
actix_web_utils.rs

1#[cfg(debug_assertions)]
2use std::str::FromStr;
3#[cfg(debug_assertions)]
4use std::sync::Mutex;
5
6use super::template_utils::SinglePageApplication;
7use super::workspace_utils::frontend_dir;
8use crate::util::template_utils::{to_template_name, DEFAULT_TEMPLATE, TEMPLATES};
9use actix_files::NamedFile;
10#[cfg(debug_assertions)]
11use actix_http::Uri;
12use actix_web::http::StatusCode;
13use actix_web::{web, HttpRequest, HttpResponse, Scope};
14use tera::Context;
15
16/// 'route': the route where the SPA should be served from, for example: "/app"
17/// 'view': the view which renders the SPA, for example: "spa/index.html"
18pub fn render_single_page_application(route: &str, view: &str) -> Scope {
19    use actix_web::web::Data;
20
21    let route = route.strip_prefix('/').unwrap_or(route);
22    let view = view.strip_prefix('/').unwrap_or(view);
23
24    actix_web::web::scope(&format!("/{route}{{tail:(/.*)?}}"))
25        .app_data(Data::new(SinglePageApplication {
26            view_name: view.to_string(),
27        }))
28        .route("", web::get().to(render_spa_handler))
29}
30
31#[allow(clippy::future_not_send)]
32async fn render_spa_handler(
33    req: HttpRequest,
34    spa_info: web::Data<SinglePageApplication>,
35) -> HttpResponse {
36    let content = TEMPLATES
37        .render(spa_info.view_name.as_str(), &Context::new())
38        .unwrap();
39    template_response(&req, content)
40}
41
42// used to count number of refresh requests sent when viteJS dev server is down
43#[allow(clippy::mutex_integer)] // TODO: can we use an atomic integer here?
44#[cfg(debug_assertions)]
45static REQUEST_REFRESH_COUNT: Mutex<i32> = Mutex::new(0);
46
47/// takes a request to, say, `www.you_webapp.com/foo/bar` and looks in the ./backend/views folder
48/// for a html file/template at the matching path (in this case, ./foo/bar.html),
49/// defaults to index.html
50///
51/// then, your frontend (all the css files, scripts, etc. in your frontend's vite manifest (at ./frontend/dist/manifest.json))
52/// will be compiled and injected into the template wherever `{{ bundle(name="index.tsx") }}` is (the `index.tsx` can be any .tsx file in ./frontend/bundles)
53///
54/// then, that compiled html is sent to the client
55///
56/// NOTE the frontend/dist/manifest.json file referenced is generated in the frontend when it compiles
57///
58/// # Panics
59/// - the mutex lock is poisoned
60#[allow(clippy::future_not_send)]
61pub async fn render_views(req: HttpRequest) -> HttpResponse {
62    let path = req.path();
63
64    #[cfg(debug_assertions)]
65    {
66        if path.eq("/__vite_ping") {
67            println!("The vite dev server seems to be down...");
68        }
69
70        // Catch viteJS ping requests and try to handle them gracefully
71        // Request the browser to refresh the page (maybe the server is up but the browser just can't reconnect)
72
73        if path.eq("/__vite_ping") {
74            #[cfg(feature = "plugin_dev")]
75            {
76                crate::dev::vitejs_ping_down().await;
77            }
78            #[allow(clippy::mutex_integer)]
79            let mut count = REQUEST_REFRESH_COUNT.lock().unwrap();
80            if *count < 3 {
81                *count += 1;
82                println!("The vite dev server seems to be down... refreshing page ({count}).");
83                drop(count);
84                return HttpResponse::build(StatusCode::TEMPORARY_REDIRECT)
85                    .append_header(("Location", "."))
86                    .finish();
87            }
88            println!("The vite dev server is down.");
89            return HttpResponse::NotFound().finish();
90        }
91        // If this is a non-viteJS ping request, let's reset the refresh attempt count
92        #[cfg(feature = "plugin_dev")]
93        {
94            crate::dev::vitejs_ping_up().await;
95        }
96        #[allow(clippy::mutex_integer)]
97        let mut count = REQUEST_REFRESH_COUNT.lock().unwrap();
98        *count = 0;
99    }
100
101    let mut template_path = to_template_name(req.path());
102    // try and render from your ./backend/views
103    let mut content_result = TEMPLATES.render(template_path, &Context::new());
104
105    // if that fails, then
106    //  if in debug mode look for views in ./frontend/... or ./frontend/public/...
107    //  else default to ./backend/views/index.html
108    if content_result.is_err() {
109        #[cfg(debug_assertions)]
110        {
111            // dev asset serving
112            let asset_path = &format!("{frontend_dir}{path}", frontend_dir = frontend_dir());
113            if std::path::PathBuf::from(asset_path).is_file() {
114                println!("ASSET_FILE {path} => {asset_path}");
115                return NamedFile::open(asset_path).unwrap().into_response(&req);
116            }
117
118            let public_path =
119                &format!("{frontend_dir}/public{path}", frontend_dir = frontend_dir());
120            if std::path::PathBuf::from(public_path).is_file() {
121                println!("PUBLIC_FILE {path} => {public_path}");
122                return NamedFile::open(public_path).unwrap().into_response(&req);
123            }
124        }
125
126        #[cfg(not(debug_assertions))]
127        {
128            // production asset serving
129            let static_path = &format!("{frontend_dir}/dist{path}", frontend_dir = frontend_dir());
130            if std::path::PathBuf::from(static_path).is_file() {
131                return NamedFile::open(static_path).unwrap().into_response(&req);
132            }
133        }
134
135        content_result = TEMPLATES.render(DEFAULT_TEMPLATE, &Context::new());
136        template_path = DEFAULT_TEMPLATE;
137        if content_result.is_err() {
138            // default template doesn't exist -- return 404 not found
139            return HttpResponse::NotFound().finish();
140        }
141    }
142
143    println!("TEMPLATE_FILE {path} => {template_path}");
144
145    let content = content_result.unwrap();
146
147    template_response(&req, content)
148}
149
150#[allow(unused_variables)]
151fn template_response(req: &HttpRequest, content: String) -> HttpResponse {
152    #[cfg(not(debug_assertions))]
153    let content = content;
154    #[cfg(debug_assertions)]
155    let mut content = content;
156    #[cfg(debug_assertions)]
157    {
158        let uri = Uri::from_str(req.connection_info().host());
159        let hostname = uri
160            .as_ref()
161            .map_or("localhost", |uri| uri.host().unwrap_or("localhost"));
162
163        let inject: &str = &format!(
164            r##"
165        <!-- development mode -->
166        <script type="module">
167            import RefreshRuntime from 'http://{hostname}:21012/@react-refresh'
168            RefreshRuntime.injectIntoGlobalHook(window)
169            window.$RefreshReg$ = () => {{}}
170            window.$RefreshSig$ = () => (type) => type
171            window.__vite_plugin_react_preamble_installed__ = true
172        </script>
173        <script type="module" src="http://{hostname}:21012/src/dev.tsx"></script>
174        "##
175        );
176
177        if content.contains("<body>") {
178            content = content.replace("<body>", &format!("<body>{inject}"));
179        } else {
180            content = format!("{inject}{content}");
181        }
182    }
183
184    HttpResponse::build(StatusCode::OK)
185        .content_type("text/html")
186        .body(content)
187}