Skip to main content

rustbasic_core/
inertia.rs

1use crate::requests::Request;
2use crate::{IntoResponse, Response};
3use crate::http::{header, StatusCode, HeaderValue};
4use crate::serde_json::{json, Value};
5use std::fs;
6
7/// Helper untuk merender halaman SPA menggunakan React.js + Inertia.js
8pub fn inertia(req: &Request, component: &str, props: Value) -> Response {
9    let is_inertia = req.headers.get("x-inertia").map(|v| v == "true").unwrap_or(false);
10    let url = req.path.clone();
11    
12    // Versi asset (bisa dikonfigurasi untuk deteksi kadaluwarsa aset)
13    let version = ""; 
14
15    let errors: std::collections::HashMap<String, String> = req.session.get("errors").unwrap_or_default();
16    req.session.remove("errors");
17
18    let success: Option<String> = req.session.get("success");
19    req.session.remove("success");
20
21    let error: Option<String> = req.session.get("error");
22    req.session.remove("error");
23
24    let warning: Option<String> = req.session.get("warning");
25    req.session.remove("warning");
26
27    let info: Option<String> = req.session.get("info");
28    req.session.remove("info");
29
30    let mut props = props;
31    if let Value::Object(ref mut map) = props {
32        map.insert("errors".to_string(), json!(errors));
33        map.insert("flash".to_string(), json!({
34            "success": success,
35            "error": error,
36            "warning": warning,
37            "info": info
38        }));
39        let named_routes = crate::router::get_named_routes();
40        map.insert("routes".to_string(), json!(named_routes));
41        let cfg = crate::Config::load();
42        map.insert("app_url".to_string(), json!(cfg.app_url));
43    }
44
45    let page_object = json!({
46        "component": component,
47        "props": props,
48        "url": url,
49        "version": version
50    });
51
52    if is_inertia {
53        // Return JSON response untuk navigasi SPA Inertia
54        let body = crate::serde_json::to_string(&page_object).unwrap_or_default();
55        crate::http::Response::builder()
56            .status(StatusCode::OK)
57            .header(header::CONTENT_TYPE, "application/json")
58            .header("X-Inertia", "true")
59            .header(header::VARY, "X-Inertia")
60            .body(body.into_bytes())
61            .unwrap()
62            .into_response()
63    } else {
64        // Return layout root HTML "app.rb.html" untuk initial page load
65        let vite_assets = get_vite_assets(req);
66        let ctx = crate::serde_json::json!({
67            "page": page_object,
68            "vite_assets": vite_assets,
69        });
70        
71        let mut response = crate::view::view(req, "app.rb.html", ctx).into_response();
72        response.headers_mut().insert(
73            header::VARY,
74            HeaderValue::from_static("X-Inertia"),
75        );
76        response
77    }
78}
79
80/// Helper untuk mendapatkan HTML tag asset Vite (JS/CSS) secara dinamis
81pub fn get_vite_assets(req: &Request) -> String {
82    let cfg = crate::Config::load();
83    let debug = cfg.app_debug;
84
85    if debug {
86        let port = cfg.vite_port;
87        // Deteksi host secara dinamis dari header request 'host' agar support beda device (misal HP)
88        let mut display_host = "localhost".to_string();
89        if let Some(host_hdr) = req.headers.get("host") {
90            let parts: Vec<&str> = host_hdr.split(':').collect();
91            if !parts.is_empty() {
92                let ip_or_domain = parts[0];
93                if ip_or_domain != "localhost" && ip_or_domain != "127.0.0.1" && !ip_or_domain.is_empty() {
94                    display_host = ip_or_domain.to_string();
95                }
96            }
97        }
98        
99        if display_host == "localhost" {
100            let host = &cfg.app_host;
101            if host != "0.0.0.0" && !host.is_empty() {
102                display_host = host.clone();
103            }
104        }
105
106        // Mode Development: Hubungkan ke Vite Dev Server kustom host dan port
107        format!(
108            r#"
109        <!-- Vite Dev Server Integration -->
110         <script type="module">
111          import RefreshRuntime from 'http://{host}:{port}/@react-refresh';
112          RefreshRuntime.injectIntoGlobalHook(window);
113          window.$RefreshReg$ = () => {{}};
114          window.$RefreshSig$ = () => (type) => type;
115          window.__vite_plugin_react_preamble_installed__ = true;
116        </script>
117        <script type="module" src="http://{host}:{port}/src/resources/js/main.tsx"></script>
118        "#,
119            host = display_host,
120            port = port
121        )
122    } else {
123        // Mode Production: Baca manifest.json dari build hasil compile Vite
124        let mut manifest_content = String::new();
125        let paths = ["src/dist/.vite/manifest.json", "src/dist/manifest.json"];
126        for path in &paths {
127            if let Ok(content) = fs::read_to_string(path) {
128                manifest_content = content;
129                break;
130            }
131        }
132        
133        // Fallback ke EmbeddedPublic jika file di disk tidak ditemukan (misal: production standalone binary)
134        if manifest_content.is_empty() {
135            if let Some(f) = crate::server::get_embedded_public_fn() {
136                if let Some(file) = f(".vite/manifest.json")
137                    && let Ok(content) = String::from_utf8(file.data.to_vec()) {
138                    manifest_content = content;
139                } else if let Some(file) = f("manifest.json")
140                    && let Ok(content) = String::from_utf8(file.data.to_vec()) {
141                    manifest_content = content;
142                }
143            }
144        }
145
146        if !manifest_content.is_empty()
147            && let Ok(manifest) = crate::serde_json::from_str::<Value>(&manifest_content)
148            && let Some(entry) = manifest.get("src/resources/js/main.tsx") {
149                let file = entry.get("file").and_then(|f| f.as_str()).unwrap_or("assets/main.js");
150                let mut assets_html = format!(r#"<script type="module" src="/{}"></script>"#, file);
151                
152                if let Some(css_arr) = entry.get("css").and_then(|c| c.as_array()) {
153                    for css in css_arr {
154                        if let Some(css_str) = css.as_str() {
155                            assets_html = format!(r#"<link rel="stylesheet" href="/{}" />"#, css_str) + &assets_html;
156                        }
157                    }
158                }
159                return assets_html;
160        }
161        
162        // Fallback jika manifest.json tidak ditemukan
163        r#"<script type="module" src="/assets/main.js"></script>"#.to_string()
164    }
165}