@ezetgalaxy/titan 25.14.0 → 25.14.2

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/index.js CHANGED
@@ -189,49 +189,64 @@ function buildProd() {
189
189
  const appJs = path.join(root, "app", "app.js");
190
190
  const bundler = path.join(root, "titan", "bundle.js");
191
191
  const serverDir = path.join(root, "server");
192
- const builtActionsDir = path.join(root, "server", "actions");
193
- const actionsOut = path.join(serverDir, "actions");
194
- // Ensure app/app.js exists
192
+ const builtActionsDir = path.join(serverDir, "actions"); // REAL output
193
+ const actionsOut = builtActionsDir; // final output
194
+
195
+ // --------------------------------------------
196
+ // 0) Basic validation
197
+ // --------------------------------------------
195
198
  if (!fs.existsSync(appJs)) {
196
199
  console.log(red("ERROR: app/app.js not found."));
197
200
  process.exit(1);
198
201
  }
199
202
 
200
- // Ensure bundler exists
201
203
  if (!fs.existsSync(bundler)) {
202
204
  console.log(red("ERROR: titan/bundle.js not found."));
203
205
  process.exit(1);
204
206
  }
205
207
 
206
- // 1) Generate routes.json and action_map.json
208
+ // --------------------------------------------
209
+ // 1) Generate routes + action_map via DSL
210
+ // --------------------------------------------
207
211
  console.log(cyan("→ Generating Titan metadata..."));
208
212
  execSync("node app/app.js --build", { stdio: "inherit" });
209
213
 
214
+ // --------------------------------------------
210
215
  // 2) Bundle JS actions
216
+ // --------------------------------------------
211
217
  console.log(cyan("→ Bundling Titan actions..."));
212
218
  execSync("node titan/bundle.js", { stdio: "inherit" });
213
219
 
220
+ // --------------------------------------------
214
221
  // 3) Ensure server/actions exists
215
- fs.mkdirSync(actionsOut, { recursive: true }); // FIX: mandatory
216
-
217
- // 4) Copy bundled actions to server/actions
218
- if (fs.existsSync(builtActionsDir)) {
219
- for (const file of fs.readdirSync(builtActionsDir)) {
220
- if (file.endsWith(".jsbundle")) {
221
- console.log(cyan(`→ Copying ${file}`));
222
- fs.copyFileSync(
223
- path.join(builtActionsDir, file),
224
- path.join(actionsOut, file)
225
- );
226
- }
227
- }
228
- } else {
222
+ // --------------------------------------------
223
+ fs.mkdirSync(actionsOut, { recursive: true });
224
+
225
+ // --------------------------------------------
226
+ // 4) List & verify bundles
227
+ // --------------------------------------------
228
+ if (!fs.existsSync(builtActionsDir)) {
229
+ console.log(red("ERROR: Titan bundler did NOT generate server/actions directory."));
230
+ console.log(red("This means titan/bundle.js output path is incorrect."));
231
+ process.exit(1);
232
+ }
233
+
234
+ const bundles = fs.readdirSync(builtActionsDir)
235
+ .filter(f => f.endsWith(".jsbundle"));
236
+
237
+ if (bundles.length === 0) {
229
238
  console.log(yellow("Warning: No actions bundled."));
239
+ } else {
240
+ for (const file of bundles) {
241
+ console.log(cyan(`→ Found bundle: ${file}`));
242
+ }
230
243
  }
231
244
 
232
245
  console.log(green("✔ Actions copied to server/actions"));
233
246
 
234
- // 5) Build Rust binary
247
+ // --------------------------------------------
248
+ // 5) Build Rust server
249
+ // --------------------------------------------
235
250
  console.log(cyan("→ Building Rust release binary..."));
236
251
  execSync("cargo build --release", {
237
252
  cwd: serverDir,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ezetgalaxy/titan",
3
- "version": "25.14.0",
3
+ "version": "25.14.2",
4
4
  "description": "JavaScript backend framework that compiles your JS into a Rust + Axum server.",
5
5
  "license": "ISC",
6
6
  "author": "ezetgalaxy",
@@ -1,4 +1,3 @@
1
- // server/src/main.rs
2
1
  use std::{collections::HashMap, fs, path::PathBuf, sync::Arc};
3
2
 
4
3
  use anyhow::Result;
@@ -11,18 +10,23 @@ use axum::{
11
10
  Router,
12
11
  };
13
12
 
14
- use boa_engine::{object::ObjectInitializer, Context, JsValue, Source};
15
- use boa_engine::{js_string, native_function::NativeFunction, property::Attribute};
13
+ use boa_engine::{
14
+ js_string,
15
+ native_function::NativeFunction,
16
+ object::ObjectInitializer,
17
+ property::Attribute,
18
+ Context, JsValue, Source,
19
+ };
16
20
 
17
- use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
18
21
  use reqwest::blocking::Client;
22
+ use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
19
23
 
20
24
  use serde::Deserialize;
21
25
  use serde_json::Value;
26
+
22
27
  use tokio::net::TcpListener;
23
28
  use tokio::task;
24
29
 
25
- /// Route configuration (loaded from routes.json)
26
30
  #[derive(Debug, Deserialize)]
27
31
  struct RouteVal {
28
32
  r#type: String,
@@ -32,307 +36,229 @@ struct RouteVal {
32
36
  #[derive(Clone)]
33
37
  struct AppState {
34
38
  routes: Arc<HashMap<String, RouteVal>>,
39
+ /// Project root — used only in dev mode
35
40
  project_root: PathBuf,
36
41
  }
37
42
 
38
- /// Try to find the directory that contains compiled action bundles.
39
- ///
40
- /// Checks multiple likely paths to support both dev and production container layouts:
41
- /// - <project_root>/server/actions
42
- /// - <project_root>/actions
43
- /// - <project_root>/../server/actions
44
- /// - /app/actions
45
- /// - ./actions
46
- fn find_actions_dir(project_root: &PathBuf) -> Option<PathBuf> {
47
- let candidates = [
48
- project_root.join("server").join("actions"),
49
- project_root.join("actions"),
50
- project_root.join("..").join("server").join("actions"),
51
- PathBuf::from("/app").join("actions"),
52
- PathBuf::from("actions"),
53
- ];
54
-
55
- for p in &candidates {
56
- if p.exists() && p.is_dir() {
57
- return Some(p.clone());
58
- }
59
- }
60
-
61
- None
62
- }
63
-
64
- /// Injects a synchronous `t.fetch(url, opts?)` function into the Boa `Context`.
65
- ///
66
- /// Implementation details:
67
- /// - Converts JS opts → `serde_json::Value` (owned) using `to_json`.
68
- /// - Executes reqwest blocking client inside `tokio::task::block_in_place` to avoid blocking async runtime.
69
- /// - Returns `{ ok: bool, status?: number, body?: string, error?: string }`.
70
43
  fn inject_t_fetch(ctx: &mut Context) {
71
- // Native function (Boa 0.20) using from_fn_ptr
72
44
  let t_fetch_native = NativeFunction::from_fn_ptr(|_this, args, ctx| {
73
- // Extract URL (owned string)
74
45
  let url = args
75
46
  .get(0)
76
47
  .and_then(|v| v.to_string(ctx).ok())
77
48
  .map(|s| s.to_std_string_escaped())
78
49
  .unwrap_or_default();
79
50
 
80
- // Extract opts -> convert to serde_json::Value (owned)
81
51
  let opts_js = args.get(1).cloned().unwrap_or(JsValue::undefined());
82
- let opts_json: Value = match opts_js.to_json(ctx) {
83
- Ok(v) => v,
84
- Err(_) => Value::Object(serde_json::Map::new()),
85
- };
52
+ let opts_json: Value = opts_js.to_json(ctx).unwrap_or(Value::Null);
86
53
 
87
- // Pull method, body, headers into owned Rust values
88
54
  let method = opts_json
89
55
  .get("method")
90
56
  .and_then(|m| m.as_str())
91
- .map(|s| s.to_string())
92
- .unwrap_or_else(|| "GET".to_string());
57
+ .unwrap_or("GET")
58
+ .to_string();
93
59
 
94
- let body_opt = match opts_json.get("body") {
95
- Some(Value::String(s)) => Some(s.clone()),
96
- Some(other) => Some(other.to_string()),
97
- None => None,
98
- };
60
+ let body_opt = opts_json.get("body").cloned();
99
61
 
100
- // headers as Vec<(String,String)>
101
- let mut header_pairs: Vec<(String, String)> = Vec::new();
62
+ let mut headers = HeaderMap::new();
102
63
  if let Some(Value::Object(map)) = opts_json.get("headers") {
103
- for (k, v) in map.iter() {
104
- let v_str = match v {
105
- Value::String(s) => s.clone(),
106
- other => other.to_string(),
107
- };
108
- header_pairs.push((k.clone(), v_str));
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
+ }
109
70
  }
110
71
  }
111
72
 
112
- // Perform the blocking HTTP request inside block_in_place to avoid runtime panic
113
- let out_json = task::block_in_place(move || {
73
+ let out = task::block_in_place(move || {
114
74
  let client = Client::new();
75
+ let mut req = client.request(method.parse().unwrap(), &url);
115
76
 
116
- let method_parsed = method.parse().unwrap_or(reqwest::Method::GET);
117
- let mut req = client.request(method_parsed, &url);
118
-
119
- if !header_pairs.is_empty() {
120
- let mut headers = HeaderMap::new();
121
- for (k, v) in header_pairs.into_iter() {
122
- if let (Ok(name), Ok(val)) =
123
- (HeaderName::from_bytes(k.as_bytes()), HeaderValue::from_str(&v))
124
- {
125
- headers.insert(name, val);
126
- }
127
- }
128
- req = req.headers(headers);
129
- }
77
+ req = req.headers(headers);
130
78
 
131
79
  if let Some(body) = body_opt {
132
- req = req.body(body);
80
+ req = req.body(body.to_string());
133
81
  }
134
82
 
135
83
  match req.send() {
136
84
  Ok(resp) => {
137
85
  let status = resp.status().as_u16();
138
86
  let text = resp.text().unwrap_or_default();
139
- serde_json::json!({
140
- "ok": true,
141
- "status": status,
142
- "body": text
143
- })
87
+ serde_json::json!({ "ok": true, "status": status, "body": text })
144
88
  }
145
- Err(e) => serde_json::json!({
146
- "ok": false,
147
- "error": e.to_string()
148
- }),
89
+ Err(e) => serde_json::json!({ "ok": false, "error": e.to_string() }),
149
90
  }
150
91
  });
151
92
 
152
- // Convert serde_json::Value -> JsValue
153
- Ok(JsValue::from_json(&out_json, ctx).unwrap_or(JsValue::undefined()))
93
+ Ok(JsValue::from_json(&out, ctx).unwrap())
154
94
  });
155
95
 
156
- // Convert native function to JS function object (requires Realm)
157
96
  let realm = ctx.realm();
158
97
  let t_fetch_js_fn = t_fetch_native.to_js_function(realm);
159
98
 
160
- // Build `t` object with `.fetch`
161
99
  let t_obj = ObjectInitializer::new(ctx)
162
100
  .property(js_string!("fetch"), t_fetch_js_fn, Attribute::all())
163
101
  .build();
164
102
 
165
103
  ctx.global_object()
166
104
  .set(js_string!("t"), JsValue::from(t_obj), false, ctx)
167
- .expect("set global t");
105
+ .unwrap();
168
106
  }
169
107
 
170
- // Root/dynamic handlers -----------------------------------------------------
171
-
172
- async fn root_route(state: State<AppState>, req: Request<Body>) -> impl IntoResponse {
173
- dynamic_handler_inner(state, req).await
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
+ }
174
130
  }
175
131
 
176
- async fn dynamic_route(state: State<AppState>, req: Request<Body>) -> impl IntoResponse {
177
- dynamic_handler_inner(state, req).await
132
+
133
+ fn resolve_actions_dir(project_root: &PathBuf) -> PathBuf {
134
+ let exe = std::env::current_exe().unwrap();
135
+ let exe_dir = exe.parent().unwrap();
136
+ let is_prod = exe_dir.to_string_lossy().contains("/app")
137
+ || exe_dir.ends_with("release");
138
+
139
+ if is_prod {
140
+ PathBuf::from("/app/actions")
141
+ } else {
142
+ project_root.join("server").join("actions")
143
+ }
178
144
  }
179
145
 
180
- /// Main handler: looks up routes.json and executes action bundles using Boa.
181
- async fn dynamic_handler_inner(
146
+
147
+ async fn dynamic_handler(
182
148
  State(state): State<AppState>,
183
149
  req: Request<Body>,
184
150
  ) -> impl IntoResponse {
185
151
  let method = req.method().as_str().to_uppercase();
186
- let path = req.uri().path();
152
+ let path = req.uri().path().to_string();
187
153
  let key = format!("{}:{}", method, path);
188
154
 
189
155
  let body_bytes = match to_bytes(req.into_body(), usize::MAX).await {
190
- Ok(b) => b,
156
+ Ok(v) => v,
191
157
  Err(_) => return (StatusCode::BAD_REQUEST, "Failed to read body").into_response(),
192
158
  };
193
159
  let body_str = String::from_utf8_lossy(&body_bytes).to_string();
194
160
 
195
- if let Some(route) = state.routes.get(&key) {
196
- match route.r#type.as_str() {
197
- "action" => {
198
- let action_name = route.value.as_str().unwrap_or("").trim();
199
- if action_name.is_empty() {
200
- return (StatusCode::INTERNAL_SERVER_ERROR, "Invalid action name").into_response();
201
- }
202
-
203
- // Resolve actions directory robustly
204
- let actions_dir = find_actions_dir(&state.project_root);
205
- let actions_dir = match actions_dir {
206
- Some(p) => p,
207
- None => {
208
- return (
209
- StatusCode::INTERNAL_SERVER_ERROR,
210
- format!("Actions directory not found (checked multiple locations)"),
211
- )
212
- .into_response();
213
- }
214
- };
161
+ let route = match state.routes.get(&key) {
162
+ Some(r) => r,
163
+ None => return (StatusCode::NOT_FOUND, "Not Found").into_response(),
164
+ };
215
165
 
216
- let action_path = actions_dir.join(format!("{}.jsbundle", action_name));
166
+ if route.r#type != "action" {
167
+ if let Some(s) = route.value.as_str() {
168
+ return s.to_string().into_response();
169
+ }
170
+ return Json(route.value.clone()).into_response();
171
+ }
217
172
 
218
- if !action_path.exists() {
219
- return (
220
- StatusCode::NOT_FOUND,
221
- format!("Action bundle not found: {:?}", action_path),
222
- )
223
- .into_response();
224
- }
173
+ let action_name = route.value.as_str().unwrap().trim().to_string();
174
+ if action_name.is_empty() {
175
+ return (
176
+ StatusCode::INTERNAL_SERVER_ERROR,
177
+ "Invalid action name",
178
+ )
179
+ .into_response();
180
+ }
225
181
 
226
- let js_code = match fs::read_to_string(&action_path) {
227
- Ok(v) => v,
228
- Err(e) => {
229
- return (
230
- StatusCode::INTERNAL_SERVER_ERROR,
231
- format!("Failed reading action bundle: {}", e),
232
- )
233
- .into_response();
234
- }
235
- };
182
+ let actions_dir = resolve_actions_dir(&state.project_root);
183
+ let action_path = actions_dir.join(format!("{}.jsbundle", action_name));
236
184
 
237
- // Build env object
238
- let mut env_map = serde_json::Map::new();
239
- for (k, v) in std::env::vars() {
240
- env_map.insert(k, Value::String(v));
241
- }
242
- let env_json = Value::Object(env_map);
243
-
244
- // Injected script: sets process.env and __titan_req and invokes action function.
245
- let injected = format!(
246
- r#"
247
- globalThis.process = {{ env: {} }};
248
- const __titan_req = {};
249
- {};
250
- {}(__titan_req);
251
- "#,
252
- env_json.to_string(),
253
- body_str,
254
- js_code,
255
- action_name
256
- );
257
-
258
- let mut ctx = Context::default();
259
- inject_t_fetch(&mut ctx);
260
-
261
- let result = match ctx.eval(Source::from_bytes(&injected)) {
262
- Ok(v) => v,
263
- Err(e) => return Json(json_error(e.to_string())).into_response(),
264
- };
265
-
266
- let result_json: Value = match result.to_json(&mut ctx) {
267
- Ok(v) => v,
268
- Err(e) => json_error(e.to_string()),
269
- };
270
-
271
- return Json(result_json).into_response();
272
- }
185
+ if !action_path.exists() {
186
+ return (
187
+ StatusCode::NOT_FOUND,
188
+ format!("Action bundle not found: {:?}", action_path),
189
+ )
190
+ .into_response();
191
+ }
273
192
 
274
- "json" => return Json(route.value.clone()).into_response(),
275
- _ => {
276
- if let Some(s) = route.value.as_str() {
277
- return s.to_string().into_response();
278
- }
279
- return route.value.to_string().into_response();
280
- }
193
+ let js_code = match fs::read_to_string(&action_path) {
194
+ Ok(v) => v,
195
+ Err(e) => {
196
+ return (
197
+ StatusCode::INTERNAL_SERVER_ERROR,
198
+ format!("Failed reading bundle: {}", e),
199
+ )
200
+ .into_response();
281
201
  }
202
+ };
203
+
204
+ let mut env_map = serde_json::Map::new();
205
+ for (k, v) in std::env::vars() {
206
+ env_map.insert(k, Value::String(v));
282
207
  }
208
+ let env_json = Value::Object(env_map);
209
+
210
+ let injected = format!(
211
+ r#"
212
+ globalThis.process = {{ env: {} }};
213
+ const __titan_req = {};
214
+ {};
215
+ {}(__titan_req);
216
+ "#,
217
+ env_json.to_string(),
218
+ body_str,
219
+ js_code,
220
+ action_name
221
+ );
283
222
 
284
- (StatusCode::NOT_FOUND, "Not Found").into_response()
285
- }
223
+ let mut ctx = Context::default();
224
+ inject_t_fetch(&mut ctx);
286
225
 
287
- fn json_error(msg: String) -> Value {
288
- serde_json::json!({ "error": msg })
289
- }
226
+ let result = match ctx.eval(Source::from_bytes(&injected)) {
227
+ Ok(v) => v,
228
+ Err(e) => return Json(serde_json::json!({ "error": e.to_string() })).into_response(),
229
+ };
290
230
 
291
- // Entrypoint ---------------------------------------------------------------
231
+ let result_json = result.to_json(&mut ctx).unwrap_or(Value::Null);
232
+ Json(result_json).into_response()
233
+ }
292
234
 
293
235
  #[tokio::main]
294
236
  async fn main() -> Result<()> {
295
237
  dotenvy::dotenv().ok();
296
238
 
297
- // Load routes.json (expected at runtime root)
298
- let raw = fs::read_to_string("./routes.json").unwrap_or_else(|_| "{}".to_string());
239
+ let raw = fs::read_to_string("./routes.json").unwrap_or("{}".into());
299
240
  let json: Value = serde_json::from_str(&raw).unwrap_or_default();
300
241
 
301
242
  let port = json["__config"]["port"].as_u64().unwrap_or(3000);
302
- let routes_json = json["routes"].clone();
303
- let map: HashMap<String, RouteVal> = serde_json::from_value(routes_json).unwrap_or_default();
304
-
305
- // Project root — heuristics: try current_dir()
306
- let project_root = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
307
-
308
- // Debug logging — show where we looked for actions
309
- eprintln!("DEBUG runtime cwd: {:?}", std::env::current_dir());
310
- eprintln!("DEBUG project_root: {:?}", project_root);
311
- eprintln!(
312
- "DEBUG found actions dir (if any): {:?}",
313
- find_actions_dir(&project_root)
314
- );
243
+ let routes_map: HashMap<String, RouteVal> =
244
+ serde_json::from_value(json["routes"].clone()).unwrap_or_default();
315
245
 
316
- let state = AppState {
317
- routes: Arc::new(map),
318
- project_root,
319
- };
246
+ let project_root = detect_project_root();
247
+
248
+ let state = AppState {
249
+ routes: Arc::new(routes_map),
250
+ project_root,
251
+ };
252
+
253
+
320
254
 
321
255
  let app = Router::new()
322
- .route("/", any(root_route))
323
- .fallback(any(dynamic_route))
256
+ .route("/", any(dynamic_handler))
257
+ .fallback(any(dynamic_handler))
324
258
  .with_state(state);
325
259
 
326
260
  let listener = TcpListener::bind(format!("0.0.0.0:{}", port)).await?;
327
-
328
- // Banner (yellow-orange) and server info
329
- println!("\n\x1b[38;5;208m████████╗██╗████████╗ █████╗ ███╗ ██╗");
330
- println!("╚══██╔══╝██║╚══██╔══╝██╔══██╗████╗ ██║");
331
- println!(" ██║ ██║ ██║ ███████║██╔██╗ ██║");
332
- println!(" ██║ ██║ ██║ ██╔══██║██║╚██╗██║");
333
- println!(" ██║ ██║ ██║ ██║ ██║██║ ╚████║");
334
- println!(" ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝\x1b[0m\n");
335
- println!("\x1b[38;5;39mTitan server running at:\x1b[0m http://localhost:{}", port);
261
+ println!("Titan server running on {}", port);
336
262
 
337
263
  axum::serve(listener, app).await?;
338
264
  Ok(())