@ezetgalaxy/titan 26.7.1 → 26.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,28 +1,27 @@
1
- use std::{collections::HashMap, fs, path::PathBuf, sync::Arc};
2
1
  use anyhow::Result;
3
2
  use axum::{
4
- body::{to_bytes, Body},
3
+ Router,
4
+ body::{Body, to_bytes},
5
5
  extract::State,
6
6
  http::{Request, StatusCode},
7
7
  response::{IntoResponse, Json},
8
8
  routing::any,
9
- Router,
10
9
  };
11
10
  use serde_json::Value;
12
- use tokio::net::TcpListener;
13
11
  use std::time::Instant;
12
+ use std::{collections::HashMap, fs, path::PathBuf, sync::Arc};
13
+ use tokio::net::TcpListener;
14
14
 
15
15
  mod utils;
16
16
 
17
- mod extensions;
18
17
  mod action_management;
18
+ mod extensions;
19
19
 
20
- use utils::{blue, white, yellow, green, gray, red};
21
- use extensions::{init_v8, inject_extensions};
22
20
  use action_management::{
23
- resolve_actions_dir, find_actions_dir, match_dynamic_route,
24
- DynamicRoute, RouteVal
21
+ DynamicRoute, RouteVal, find_actions_dir, match_dynamic_route, resolve_actions_dir,
25
22
  };
23
+ use extensions::{init_v8, inject_extensions};
24
+ use utils::{blue, gray, green, red, white, yellow};
26
25
 
27
26
  #[derive(Clone)]
28
27
  struct AppState {
@@ -45,7 +44,6 @@ async fn dynamic_handler_inner(
45
44
  State(state): State<AppState>,
46
45
  req: Request<Body>,
47
46
  ) -> impl IntoResponse {
48
-
49
47
  // ---------------------------
50
48
  // BASIC REQUEST INFO
51
49
  // ---------------------------
@@ -70,10 +68,7 @@ async fn dynamic_handler_inner(
70
68
  q.split('&')
71
69
  .filter_map(|pair| {
72
70
  let mut it = pair.splitn(2, '=');
73
- Some((
74
- it.next()?.to_string(),
75
- it.next().unwrap_or("").to_string(),
76
- ))
71
+ Some((it.next()?.to_string(), it.next().unwrap_or("").to_string()))
77
72
  })
78
73
  .collect()
79
74
  })
@@ -83,7 +78,7 @@ async fn dynamic_handler_inner(
83
78
  // HEADERS & BODY
84
79
  // ---------------------------
85
80
  let (parts, body) = req.into_parts();
86
-
81
+
87
82
  let headers = parts
88
83
  .headers
89
84
  .iter()
@@ -92,9 +87,7 @@ async fn dynamic_handler_inner(
92
87
 
93
88
  let body_bytes = match to_bytes(body, usize::MAX).await {
94
89
  Ok(b) => b,
95
- Err(_) => {
96
- return (StatusCode::BAD_REQUEST, "Failed to read request body").into_response()
97
- }
90
+ Err(_) => return (StatusCode::BAD_REQUEST, "Failed to read request body").into_response(),
98
91
  };
99
92
 
100
93
  let body_str = String::from_utf8_lossy(&body_bytes).to_string();
@@ -119,18 +112,32 @@ async fn dynamic_handler_inner(
119
112
  action_name = Some(name);
120
113
  } else if route.r#type == "json" {
121
114
  let elapsed = start.elapsed();
122
- println!("{} {} {} {}", blue("[Titan]"), white(&format!("{} {}", method, path)), white("→ json"), gray(&format!("in {:.2?}", elapsed)));
115
+ println!(
116
+ "{} {} {} {}",
117
+ blue("[Titan]"),
118
+ white(&format!("{} {}", method, path)),
119
+ white("→ json"),
120
+ gray(&format!("in {:.2?}", elapsed))
121
+ );
123
122
  return Json(route.value.clone()).into_response();
124
123
  } else if let Some(s) = route.value.as_str() {
125
124
  let elapsed = start.elapsed();
126
- println!("{} {} {} {}", blue("[Titan]"), white(&format!("{} {}", method, path)), white("→ reply"), gray(&format!("in {:.2?}", elapsed)));
125
+ println!(
126
+ "{} {} {} {}",
127
+ blue("[Titan]"),
128
+ white(&format!("{} {}", method, path)),
129
+ white("→ reply"),
130
+ gray(&format!("in {:.2?}", elapsed))
131
+ );
127
132
  return s.to_string().into_response();
128
133
  }
129
134
  }
130
135
 
131
136
  // Dynamic route
132
137
  if action_name.is_none() {
133
- if let Some((action, p)) = match_dynamic_route(&method, &path, state.dynamic_routes.as_slice()) {
138
+ if let Some((action, p)) =
139
+ match_dynamic_route(&method, &path, state.dynamic_routes.as_slice())
140
+ {
134
141
  route_kind = "dynamic";
135
142
  route_label = action.clone();
136
143
  action_name = Some(action);
@@ -142,7 +149,13 @@ async fn dynamic_handler_inner(
142
149
  Some(a) => a,
143
150
  None => {
144
151
  let elapsed = start.elapsed();
145
- println!("{} {} {} {}", blue("[Titan]"), white(&format!("{} {}", method, path)), white("→ 404"), gray(&format!("in {:.2?}", elapsed)));
152
+ println!(
153
+ "{} {} {} {}",
154
+ blue("[Titan]"),
155
+ white(&format!("{} {}", method, path)),
156
+ white("→ 404"),
157
+ gray(&format!("in {:.2?}", elapsed))
158
+ );
146
159
  return (StatusCode::NOT_FOUND, "Not Found").into_response();
147
160
  }
148
161
  };
@@ -151,7 +164,8 @@ async fn dynamic_handler_inner(
151
164
  // LOAD ACTION
152
165
  // ---------------------------
153
166
  let resolved = resolve_actions_dir();
154
- let actions_dir = resolved.exists()
167
+ let actions_dir = resolved
168
+ .exists()
155
169
  .then(|| resolved)
156
170
  .or_else(|| find_actions_dir(&state.project_root))
157
171
  .unwrap();
@@ -164,17 +178,24 @@ async fn dynamic_handler_inner(
164
178
  }
165
179
  }
166
180
 
167
- let js_code = match fs::read_to_string(&action_path) {
168
- Ok(c) => c,
169
- Err(_) => {
170
- return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": "Action bundle not found", "action": action_name}))).into_response()
171
- }
172
- };
181
+ let js_code =
182
+ match fs::read_to_string(&action_path) {
183
+ Ok(c) => c,
184
+ Err(_) => return (
185
+ StatusCode::INTERNAL_SERVER_ERROR,
186
+ Json(
187
+ serde_json::json!({"error": "Action bundle not found", "action": action_name}),
188
+ ),
189
+ )
190
+ .into_response(),
191
+ };
173
192
 
174
193
  // ---------------------------
175
194
  // EXECUTE IN V8
176
195
  // ---------------------------
177
- let env_json = std::env::vars().map(|(k, v)| (k, Value::String(v))).collect::<serde_json::Map<_, _>>();
196
+ let env_json = std::env::vars()
197
+ .map(|(k, v)| (k, Value::String(v)))
198
+ .collect::<serde_json::Map<_, _>>();
178
199
 
179
200
  let injected = format!(
180
201
  r#"
@@ -215,43 +236,42 @@ async fn dynamic_handler_inner(
215
236
  // We can use `task::spawn_blocking`.
216
237
  let root = state.project_root.clone();
217
238
  let action_name_for_v8 = action_name.clone();
218
-
239
+
219
240
  let result_json: Value = tokio::task::spawn_blocking(move || {
220
241
  let isolate = &mut v8::Isolate::new(v8::CreateParams::default());
221
242
  let handle_scope = &mut v8::HandleScope::new(isolate);
222
243
  let context = v8::Context::new(handle_scope, v8::ContextOptions::default());
223
244
  let scope = &mut v8::ContextScope::new(handle_scope, context);
224
-
245
+
225
246
  let global = context.global(scope);
226
247
 
227
248
  // Inject extensions (t.read, etc)
228
249
  inject_extensions(scope, global);
229
-
230
- // Set metadata globals
250
+
231
251
  // Set metadata globals
232
252
  let root_str = v8::String::new(scope, root.to_str().unwrap_or(".")).unwrap();
233
253
  let root_key = v8::String::new(scope, "__titan_root").unwrap();
234
254
  global.set(scope, root_key.into(), root_str.into());
235
-
255
+
236
256
  let action_str = v8::String::new(scope, &action_name_for_v8).unwrap();
237
257
  let action_key = v8::String::new(scope, "__titan_action").unwrap();
238
258
  global.set(scope, action_key.into(), action_str.into());
239
-
259
+
240
260
  let source = v8::String::new(scope, &injected).unwrap();
241
-
261
+
242
262
  let try_catch = &mut v8::TryCatch::new(scope);
243
-
263
+
244
264
  let script = match v8::Script::compile(try_catch, source, None) {
245
- Some(s) => s,
246
- None => {
247
- let err = try_catch.message().unwrap();
248
- let msg = err.get(try_catch).to_rust_string_lossy(try_catch);
249
- return serde_json::json!({ "error": msg, "phase": "compile" });
250
- }
265
+ Some(s) => s,
266
+ None => {
267
+ let err = try_catch.message().unwrap();
268
+ let msg = err.get(try_catch).to_rust_string_lossy(try_catch);
269
+ return serde_json::json!({ "error": msg, "phase": "compile" });
270
+ }
251
271
  };
252
-
272
+
253
273
  let result = script.run(try_catch);
254
-
274
+
255
275
  match result {
256
276
  Some(val) => {
257
277
  // Convert v8 Value to Serde JSON
@@ -259,37 +279,59 @@ async fn dynamic_handler_inner(
259
279
  let json_obj = v8::json::stringify(try_catch, val).unwrap();
260
280
  let json_str = json_obj.to_rust_string_lossy(try_catch);
261
281
  serde_json::from_str(&json_str).unwrap_or(Value::Null)
262
- },
282
+ }
263
283
  None => {
264
- let err = try_catch.message().unwrap();
265
- let msg = err.get(try_catch).to_rust_string_lossy(try_catch);
266
- serde_json::json!({ "error": msg, "phase": "execution" })
284
+ let err = try_catch.message().unwrap();
285
+ let msg = err.get(try_catch).to_rust_string_lossy(try_catch);
286
+ serde_json::json!({ "error": msg, "phase": "execution" })
267
287
  }
268
288
  }
269
- }).await.unwrap_or(serde_json::json!({"error": "V8 task failed"}));
289
+ })
290
+ .await
291
+ .unwrap_or(serde_json::json!({"error": "V8 task failed"}));
270
292
 
271
293
  // ---------------------------
272
294
  // FINAL LOG
273
295
  // ---------------------------
274
296
  let elapsed = start.elapsed();
275
-
297
+
276
298
  // Check for errors in result
277
299
  if let Some(err) = result_json.get("error") {
278
- println!("{} {} {} {}", blue("[Titan]"), red(&format!("{} {}", method, path)), red("→ error"), gray(&format!("in {:.2?}", elapsed)));
300
+ println!(
301
+ "{} {} {} {}",
302
+ blue("[Titan]"),
303
+ red(&format!("{} {}", method, path)),
304
+ red("→ error"),
305
+ gray(&format!("in {:.2?}", elapsed))
306
+ );
279
307
  println!("{}", red(err.as_str().unwrap_or("Unknown")));
280
308
  return (StatusCode::INTERNAL_SERVER_ERROR, Json(result_json)).into_response();
281
309
  }
282
310
 
283
311
  match route_kind {
284
- "dynamic" => println!("{} {} {} {} {} {}", blue("[Titan]"), green(&format!("{} {}", method, path)), white("→"), green(&route_label), white("(dynamic)"), gray(&format!("in {:.2?}", elapsed))),
285
- "exact" => println!("{} {} {} {} {}", blue("[Titan]"), white(&format!("{} {}", method, path)), white("→"), yellow(&route_label), gray(&format!("in {:.2?}", elapsed))),
312
+ "dynamic" => println!(
313
+ "{} {} {} {} {} {}",
314
+ blue("[Titan]"),
315
+ green(&format!("{} {}", method, path)),
316
+ white("→"),
317
+ green(&route_label),
318
+ white("(dynamic)"),
319
+ gray(&format!("in {:.2?}", elapsed))
320
+ ),
321
+ "exact" => println!(
322
+ "{} {} {} {} {}",
323
+ blue("[Titan]"),
324
+ white(&format!("{} {}", method, path)),
325
+ white("→"),
326
+ yellow(&route_label),
327
+ gray(&format!("in {:.2?}", elapsed))
328
+ ),
286
329
  _ => {}
287
330
  }
288
331
 
289
332
  Json(result_json).into_response()
290
333
  }
291
334
 
292
-
293
335
  // Entrypoint ---------------------------------------------------------------
294
336
 
295
337
  #[tokio::main]
@@ -304,28 +346,22 @@ async fn main() -> Result<()> {
304
346
  let port = json["__config"]["port"].as_u64().unwrap_or(3000);
305
347
  let routes_json = json["routes"].clone();
306
348
  let map: HashMap<String, RouteVal> = serde_json::from_value(routes_json).unwrap_or_default();
307
- let dynamic_routes: Vec<DynamicRoute> = serde_json::from_value(json["__dynamic_routes"].clone()).unwrap_or_default();
349
+ let dynamic_routes: Vec<DynamicRoute> =
350
+ serde_json::from_value(json["__dynamic_routes"].clone()).unwrap_or_default();
308
351
 
309
- // Project root heuristics
310
- let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
311
- // Ensure we find the 'app' directory as sibling if running from 'server'
312
- let project_root = if cwd.join("../app").exists() {
313
- cwd.join("../app")
314
- } else {
315
- cwd
316
- };
352
+ // Identify project root (where .ext or node_modules lives)
353
+ let project_root = resolve_project_root();
317
354
 
318
355
  let state = AppState {
319
356
  routes: Arc::new(map),
320
357
  dynamic_routes: Arc::new(dynamic_routes),
321
358
  project_root: project_root.clone(),
322
359
  };
323
-
360
+
324
361
  // Load extensions
325
362
  extensions::load_project_extensions(project_root.clone());
326
363
 
327
364
  let app = Router::new()
328
-
329
365
  .route("/", any(root_route))
330
366
  .fallback(any(dynamic_route))
331
367
  .with_state(state);
@@ -338,8 +374,38 @@ async fn main() -> Result<()> {
338
374
  println!(" ██║ ██║ ██║ ██╔══██║██║╚██╗██║");
339
375
  println!(" ██║ ██║ ██║ ██║ ██║██║ ╚████║");
340
376
  println!(" ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝\x1b[0m\n");
341
- println!("\x1b[38;5;39mTitan server running at:\x1b[0m http://localhost:{}", port);
377
+ println!(
378
+ "\x1b[38;5;39mTitan server running at:\x1b[0m http://localhost:{}",
379
+ port
380
+ );
342
381
 
343
382
  axum::serve(listener, app).await?;
344
383
  Ok(())
345
384
  }
385
+
386
+ fn resolve_project_root() -> PathBuf {
387
+ // 1. Check CWD (preferred for local dev/tooling)
388
+ if let Ok(cwd) = std::env::current_dir() {
389
+ if cwd.join("node_modules").exists()
390
+ || cwd.join("package.json").exists()
391
+ || cwd.join(".ext").exists()
392
+ {
393
+ return cwd;
394
+ }
395
+ }
396
+
397
+ // 2. Check executable persistence (Docker / Production)
398
+ // Walk up from the executable to find .ext or node_modules
399
+ if let Ok(exe) = std::env::current_exe() {
400
+ let mut current = exe.parent();
401
+ while let Some(dir) = current {
402
+ if dir.join(".ext").exists() || dir.join("node_modules").exists() {
403
+ return dir.to_path_buf();
404
+ }
405
+ current = dir.parent();
406
+ }
407
+ }
408
+
409
+ // 3. Fallback to CWD
410
+ std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
411
+ }