@ezetgalaxy/titan 25.14.0 → 25.14.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.
package/index.js CHANGED
@@ -187,51 +187,41 @@ function buildProd() {
187
187
 
188
188
  const root = process.cwd();
189
189
  const appJs = path.join(root, "app", "app.js");
190
- const bundler = path.join(root, "titan", "bundle.js");
191
190
  const serverDir = path.join(root, "server");
192
- const builtActionsDir = path.join(root, "server", "actions");
193
191
  const actionsOut = path.join(serverDir, "actions");
194
- // Ensure app/app.js exists
192
+
193
+ // BASIC CHECKS
195
194
  if (!fs.existsSync(appJs)) {
196
195
  console.log(red("ERROR: app/app.js not found."));
197
196
  process.exit(1);
198
197
  }
199
198
 
200
- // Ensure bundler exists
201
- if (!fs.existsSync(bundler)) {
202
- console.log(red("ERROR: titan/bundle.js not found."));
203
- process.exit(1);
204
- }
205
-
206
- // 1) Generate routes.json and action_map.json
207
- console.log(cyan("→ Generating Titan metadata..."));
199
+ // ----------------------------------------------------
200
+ // 1) BUILD METADATA + BUNDLE ACTIONS (ONE TIME ONLY)
201
+ // ----------------------------------------------------
202
+ console.log(cyan("→ Building Titan metadata + bundling actions..."));
208
203
  execSync("node app/app.js --build", { stdio: "inherit" });
209
204
 
210
- // 2) Bundle JS actions
211
- console.log(cyan("→ Bundling Titan actions..."));
212
- execSync("node titan/bundle.js", { stdio: "inherit" });
213
-
214
- // 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 {
229
- console.log(yellow("Warning: No actions bundled."));
205
+ // ensure actions directory exists
206
+ fs.mkdirSync(actionsOut, { recursive: true });
207
+
208
+ // verify bundled actions exist
209
+ const bundles = fs.readdirSync(actionsOut).filter(f => f.endsWith(".jsbundle"));
210
+ if (bundles.length === 0) {
211
+ console.log(red("ERROR: No actions bundled."));
212
+ console.log(red("Make sure your DSL outputs to server/actions."));
213
+ process.exit(1);
230
214
  }
231
215
 
232
- console.log(green("✔ Actions copied to server/actions"));
216
+ bundles.forEach(file => {
217
+ console.log(cyan(`→ Found action bundle: ${file}`));
218
+ });
219
+
220
+ console.log(green("✔ Actions ready in server/actions"));
233
221
 
234
- // 5) Build Rust binary
222
+ // ----------------------------------------------------
223
+ // 2) BUILD RUST BINARY
224
+ // ----------------------------------------------------
235
225
  console.log(cyan("→ Building Rust release binary..."));
236
226
  execSync("cargo build --release", {
237
227
  cwd: serverDir,
@@ -242,6 +232,7 @@ function buildProd() {
242
232
  }
243
233
 
244
234
 
235
+
245
236
  /* START PRODUCTION BINARY */
246
237
  function startProd() {
247
238
  const isWin = process.platform === "win32";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ezetgalaxy/titan",
3
- "version": "25.14.0",
3
+ "version": "25.14.4",
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,239 @@ 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() -> 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";
140
+
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")
178
153
  }
179
154
 
180
- /// Main handler: looks up routes.json and executes action bundles using Boa.
181
- async fn dynamic_handler_inner(
155
+
156
+
157
+ async fn dynamic_handler(
182
158
  State(state): State<AppState>,
183
159
  req: Request<Body>,
184
160
  ) -> impl IntoResponse {
185
161
  let method = req.method().as_str().to_uppercase();
186
- let path = req.uri().path();
162
+ let path = req.uri().path().to_string();
187
163
  let key = format!("{}:{}", method, path);
188
164
 
189
165
  let body_bytes = match to_bytes(req.into_body(), usize::MAX).await {
190
- Ok(b) => b,
166
+ Ok(v) => v,
191
167
  Err(_) => return (StatusCode::BAD_REQUEST, "Failed to read body").into_response(),
192
168
  };
193
169
  let body_str = String::from_utf8_lossy(&body_bytes).to_string();
194
170
 
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
- }
171
+ let route = match state.routes.get(&key) {
172
+ Some(r) => r,
173
+ None => return (StatusCode::NOT_FOUND, "Not Found").into_response(),
174
+ };
202
175
 
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
- };
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
+ }
215
182
 
216
- let action_path = actions_dir.join(format!("{}.jsbundle", action_name));
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
+ }
217
191
 
218
- if !action_path.exists() {
219
- return (
220
- StatusCode::NOT_FOUND,
221
- format!("Action bundle not found: {:?}", action_path),
222
- )
223
- .into_response();
224
- }
192
+ let actions_dir = resolve_actions_dir(&state.project_root);
193
+ let action_path = actions_dir.join(format!("{}.jsbundle", action_name));
225
194
 
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
- };
236
-
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
- }
195
+ if !action_path.exists() {
196
+ return (
197
+ StatusCode::NOT_FOUND,
198
+ format!("Action bundle not found: {:?}", action_path),
199
+ )
200
+ .into_response();
201
+ }
273
202
 
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
- }
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();
281
211
  }
212
+ };
213
+
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));
282
217
  }
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
+ );
283
232
 
284
- (StatusCode::NOT_FOUND, "Not Found").into_response()
285
- }
233
+ let mut ctx = Context::default();
234
+ inject_t_fetch(&mut ctx);
286
235
 
287
- fn json_error(msg: String) -> Value {
288
- serde_json::json!({ "error": msg })
289
- }
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
+ };
290
240
 
291
- // Entrypoint ---------------------------------------------------------------
241
+ let result_json = result.to_json(&mut ctx).unwrap_or(Value::Null);
242
+ Json(result_json).into_response()
243
+ }
292
244
 
293
245
  #[tokio::main]
294
246
  async fn main() -> Result<()> {
295
247
  dotenvy::dotenv().ok();
296
248
 
297
- // Load routes.json (expected at runtime root)
298
- let raw = fs::read_to_string("./routes.json").unwrap_or_else(|_| "{}".to_string());
249
+ let raw = fs::read_to_string("./routes.json").unwrap_or("{}".into());
299
250
  let json: Value = serde_json::from_str(&raw).unwrap_or_default();
300
251
 
301
252
  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
- );
253
+ let routes_map: HashMap<String, RouteVal> =
254
+ serde_json::from_value(json["routes"].clone()).unwrap_or_default();
315
255
 
316
- let state = AppState {
317
- routes: Arc::new(map),
318
- project_root,
319
- };
256
+ let project_root = detect_project_root();
257
+
258
+ let state = AppState {
259
+ routes: Arc::new(routes_map),
260
+ project_root,
261
+ };
262
+
263
+
320
264
 
321
265
  let app = Router::new()
322
- .route("/", any(root_route))
323
- .fallback(any(dynamic_route))
266
+ .route("/", any(dynamic_handler))
267
+ .fallback(any(dynamic_handler))
324
268
  .with_state(state);
325
269
 
326
270
  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);
271
+ println!("Titan server running on {}", port);
336
272
 
337
273
  axum::serve(listener, app).await?;
338
274
  Ok(())