1use crate::{
2 config::{Config, PackageManager},
3 js_commands::{bun_build, deno_build, npm_build, pnpm_build, yarn_build},
4 struct_extractor::extract_struct_def,
5};
6use regex::Regex;
7use std::{fs, io::Write, path::Path, process::ExitStatus};
8
9pub fn build_frontend() -> Result<(), Box<dyn std::error::Error>> {
10 let package_manager = Config::from_file().unwrap().package_manager;
11 let status: ExitStatus;
12
13 match package_manager {
14 PackageManager::Bun => {
15 status = bun_build()?;
16 }
17 PackageManager::Deno => {
18 status = deno_build()?;
19 }
20 PackageManager::NPM => {
21 status = npm_build()?;
22 }
23 PackageManager::PNPM => {
24 status = pnpm_build()?;
25 }
26 PackageManager::YARN => {
27 status = yarn_build()?;
28 }
29 }
30
31 if !status.success() {
32 return Err(format!(
33 "Failed to build the frontend, exit code: {:?}",
34 status.code()
35 )
36 .into());
37 }
38
39 Ok(())
40}
41
42pub fn rust_functions_to_axum_handlers(input_path: &str, output_path: &str) {
43 let content = fs::read_to_string(input_path).expect("Failed to read Rust file");
44
45 let mut file = fs::File::create(format!("{}/functions.rs", output_path))
46 .expect("Failed to create output Rust file");
47 file.write_all(content.as_bytes())
48 .expect("Failed to write to output Rust file");
49
50 let function_regex = Regex::new(r"fn\s+(\w+)\s*\(([^)]*)\)").expect("Invalid function regex");
52
53 let mut axum_content = String::from(
54 "mod functions;\n\nuse std::fs;\nuse axum::{Router, routing::{get,post}, response::{Html, IntoResponse}, Json};\nuse serde_json::json;\nuse tower_http::services::ServeDir;\nuse tower_livereload::LiveReloadLayer;\n\n",
55 );
56
57 let mut routes = Vec::new();
58
59 for cap in function_regex.captures_iter(&content) {
61 let function_name = &cap[1];
62 let params = &cap[2];
63
64 let mut param_names = Vec::new();
65 let mut param_types = Vec::new();
66
67 params
68 .split(',')
69 .filter(|p| !p.trim().is_empty())
70 .for_each(|p| {
71 let parts: Vec<&str> = p.trim().split(':').collect();
72 if parts.len() == 2 {
73 let param_name = parts[0].trim();
74 let param_type = parts[1].trim();
75 param_names.push(param_name);
76 param_types.push(param_type);
77 }
78 });
79
80 let handler_name = format!("{}_handler", function_name);
82 let handler = format!(
83 "async fn {}(Json(({})): Json<({})>) -> impl IntoResponse {{\n let response = functions::{}({});\n Json(json!(response))\n}}\n\n",
84 handler_name,param_names.join(","),param_types.join(","), function_name,param_names.join(","),
85 );
86
87 axum_content.push_str(&handler);
88 routes.push(format!(
89 " .route(\"/{0}\", post({0}_handler))",
90 function_name
91 ));
92 }
93
94 axum_content
96 .push_str("\npub fn create_router() -> Router {\n let mut router = Router::new()\n");
97 axum_content.push_str(&routes.join("\n"));
98 axum_content.push_str(";\n");
99 axum_content.push_str("router = router.nest_service(
100 \"/assets\",
101 ServeDir::new(&format!(\"{}/{}\", env!(\"CARGO_MANIFEST_DIR\"), \"src/static/assets\")),
102 );\n");
103 axum_content.push_str(
104 "
105 router = router.fallback(get({
106 match fs::read_to_string(format!(
107 \"{}/{}\",
108 env!(\"CARGO_MANIFEST_DIR\"),
109 \"src/static/index.html\"
110 )) {
111 Ok(contents) => Html(contents),
112 Err(_) => Html(\"Error reading file\".to_string()),
113 }
114 }));\n",
115 );
116 axum_content.push_str("router = router.layer(LiveReloadLayer::new());\n");
117 axum_content.push_str("return router;");
118 axum_content.push_str("\n}\n");
119
120 let mut file = fs::File::create(format!("{}/mod.rs", output_path))
122 .expect("Failed to create output Rust file");
123 file.write_all(axum_content.as_bytes())
124 .expect("Failed to write to output Rust file");
125}
126
127pub fn rust_to_typescript_type(
128 rust_type: &str,
129 custom_types: &Vec<String>,
130 content: &str,
131 project_name: &str,
132) -> String {
133 match rust_type {
134 "i32" | "i64" | "u32" | "u64" | "f32" | "f64" => "number".to_string(),
135 "String" | "&str" => "string".to_string(),
136 "bool" => "boolean".to_string(),
137 "Vec" => "Array<any>".to_string(),
138 "Option" => "any | null".to_string(),
139 _ if custom_types.contains(&rust_type.to_string()) => rust_type.to_string(),
140 _ => match extract_struct_def(rust_type.to_string(), content, project_name) {
141 Some(ts_type) => ts_type,
142 None => "any".to_string(),
143 },
144 }
145}
146
147pub fn rust_to_typescript_functons<P: AsRef<Path>>(
148 input_path: P,
149 output_path: P,
150 project_name: &str,
151) {
152 let content = fs::read_to_string(input_path).expect("Failed to read Rust file");
153 let struct_regex = Regex::new(r"struct\s+(\w+)\s*\{([^}]*)\}").expect("Invalid struct regex");
154 let function_regex =
155 Regex::new(r"fn\s+(\w+)\s*\(([^)]*)\)\s*->\s*([^{]+)").expect("Invalid function regex");
156
157 let mut ts_content = String::from("import axios from 'axios'\n\n");
158 let mut custom_types = Vec::new();
159
160 for cap in struct_regex.captures_iter(&content) {
161 let struct_name = &cap[1];
162 let fields = &cap[2];
163 custom_types.push(struct_name.to_string());
164
165 let ts_fields: Vec<String> = fields
166 .lines()
167 .filter(|line| !line.trim().is_empty())
168 .map(|line| {
169 let parts: Vec<&str> = line.trim().split(':').collect();
170 if parts.len() == 2 {
171 let field_name = parts[0].trim();
172 let field_type = rust_to_typescript_type(
173 parts[1].trim().trim_end_matches(','),
174 &custom_types,
175 &content,
176 project_name,
177 );
178 format!(" {}: {};", field_name, field_type)
179 } else {
180 String::new()
181 }
182 })
183 .collect();
184
185 ts_content.push_str(&format!(
186 "export interface {} {{\n{}\n}}\n\n",
187 struct_name,
188 ts_fields.join("\n").replace("pub", "")
189 ));
190 }
191
192 for cap in function_regex.captures_iter(&content) {
194 let function_name = &cap[1];
195 let params = &cap[2];
196 let return_type = &cap[3].trim();
197
198 let mut ts_params_without_types: Vec<String> = vec![];
201 let ts_params: Vec<String> = params
202 .split(',')
203 .filter(|p| !p.trim().is_empty())
204 .map(|p| {
205 let parts: Vec<&str> = p.trim().split(':').collect();
206 if parts.len() == 2 {
207 let param_name = parts[0].trim();
208 match parts[1].trim() {
209 "i32" | "i64" | "u32" | "u64" => ts_params_without_types.push(format!(
210 "parseInt({}.toString(), 10)",
211 param_name.to_string()
212 )),
213 "f32" | "f64" => ts_params_without_types
214 .push(format!("parseFloat({}.toString())", param_name.to_string())),
215 _ => ts_params_without_types.push(param_name.to_string()),
216 }
217 let param_type = rust_to_typescript_type(
218 parts[1].trim(),
219 &custom_types,
220 &content,
221 project_name,
222 );
223 format!("{}: {}", param_name, param_type)
224 } else {
225 "unknown: any".to_string()
226 }
227 })
228 .collect();
229
230 let ts_return_type =
232 rust_to_typescript_type(return_type, &custom_types, &content, project_name);
233
234 ts_content.push_str(&format!(
236 "export async function {}({}): Promise<{}>{{\nlet base_url = window.origin;\nlet data:any = [{}];\n let response = await axios.post(`${{base_url}}/{}`, data);\n return response.data;\n}}\n\n",
237 function_name,
238 ts_params.join(", "),
239 ts_return_type,
240 ts_params_without_types.join(", "),
241 function_name,
242 ));
243 }
244
245 let mut ts_file = fs::File::create(output_path).expect("Failed to create TypeScript file");
247 ts_file
248 .write_all(ts_content.as_bytes())
249 .expect("Failed to write to TypeScript file");
250}