@ezetgalaxy/titan 25.14.5 → 25.14.6

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.
package/README.md CHANGED
@@ -172,7 +172,7 @@ Titan now includes a built-in server-side `fetch` bridge powered by Rust.
172
172
  Use it to call any external API:
173
173
 
174
174
  ```js
175
- export function hello(req) {
175
+ function hello(req) {
176
176
  const body = JSON.stringify({
177
177
  model: "gpt-4.1-mini",
178
178
  messages: [{ role: "user", content: "hii" }]
package/index.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  import fs from "fs";
3
3
  import path from "path";
4
4
  import { execSync, spawn } from "child_process";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ezetgalaxy/titan",
3
- "version": "25.14.5",
3
+ "version": "25.14.6",
4
4
  "description": "JavaScript backend framework that compiles your JS into a Rust + Axum server.",
5
5
  "license": "ISC",
6
6
  "author": "ezetgalaxy",
@@ -1,6 +1,5 @@
1
1
  function hello(req) {
2
- const name = req.name;
3
- return { name: name, msg: `Hello ${name}` }
2
+ return { "name": `${req.name || "user"}`, msg: `welcome to titan planet ${req.name || "user"}` }
4
3
  }
5
4
 
6
5
  globalThis.hello = hello;
@@ -1,4 +1,5 @@
1
- use std::{collections::HashMap, fs, path::PathBuf, sync::Arc};
1
+ // server/src/main.rs
2
+ use std::{collections::HashMap, env, fs, path::PathBuf, sync::Arc, path::Path};
2
3
 
3
4
  use anyhow::Result;
4
5
  use axum::{
@@ -10,25 +11,18 @@ use axum::{
10
11
  Router,
11
12
  };
12
13
 
13
- use std::path::Path;
14
+ use boa_engine::{object::ObjectInitializer, Context, JsValue, Source};
15
+ use boa_engine::{js_string, native_function::NativeFunction, property::Attribute};
14
16
 
15
- use boa_engine::{
16
- js_string,
17
- native_function::NativeFunction,
18
- object::ObjectInitializer,
19
- property::Attribute,
20
- Context, JsValue, Source,
21
- };
22
-
23
- use reqwest::blocking::Client;
24
17
  use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
18
+ use reqwest::blocking::Client;
25
19
 
26
20
  use serde::Deserialize;
27
21
  use serde_json::Value;
28
-
29
22
  use tokio::net::TcpListener;
30
23
  use tokio::task;
31
24
 
25
+ /// Route configuration (loaded from routes.json)
32
26
  #[derive(Debug, Deserialize)]
33
27
  struct RouteVal {
34
28
  r#type: String,
@@ -38,244 +32,335 @@ struct RouteVal {
38
32
  #[derive(Clone)]
39
33
  struct AppState {
40
34
  routes: Arc<HashMap<String, RouteVal>>,
35
+ project_root: PathBuf,
36
+ }
37
+
38
+ // -------------------------
39
+ // ACTION DIRECTORY RESOLUTION
40
+ // -------------------------
41
+
42
+ fn resolve_actions_dir() -> PathBuf {
43
+ // Respect explicit override first
44
+ if let Ok(override_dir) = env::var("TITAN_ACTIONS_DIR") {
45
+ return PathBuf::from(override_dir);
46
+ }
47
+
48
+ // Production container layout
49
+ if Path::new("/app/actions").exists() {
50
+ return PathBuf::from("/app/actions");
51
+ }
52
+
53
+ // Try to walk up from the executing binary to discover `<...>/server/actions`
54
+ if let Ok(exe) = std::env::current_exe() {
55
+ if let Some(parent) = exe.parent() {
56
+ if let Some(target_dir) = parent.parent() {
57
+ if let Some(server_dir) = target_dir.parent() {
58
+ let candidate = server_dir.join("actions");
59
+ if candidate.exists() {
60
+ return candidate;
61
+ }
62
+ }
63
+ }
64
+ }
65
+ }
66
+
67
+ // Fall back to local ./actions
68
+ PathBuf::from("./actions")
41
69
  }
42
70
 
71
+ /// Try to find the directory that contains compiled action bundles.
72
+ ///
73
+ /// Checks multiple likely paths to support both dev and production container layouts:
74
+ /// - <project_root>/server/actions
75
+ /// - <project_root>/actions
76
+ /// - <project_root>/../server/actions
77
+ /// - /app/actions
78
+ /// - ./actions
79
+ fn find_actions_dir(project_root: &PathBuf) -> Option<PathBuf> {
80
+ let candidates = [
81
+ project_root.join("server").join("actions"),
82
+ project_root.join("actions"),
83
+ project_root.join("..").join("server").join("actions"),
84
+ PathBuf::from("/app").join("actions"),
85
+ PathBuf::from("actions"),
86
+ ];
87
+
88
+ for p in &candidates {
89
+ if p.exists() && p.is_dir() {
90
+ return Some(p.clone());
91
+ }
92
+ }
93
+
94
+ None
95
+ }
96
+
97
+ /// Injects a synchronous `t.fetch(url, opts?)` function into the Boa `Context`.
98
+ ///
99
+ /// Implementation details:
100
+ /// - Converts JS opts → `serde_json::Value` (owned) using `to_json`.
101
+ /// - Executes reqwest blocking client inside `tokio::task::block_in_place` to avoid blocking async runtime.
102
+ /// - Returns `{ ok: bool, status?: number, body?: string, error?: string }`.
43
103
  fn inject_t_fetch(ctx: &mut Context) {
104
+ // Native function (Boa 0.20) using from_fn_ptr
44
105
  let t_fetch_native = NativeFunction::from_fn_ptr(|_this, args, ctx| {
106
+ // Extract URL (owned string)
45
107
  let url = args
46
108
  .get(0)
47
109
  .and_then(|v| v.to_string(ctx).ok())
48
110
  .map(|s| s.to_std_string_escaped())
49
111
  .unwrap_or_default();
50
112
 
113
+ // Extract opts -> convert to serde_json::Value (owned)
51
114
  let opts_js = args.get(1).cloned().unwrap_or(JsValue::undefined());
52
- let opts_json: Value = opts_js.to_json(ctx).unwrap_or(Value::Null);
115
+ let opts_json: Value = match opts_js.to_json(ctx) {
116
+ Ok(v) => v,
117
+ Err(_) => Value::Object(serde_json::Map::new()),
118
+ };
53
119
 
120
+ // Pull method, body, headers into owned Rust values
54
121
  let method = opts_json
55
122
  .get("method")
56
123
  .and_then(|m| m.as_str())
57
- .unwrap_or("GET")
58
- .to_string();
124
+ .map(|s| s.to_string())
125
+ .unwrap_or_else(|| "GET".to_string());
59
126
 
60
- let body_opt = opts_json.get("body").cloned();
127
+ let body_opt = match opts_json.get("body") {
128
+ Some(Value::String(s)) => Some(s.clone()),
129
+ Some(other) => Some(other.to_string()),
130
+ None => None,
131
+ };
61
132
 
62
- let mut headers = HeaderMap::new();
133
+ // headers as Vec<(String,String)>
134
+ let mut header_pairs: Vec<(String, String)> = Vec::new();
63
135
  if let Some(Value::Object(map)) = opts_json.get("headers") {
64
- for (k, v) in map {
65
- if let Ok(name) = HeaderName::from_bytes(k.as_bytes()) {
66
- if let Ok(val) = HeaderValue::from_str(&v.to_string()) {
67
- headers.insert(name, val);
68
- }
69
- }
136
+ for (k, v) in map.iter() {
137
+ let v_str = match v {
138
+ Value::String(s) => s.clone(),
139
+ other => other.to_string(),
140
+ };
141
+ header_pairs.push((k.clone(), v_str));
70
142
  }
71
143
  }
72
144
 
73
- let out = task::block_in_place(move || {
145
+ // Perform the blocking HTTP request inside block_in_place to avoid runtime panic
146
+ let out_json = task::block_in_place(move || {
74
147
  let client = Client::new();
75
- let mut req = client.request(method.parse().unwrap(), &url);
76
148
 
77
- req = req.headers(headers);
149
+ let method_parsed = method.parse().unwrap_or(reqwest::Method::GET);
150
+ let mut req = client.request(method_parsed, &url);
151
+
152
+ if !header_pairs.is_empty() {
153
+ let mut headers = HeaderMap::new();
154
+ for (k, v) in header_pairs.into_iter() {
155
+ if let (Ok(name), Ok(val)) =
156
+ (HeaderName::from_bytes(k.as_bytes()), HeaderValue::from_str(&v))
157
+ {
158
+ headers.insert(name, val);
159
+ }
160
+ }
161
+ req = req.headers(headers);
162
+ }
78
163
 
79
164
  if let Some(body) = body_opt {
80
- req = req.body(body.to_string());
165
+ req = req.body(body);
81
166
  }
82
167
 
83
168
  match req.send() {
84
169
  Ok(resp) => {
85
170
  let status = resp.status().as_u16();
86
171
  let text = resp.text().unwrap_or_default();
87
- serde_json::json!({ "ok": true, "status": status, "body": text })
172
+ serde_json::json!({
173
+ "ok": true,
174
+ "status": status,
175
+ "body": text
176
+ })
88
177
  }
89
- Err(e) => serde_json::json!({ "ok": false, "error": e.to_string() }),
178
+ Err(e) => serde_json::json!({
179
+ "ok": false,
180
+ "error": e.to_string()
181
+ }),
90
182
  }
91
183
  });
92
184
 
93
- Ok(JsValue::from_json(&out, ctx).unwrap())
185
+ // Convert serde_json::Value -> JsValue
186
+ Ok(JsValue::from_json(&out_json, ctx).unwrap_or(JsValue::undefined()))
94
187
  });
95
188
 
189
+ // Convert native function to JS function object (requires Realm)
96
190
  let realm = ctx.realm();
97
191
  let t_fetch_js_fn = t_fetch_native.to_js_function(realm);
98
192
 
193
+ // Build `t` object with `.fetch`
99
194
  let t_obj = ObjectInitializer::new(ctx)
100
195
  .property(js_string!("fetch"), t_fetch_js_fn, Attribute::all())
101
196
  .build();
102
197
 
103
198
  ctx.global_object()
104
199
  .set(js_string!("t"), JsValue::from(t_obj), false, ctx)
105
- .unwrap();
200
+ .expect("set global t");
106
201
  }
107
202
 
108
- /// DEV: actions inside project/server/actions
109
- /// PROD: actions inside /app/actions
110
-
111
- fn detect_project_root() -> PathBuf {
112
- let exe = std::env::current_exe().unwrap();
113
- let dir = exe.parent().unwrap(); // target/debug
114
- let is_release = dir.ends_with("release");
115
-
116
- if is_release {
117
- // Production
118
- PathBuf::from("/app")
119
- } else {
120
- // Dev executable location:
121
- // <project>/server/target/debug/server.exe
122
- dir.parent() // target
123
- .unwrap()
124
- .parent() // server
125
- .unwrap()
126
- .parent() // <project>
127
- .unwrap()
128
- .to_path_buf()
129
- }
130
- }
131
-
132
-
133
- fn resolve_actions_dir() -> PathBuf {
134
- let exe = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("."));
135
- let exe_dir = exe.parent().unwrap_or(Path::new("/app")).to_path_buf();
136
-
137
- // Detect production if running from Docker binary location
138
- let is_prod = exe_dir.to_string_lossy().contains("/app")
139
- || exe.file_name().unwrap_or_default() == "titan-server";
203
+ // Root/dynamic handlers -----------------------------------------------------
140
204
 
141
- if is_prod {
142
- // Final production directory
143
- return PathBuf::from("/app/actions");
144
- }
145
-
146
- // DEV: exe = <root>/server/target/debug/server(.exe)
147
- exe_dir
148
- .parent() // target
149
- .unwrap_or(Path::new("."))
150
- .parent() // server
151
- .unwrap_or(Path::new("."))
152
- .join("actions")
205
+ async fn root_route(state: State<AppState>, req: Request<Body>) -> impl IntoResponse {
206
+ dynamic_handler_inner(state, req).await
153
207
  }
154
208
 
209
+ async fn dynamic_route(state: State<AppState>, req: Request<Body>) -> impl IntoResponse {
210
+ dynamic_handler_inner(state, req).await
211
+ }
155
212
 
156
-
157
- async fn dynamic_handler(
213
+ /// Main handler: looks up routes.json and executes action bundles using Boa.
214
+ async fn dynamic_handler_inner(
158
215
  State(state): State<AppState>,
159
216
  req: Request<Body>,
160
217
  ) -> impl IntoResponse {
161
218
  let method = req.method().as_str().to_uppercase();
162
- let path = req.uri().path().to_string();
219
+ let path = req.uri().path();
163
220
  let key = format!("{}:{}", method, path);
164
221
 
165
222
  let body_bytes = match to_bytes(req.into_body(), usize::MAX).await {
166
- Ok(v) => v,
223
+ Ok(b) => b,
167
224
  Err(_) => return (StatusCode::BAD_REQUEST, "Failed to read body").into_response(),
168
225
  };
169
226
  let body_str = String::from_utf8_lossy(&body_bytes).to_string();
170
227
 
171
- let route = match state.routes.get(&key) {
172
- Some(r) => r,
173
- None => return (StatusCode::NOT_FOUND, "Not Found").into_response(),
174
- };
228
+ if let Some(route) = state.routes.get(&key) {
229
+ match route.r#type.as_str() {
230
+ "action" => {
231
+ let action_name = route.value.as_str().unwrap_or("").trim();
232
+ if action_name.is_empty() {
233
+ return (StatusCode::INTERNAL_SERVER_ERROR, "Invalid action name").into_response();
234
+ }
175
235
 
176
- if route.r#type != "action" {
177
- if let Some(s) = route.value.as_str() {
178
- return s.to_string().into_response();
179
- }
180
- return Json(route.value.clone()).into_response();
181
- }
236
+ // Resolve actions directory: prefer resolve_actions_dir(), fall back to heuristic find_actions_dir
237
+ let resolved = resolve_actions_dir();
238
+ let actions_dir = if resolved.exists() && resolved.is_dir() {
239
+ resolved
240
+ } else {
241
+ match find_actions_dir(&state.project_root) {
242
+ Some(p) => p,
243
+ None => {
244
+ return (
245
+ StatusCode::INTERNAL_SERVER_ERROR,
246
+ format!("Actions directory not found (checked multiple locations)"),
247
+ )
248
+ .into_response();
249
+ }
250
+ }
251
+ };
182
252
 
183
- let action_name = route.value.as_str().unwrap().trim().to_string();
184
- if action_name.is_empty() {
185
- return (
186
- StatusCode::INTERNAL_SERVER_ERROR,
187
- "Invalid action name",
188
- )
189
- .into_response();
190
- }
253
+ let action_path = actions_dir.join(format!("{}.jsbundle", action_name));
191
254
 
192
- let actions_dir = resolve_actions_dir();
193
- let action_path = actions_dir.join(format!("{}.jsbundle", action_name));
255
+ if !action_path.exists() {
256
+ return (
257
+ StatusCode::NOT_FOUND,
258
+ format!("Action bundle not found: {:?}", action_path),
259
+ )
260
+ .into_response();
261
+ }
194
262
 
195
- if !action_path.exists() {
196
- return (
197
- StatusCode::NOT_FOUND,
198
- format!("Action bundle not found: {:?}", action_path),
199
- )
200
- .into_response();
201
- }
263
+ let js_code = match fs::read_to_string(&action_path) {
264
+ Ok(v) => v,
265
+ Err(e) => {
266
+ return (
267
+ StatusCode::INTERNAL_SERVER_ERROR,
268
+ format!("Failed reading action bundle: {}", e),
269
+ )
270
+ .into_response();
271
+ }
272
+ };
202
273
 
203
- let js_code = match fs::read_to_string(&action_path) {
204
- Ok(v) => v,
205
- Err(e) => {
206
- return (
207
- StatusCode::INTERNAL_SERVER_ERROR,
208
- format!("Failed reading bundle: {}", e),
209
- )
210
- .into_response();
211
- }
212
- };
274
+ // Build env object
275
+ let mut env_map = serde_json::Map::new();
276
+ for (k, v) in std::env::vars() {
277
+ env_map.insert(k, Value::String(v));
278
+ }
279
+ let env_json = Value::Object(env_map);
280
+
281
+ // Injected script: sets process.env and __titan_req and invokes action function.
282
+ let injected = format!(
283
+ r#"
284
+ globalThis.process = {{ env: {} }};
285
+ const __titan_req = {};
286
+ {};
287
+ {}(__titan_req);
288
+ "#,
289
+ env_json.to_string(),
290
+ body_str,
291
+ js_code,
292
+ action_name
293
+ );
294
+
295
+ let mut ctx = Context::default();
296
+ inject_t_fetch(&mut ctx);
297
+
298
+ let result = match ctx.eval(Source::from_bytes(&injected)) {
299
+ Ok(v) => v,
300
+ Err(e) => return Json(json_error(e.to_string())).into_response(),
301
+ };
302
+
303
+ let result_json: Value = match result.to_json(&mut ctx) {
304
+ Ok(v) => v,
305
+ Err(e) => json_error(e.to_string()),
306
+ };
307
+
308
+ return Json(result_json).into_response();
309
+ }
213
310
 
214
- let mut env_map = serde_json::Map::new();
215
- for (k, v) in std::env::vars() {
216
- env_map.insert(k, Value::String(v));
311
+ "json" => return Json(route.value.clone()).into_response(),
312
+ _ => {
313
+ if let Some(s) = route.value.as_str() {
314
+ return s.to_string().into_response();
315
+ }
316
+ return route.value.to_string().into_response();
317
+ }
318
+ }
217
319
  }
218
- let env_json = Value::Object(env_map);
219
-
220
- let injected = format!(
221
- r#"
222
- globalThis.process = {{ env: {} }};
223
- const __titan_req = {};
224
- {};
225
- {}(__titan_req);
226
- "#,
227
- env_json.to_string(),
228
- body_str,
229
- js_code,
230
- action_name
231
- );
232
-
233
- let mut ctx = Context::default();
234
- inject_t_fetch(&mut ctx);
235
-
236
- let result = match ctx.eval(Source::from_bytes(&injected)) {
237
- Ok(v) => v,
238
- Err(e) => return Json(serde_json::json!({ "error": e.to_string() })).into_response(),
239
- };
240
320
 
241
- let result_json = result.to_json(&mut ctx).unwrap_or(Value::Null);
242
- Json(result_json).into_response()
321
+ (StatusCode::NOT_FOUND, "Not Found").into_response()
322
+ }
323
+
324
+ fn json_error(msg: String) -> Value {
325
+ serde_json::json!({ "error": msg })
243
326
  }
244
327
 
328
+ // Entrypoint ---------------------------------------------------------------
329
+
245
330
  #[tokio::main]
246
331
  async fn main() -> Result<()> {
247
332
  dotenvy::dotenv().ok();
248
333
 
249
- let raw = fs::read_to_string("./routes.json").unwrap_or("{}".into());
334
+ // Load routes.json (expected at runtime root)
335
+ let raw = fs::read_to_string("./routes.json").unwrap_or_else(|_| "{}".to_string());
250
336
  let json: Value = serde_json::from_str(&raw).unwrap_or_default();
251
337
 
252
338
  let port = json["__config"]["port"].as_u64().unwrap_or(3000);
253
- let routes_map: HashMap<String, RouteVal> =
254
- serde_json::from_value(json["routes"].clone()).unwrap_or_default();
255
-
256
- let _project_root = detect_project_root();
339
+ let routes_json = json["routes"].clone();
340
+ let map: HashMap<String, RouteVal> = serde_json::from_value(routes_json).unwrap_or_default();
257
341
 
258
- let state = AppState {
259
- routes: Arc::new(routes_map),
260
- };
342
+ // Project root heuristics: try current_dir()
343
+ let project_root = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
261
344
 
262
-
345
+ let state = AppState {
346
+ routes: Arc::new(map),
347
+ project_root,
348
+ };
263
349
 
264
350
  let app = Router::new()
265
- .route("/", any(dynamic_handler))
266
- .fallback(any(dynamic_handler))
351
+ .route("/", any(root_route))
352
+ .fallback(any(dynamic_route))
267
353
  .with_state(state);
268
354
 
269
355
  let listener = TcpListener::bind(format!("0.0.0.0:{}", port)).await?;
270
- // TITAN BANNER
271
- println!("\n\x1b[38;5;208m████████╗██╗████████╗ █████╗ ███╗ ██╗");
272
- println!("╚══██╔══╝██║╚══██╔══╝██╔══██╗████╗ ██║");
273
- println!(" ██║ ██║ ██║ ███████║██╔██╗ ██║");
274
- println!(" ██║ ██║ ██║ ██╔══██║██║╚██╗██║");
275
- println!(" ██║ ██║ ██║ ██║ ██║██║ ╚████║");
276
- println!(" ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝\x1b[0m\n");
277
-
278
356
 
357
+ // Banner (yellow-orange) and server info
358
+ println!("\n\x1b[38;5;208m████████╗██╗████████╗ █████╗ ███╗ ██╗");
359
+ println!("╚══██╔══╝██║╚══██╔══╝██╔══██╗████╗ ██║");
360
+ println!(" ██║ ██║ ██║ ███████║██╔██╗ ██║");
361
+ println!(" ██║ ██║ ██║ ██╔══██║██║╚██╗██║");
362
+ println!(" ██║ ██║ ██║ ██║ ██║██║ ╚████║");
363
+ println!(" ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝\x1b[0m\n");
279
364
  println!("\x1b[38;5;39mTitan server running at:\x1b[0m http://localhost:{}", port);
280
365
 
281
366
  axum::serve(listener, app).await?;
@@ -28,12 +28,16 @@ export async function bundle() {
28
28
 
29
29
  await esbuild.build({
30
30
  entryPoints: [entry],
31
+ outfile: outfile,
31
32
  bundle: true,
32
- format: "cjs",
33
+ format: "iife",
33
34
  platform: "neutral",
34
- outfile,
35
- minify: false,
36
- });
35
+ target: "es2020",
36
+ banner: {
37
+ js: ""
38
+ }
39
+ });
40
+
37
41
  }
38
42
 
39
43
  console.log("[Titan] Bundling finished.");