@ezetgalaxy/titan 25.13.3 → 25.13.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ezetgalaxy/titan",
3
- "version": "25.13.3",
3
+ "version": "25.13.4",
4
4
  "description": "JavaScript backend framework that compiles your JS into a Rust + Axum server.",
5
5
  "license": "ISC",
6
6
  "author": "ezetgalaxy",
@@ -3,57 +3,49 @@
3
3
  # ================================================================
4
4
  FROM rust:1.91.1 AS builder
5
5
 
6
- # Install Node for Titan CLI and bundler
6
+ # Install Node for Titan CLI + bundler
7
7
  RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
8
8
  && apt-get install -y nodejs
9
9
 
10
- # Install Titan CLI globally
11
- RUN npm install -g @ezetgalaxy/titan
10
+ # Install Titan CLI (latest)
11
+ RUN npm install -g @ezetgalaxy/titan@latest
12
12
 
13
13
  WORKDIR /app
14
14
 
15
- # Copy project files into container
15
+ # Copy project files
16
16
  COPY . .
17
17
 
18
- # Install JS dependencies (for bundler & DSL)
18
+ # Install JS dependencies (needed for Titan DSL + bundler)
19
19
  RUN npm install
20
20
 
21
- # Build Titan metadata (routes.json + actions/*.jsbundle)
21
+ # Build Titan metadata + bundle JS actions
22
22
  RUN tit build
23
23
 
24
- # Build the Rust backend (release mode)
24
+ # Build Rust binary
25
25
  RUN cd server && cargo build --release
26
26
 
27
27
 
28
28
 
29
29
  # ================================================================
30
- # STAGE 2 — Runtime Image
30
+ # STAGE 2 — Runtime Image (Lightweight)
31
31
  # ================================================================
32
32
  FROM debian:stable-slim
33
33
 
34
34
  WORKDIR /app
35
35
 
36
- # -------------------------------------------------------------
37
- # Copy the Rust binary
38
- # -------------------------------------------------------------
36
+ # Copy Rust binary from builder stage
39
37
  COPY --from=builder /app/server/target/release/server ./titan-server
40
38
 
41
- # -------------------------------------------------------------
42
- # Copy Titan routing metadata (REQUIRED)
43
- # -------------------------------------------------------------
39
+ # Copy Titan routing metadata
44
40
  COPY --from=builder /app/server/routes.json ./routes.json
45
41
  COPY --from=builder /app/server/action_map.json ./action_map.json
46
42
 
47
- # -------------------------------------------------------------
48
- # Copy Titan JS bundles directory (REQUIRED)
49
- # -------------------------------------------------------------
43
+ # Copy Titan JS bundles
50
44
  RUN mkdir -p /app/actions
51
45
  COPY --from=builder /app/server/actions /app/actions
52
46
 
53
-
54
- # -------------------------------------------------------------
55
- # Runtime configuration
56
- # -------------------------------------------------------------
47
+ # Expose Titan port
57
48
  EXPOSE 3000
58
49
 
50
+ # Start Titan
59
51
  CMD ["./titan-server"]
@@ -1,9 +1,9 @@
1
- // src/main.rs
2
- use std::{collections::HashMap, fs, sync::Arc, path::PathBuf};
1
+ // server/src/main.rs
2
+ use std::{collections::HashMap, fs, path::PathBuf, sync::Arc};
3
3
 
4
4
  use anyhow::Result;
5
5
  use axum::{
6
- body::{Body, to_bytes},
6
+ body::{to_bytes, Body},
7
7
  extract::State,
8
8
  http::{Request, StatusCode},
9
9
  response::{IntoResponse, Json},
@@ -11,23 +11,18 @@ use axum::{
11
11
  Router,
12
12
  };
13
13
 
14
- use boa_engine::{
15
- Context, JsValue, Source,
16
- native_function::NativeFunction,
17
- object::ObjectInitializer,
18
- property::Attribute,
19
- };
20
- use boa_engine::js_string;
14
+ use boa_engine::{object::ObjectInitializer, Context, JsValue, Source};
15
+ use boa_engine::{js_string, native_function::NativeFunction, property::Attribute};
16
+
17
+ use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
18
+ use reqwest::blocking::Client;
21
19
 
22
20
  use serde::Deserialize;
23
21
  use serde_json::Value;
24
22
  use tokio::net::TcpListener;
25
23
  use tokio::task;
26
24
 
27
- use reqwest::blocking::Client;
28
- use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
29
-
30
- /// Route configuration entry parsed from routes.json
25
+ /// Route configuration (loaded from routes.json)
31
26
  #[derive(Debug, Deserialize)]
32
27
  struct RouteVal {
33
28
  r#type: String,
@@ -40,24 +35,56 @@ struct AppState {
40
35
  project_root: PathBuf,
41
36
  }
42
37
 
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
+ }
43
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 }`.
44
70
  fn inject_t_fetch(ctx: &mut Context) {
45
- // Create native Rust function (Boa v0.20)
71
+ // Native function (Boa 0.20) using from_fn_ptr
46
72
  let t_fetch_native = NativeFunction::from_fn_ptr(|_this, args, ctx| {
47
- // Extract arguments (safely convert JS strings to owned Rust Strings)
73
+ // Extract URL (owned string)
48
74
  let url = args
49
75
  .get(0)
50
- .and_then(|v| v.as_string().map(|s| s.to_std_string_escaped()))
76
+ .and_then(|v| v.to_string(ctx).ok())
77
+ .map(|s| s.to_std_string_escaped())
51
78
  .unwrap_or_default();
52
79
 
53
- // opts may be undefined. Convert to serde_json::Value for thread-safety.
80
+ // Extract opts -> convert to serde_json::Value (owned)
54
81
  let opts_js = args.get(1).cloned().unwrap_or(JsValue::undefined());
55
82
  let opts_json: Value = match opts_js.to_json(ctx) {
56
83
  Ok(v) => v,
57
84
  Err(_) => Value::Object(serde_json::Map::new()),
58
85
  };
59
86
 
60
- // Extract method, body, headers from opts_json (owned data, Send)
87
+ // Pull method, body, headers into owned Rust values
61
88
  let method = opts_json
62
89
  .get("method")
63
90
  .and_then(|m| m.as_str())
@@ -70,7 +97,7 @@ fn inject_t_fetch(ctx: &mut Context) {
70
97
  None => None,
71
98
  };
72
99
 
73
- // Build header map from opts_json["headers"] if present
100
+ // headers as Vec<(String,String)>
74
101
  let mut header_pairs: Vec<(String, String)> = Vec::new();
75
102
  if let Some(Value::Object(map)) = opts_json.get("headers") {
76
103
  for (k, v) in map.iter() {
@@ -82,16 +109,13 @@ fn inject_t_fetch(ctx: &mut Context) {
82
109
  }
83
110
  }
84
111
 
85
- // Perform blocking HTTP request inside block_in_place so we don't drop a blocking runtime
112
+ // Perform the blocking HTTP request inside block_in_place to avoid runtime panic
86
113
  let out_json = task::block_in_place(move || {
87
- // Create blocking client
88
114
  let client = Client::new();
89
115
 
90
- // Build request
91
116
  let method_parsed = method.parse().unwrap_or(reqwest::Method::GET);
92
117
  let mut req = client.request(method_parsed, &url);
93
118
 
94
- // Attach headers
95
119
  if !header_pairs.is_empty() {
96
120
  let mut headers = HeaderMap::new();
97
121
  for (k, v) in header_pairs.into_iter() {
@@ -108,11 +132,9 @@ fn inject_t_fetch(ctx: &mut Context) {
108
132
  req = req.body(body);
109
133
  }
110
134
 
111
- // Send request
112
135
  match req.send() {
113
136
  Ok(resp) => {
114
137
  let status = resp.status().as_u16();
115
- // Try to read text, fallback to empty string on error
116
138
  let text = resp.text().unwrap_or_default();
117
139
  serde_json::json!({
118
140
  "ok": true,
@@ -120,44 +142,42 @@ fn inject_t_fetch(ctx: &mut Context) {
120
142
  "body": text
121
143
  })
122
144
  }
123
- Err(e) => {
124
- serde_json::json!({
125
- "ok": false,
126
- "error": e.to_string()
127
- })
128
- }
145
+ Err(e) => serde_json::json!({
146
+ "ok": false,
147
+ "error": e.to_string()
148
+ }),
129
149
  }
130
150
  });
131
151
 
132
- // Convert serde_json::Value -> JsValue for return to JS
152
+ // Convert serde_json::Value -> JsValue
133
153
  Ok(JsValue::from_json(&out_json, ctx).unwrap_or(JsValue::undefined()))
134
154
  });
135
155
 
136
- // Convert the native function into a JS function object (requires Realm in Boa 0.20)
156
+ // Convert native function to JS function object (requires Realm)
137
157
  let realm = ctx.realm();
138
158
  let t_fetch_js_fn = t_fetch_native.to_js_function(realm);
139
159
 
140
- // Build `t` object with `.fetch` property
160
+ // Build `t` object with `.fetch`
141
161
  let t_obj = ObjectInitializer::new(ctx)
142
162
  .property(js_string!("fetch"), t_fetch_js_fn, Attribute::all())
143
163
  .build();
144
164
 
145
- // Attach to globalThis.t
146
165
  ctx.global_object()
147
166
  .set(js_string!("t"), JsValue::from(t_obj), false, ctx)
148
- .unwrap();
167
+ .expect("set global t");
149
168
  }
150
169
 
151
- // Axum handlers --------------------------------------------------------------
170
+ // Root/dynamic handlers -----------------------------------------------------
152
171
 
153
172
  async fn root_route(state: State<AppState>, req: Request<Body>) -> impl IntoResponse {
154
173
  dynamic_handler_inner(state, req).await
155
174
  }
175
+
156
176
  async fn dynamic_route(state: State<AppState>, req: Request<Body>) -> impl IntoResponse {
157
177
  dynamic_handler_inner(state, req).await
158
178
  }
159
179
 
160
- /// Main handler that evaluates JS actions from bundles using Boa
180
+ /// Main handler: looks up routes.json and executes action bundles using Boa.
161
181
  async fn dynamic_handler_inner(
162
182
  State(state): State<AppState>,
163
183
  req: Request<Body>,
@@ -177,35 +197,33 @@ async fn dynamic_handler_inner(
177
197
  "action" => {
178
198
  let action_name = route.value.as_str().unwrap_or("").trim();
179
199
  if action_name.is_empty() {
180
- return (
181
- StatusCode::INTERNAL_SERVER_ERROR,
182
- "Invalid action name",
183
- )
184
- .into_response();
200
+ return (StatusCode::INTERNAL_SERVER_ERROR, "Invalid action name").into_response();
185
201
  }
186
202
 
187
-
188
- let dev_actions_dir = state.project_root.join("server").join("actions");
189
- let prod_actions_dir = state.project_root.join("actions");
190
-
191
- let actions_dir = if dev_actions_dir.exists() {
192
- dev_actions_dir
193
- } else {
194
- prod_actions_dir
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
+ }
195
214
  };
196
215
 
197
216
  let action_path = actions_dir.join(format!("{}.jsbundle", action_name));
198
217
 
199
- if !action_path.exists() {
200
- return (
201
- StatusCode::NOT_FOUND,
202
- format!("Action bundle not found: {:?}", action_path),
203
- )
218
+ if !action_path.exists() {
219
+ return (
220
+ StatusCode::NOT_FOUND,
221
+ format!("Action bundle not found: {:?}", action_path),
222
+ )
204
223
  .into_response();
205
- }
206
-
224
+ }
207
225
 
208
- let js_code = match fs::read_to_string(action_path) {
226
+ let js_code = match fs::read_to_string(&action_path) {
209
227
  Ok(v) => v,
210
228
  Err(e) => {
211
229
  return (
@@ -216,14 +234,14 @@ async fn dynamic_handler_inner(
216
234
  }
217
235
  };
218
236
 
219
- // Build env
237
+ // Build env object
220
238
  let mut env_map = serde_json::Map::new();
221
239
  for (k, v) in std::env::vars() {
222
240
  env_map.insert(k, Value::String(v));
223
241
  }
224
242
  let env_json = Value::Object(env_map);
225
243
 
226
- // Injected JS: set process.env, provide request payload, then eval bundle and call action
244
+ // Injected script: sets process.env and __titan_req and invokes action function.
227
245
  let injected = format!(
228
246
  r#"
229
247
  globalThis.process = {{ env: {} }};
@@ -237,7 +255,6 @@ async fn dynamic_handler_inner(
237
255
  action_name
238
256
  );
239
257
 
240
- // Create Boa context, inject t.fetch, evaluate
241
258
  let mut ctx = Context::default();
242
259
  inject_t_fetch(&mut ctx);
243
260
 
@@ -246,7 +263,6 @@ async fn dynamic_handler_inner(
246
263
  Err(e) => return Json(json_error(e.to_string())).into_response(),
247
264
  };
248
265
 
249
- // to_json returns Result<Value, JsError> in Boa 0.20
250
266
  let result_json: Value = match result.to_json(&mut ctx) {
251
267
  Ok(v) => v,
252
268
  Err(e) => json_error(e.to_string()),
@@ -272,25 +288,30 @@ fn json_error(msg: String) -> Value {
272
288
  serde_json::json!({ "error": msg })
273
289
  }
274
290
 
275
- // Entrypoint -----------------------------------------------------------------
291
+ // Entrypoint ---------------------------------------------------------------
276
292
 
277
293
  #[tokio::main]
278
294
  async fn main() -> Result<()> {
279
295
  dotenvy::dotenv().ok();
280
296
 
297
+ // Load routes.json (expected at runtime root)
281
298
  let raw = fs::read_to_string("./routes.json").unwrap_or_else(|_| "{}".to_string());
282
299
  let json: Value = serde_json::from_str(&raw).unwrap_or_default();
283
300
 
284
301
  let port = json["__config"]["port"].as_u64().unwrap_or(3000);
285
-
286
302
  let routes_json = json["routes"].clone();
287
- let map: HashMap<String, RouteVal> =
288
- serde_json::from_value(routes_json).unwrap_or_default();
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("."));
289
307
 
290
- let project_root = std::env::current_dir()?
291
- .parent()
292
- .unwrap()
293
- .to_path_buf();
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
+ );
294
315
 
295
316
  let state = AppState {
296
317
  routes: Arc::new(map),
@@ -304,18 +325,15 @@ async fn main() -> Result<()> {
304
325
 
305
326
  let listener = TcpListener::bind(format!("0.0.0.0:{}", port)).await?;
306
327
 
307
- //
308
- // TITAN BANNER
309
- //
328
+ // Banner (yellow-orange) and server info
310
329
  println!("\n\x1b[38;5;208m████████╗██╗████████╗ █████╗ ███╗ ██╗");
311
330
  println!("╚══██╔══╝██║╚══██╔══╝██╔══██╗████╗ ██║");
312
331
  println!(" ██║ ██║ ██║ ███████║██╔██╗ ██║");
313
332
  println!(" ██║ ██║ ██║ ██╔══██║██║╚██╗██║");
314
333
  println!(" ██║ ██║ ██║ ██║ ██║██║ ╚████║");
315
334
  println!(" ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝\x1b[0m\n");
316
-
317
335
  println!("\x1b[38;5;39mTitan server running at:\x1b[0m http://localhost:{}", port);
318
-
336
+
319
337
  axum::serve(listener, app).await?;
320
338
  Ok(())
321
339
  }