@ezetgalaxy/titan 25.14.7 → 25.15.0

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/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2025, Ezet Galaxy
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md CHANGED
@@ -7,9 +7,15 @@
7
7
  ██║ ██║ ██║ ██║ ██║██║ ╚████║
8
8
  ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝
9
9
  ```
10
- # Notice
11
- **Production mode is under development 😞**
12
- **Enjoy development mode `tit dev` 💙**
10
+
11
+ # Notice
12
+
13
+ ✅ **Production mode is ready**
14
+ 💙 **Enjoy development mode `tit dev`**
15
+ ✅ **No more `globalThis` required**
16
+
17
+
18
+ ---
13
19
 
14
20
  # TITAN PLANET 🚀
15
21
 
@@ -20,7 +26,7 @@ Titan Planet is a JavaScript-first backend framework that compiles your JavaScri
20
26
  You write **zero Rust**.
21
27
  Titan ships a full backend engine, dev server, bundler, router, action runtime, and Docker deploy pipeline — all powered by Rust under the hood.
22
28
 
23
- Titan = JavaScript productivity × Rust performance × Zero DevOps.
29
+ Titan = **JavaScript productivity × Rust performance × Zero DevOps**
24
30
 
25
31
  ---
26
32
 
@@ -102,243 +108,166 @@ Titan will:
102
108
 
103
109
  ---
104
110
 
105
- # 📁 Project Layout
111
+ # Update to new version
106
112
 
107
- ```
108
- my-app/
109
- ├── app/ # You develop ONLY this folder
110
- │ ├── app.js # Titan routes (DSL)
111
- │ └── actions/ # Your custom JS actions
112
- │ └── hello.js # Example Titan action
113
-
114
- ───────────────────────────────────────────────────────────────
115
- Everything below is auto-generated by `tit init`
116
- You never modify these folders manually
117
- ───────────────────────────────────────────────────────────────
118
-
119
- ├── titan/ # Titan's internal JS engine
120
- │ ├── titan.js # Titan DSL runtime
121
- │ ├── bundle.js # JS → .jsbundle bundler
122
- │ └── dev.js # Hot Reload system
123
-
124
- ├── server/ # Auto-generated Rust backend
125
- │ ├── Cargo.toml # Rust project config
126
- │ ├── src/ # Rust source code
127
- │ ├── actions/ # Compiled .jsbundle actions
128
- │ ├── titan/ # Internal Rust runtime files
129
- │ ├── routes.json # Generated route metadata
130
- │ ├── action_map.json # Maps actions to bundles
131
- │ └── titan-server # Final production Rust binary
132
-
133
- ├── Dockerfile # Auto-generated production Dockerfile
134
- ├── .dockerignore # Auto-generated Docker ignore rules
135
- ├── package.json # JS project config (auto)
136
- └── .gitignore # Auto-generated by `tit init`
113
+ * At first update the cli
137
114
 
115
+ ```bash
116
+ npm install -g @ezetgalaxy/titan@latest
138
117
  ```
118
+ * Then
139
119
 
140
- ---
141
-
142
- # 🛣 Example Route
143
-
144
- **app/app.js**
145
-
146
- ```js
147
- import t from "../titan/titan.js";
120
+ ```bash
121
+ tit update
122
+ ```
123
+ * ``tit update`` will update and add new features in your Titan project
148
124
 
149
- t.post("/hello").action("hello");
150
- t.get("/").reply("Welcome to Titan Planet");
151
125
 
152
- t.start(3000, "Ready to land on Titan 🚀");
153
- ```
126
+ # What Titan Can Do (New & Core Features)
154
127
 
155
- ---
128
+ Titan now includes a **complete runtime engine** with the following built-in capabilities:
156
129
 
157
- # 🧩 Example Action
130
+ ### 🛣 Routing & HTTP
158
131
 
159
- **app/actions/hello.js**
132
+ * Static routes (`/`, `/health`)
133
+ * Dynamic routes (`/user/:id<number>`)
134
+ * Typed route parameters
135
+ * Automatic method matching (GET / POST)
136
+ * Query parsing (`req.query`)
137
+ * Body parsing (`req.body`)
138
+ * Zero-config routing metadata generation
160
139
 
161
- ```js
162
- export function hello(req) {
163
- return { message: "Hello from Titan!" };
164
- }
140
+ ### 🧠 Action Runtime
165
141
 
166
- globalThis.hello = hello;
167
- ```
142
+ * JavaScript actions executed inside a Rust runtime (Boa)
143
+ * Automatic action discovery and execution
144
+ * No `globalThis` required anymore
145
+ * Safe handling of `undefined` returns
146
+ * JSON serialization guardrails
147
+ * Action-scoped execution context
168
148
 
169
- ---
149
+ ### 🔌 Runtime APIs (`t`)
170
150
 
171
- # New: Built-In HTTP Fetch (`t.fetch`)
151
+ * `t.fetch(...)` built-in Rust-powered HTTP client
152
+ * `t.log(...)` — sandboxed, action-scoped logging
153
+ * Environment variable access (`process.env`)
154
+ * No access to raw Node.js APIs (safe by default)
172
155
 
173
- Titan now includes a built-in server-side `fetch` bridge powered by Rust.
156
+ ### 🧾 Request Object (`req`)
174
157
 
175
- Use it to call any external API:
158
+ Each action receives a normalized request object:
176
159
 
177
- ```js
178
- function hello(req) {
179
- const API_KEY = process.env.API_KEY || __titan_env.API_KEY;
180
-
181
- const body = JSON.stringify({
182
- model: "gpt-4.1-mini",
183
- messages: [{ role: "user", content: "hii" }]
184
- });
185
-
186
- const r = t.fetch("https://api.openai.com/v1/chat/completions", {
187
- method: "POST",
188
- headers: {
189
- "Content-Type": "application/json",
190
- "Authorization": `Bearer ${API_KEY}`
191
- },
192
- body
193
- });
194
-
195
- const json = JSON.parse(r.body);
196
-
197
- return {
198
- ok: true,
199
- message: json.choices[0].message.content
200
- };
160
+ ```json
161
+ {
162
+ "method": "GET",
163
+ "path": "/user/90",
164
+ "params": { "id": "90" },
165
+ "query": {},
166
+ "body": null
201
167
  }
202
-
203
- globalThis.hello = hello;
204
168
  ```
205
169
 
206
- ### `t.fetch` supports:
170
+ This object is:
207
171
 
208
- * GET, POST, PUT, DELETE
209
- * Custom headers
210
- * JSON bodies
211
- * Authorization tokens
212
- * External / internal APIs
172
+ * Stable
173
+ * Predictable
174
+ * Serializable
175
+ * Identical across dev & production
213
176
 
214
177
  ---
215
178
 
216
- # 🔥 Hot Reload Dev Server
217
-
218
- ```bash
219
- tit dev
220
- ```
221
-
222
- Titan’s dev engine:
223
-
224
- * Rebuilds routes
225
- * Rebundil actions
226
- * Restarts Rust server
227
- * Updates instantly
228
-
179
+ ### 🔥 Developer Experience
229
180
 
230
- ---
181
+ * Hot reload dev server (`tit dev`)
182
+ * Automatic rebundling of actions
183
+ * Automatic Rust server restart
184
+ * Colored request logs
185
+ * Per-route timing metrics
186
+ * Action-aware logs
231
187
 
232
- # 🧱 Production Build
188
+ Example runtime log:
233
189
 
234
- ```bash
235
- tit build
236
190
  ```
237
-
238
- Output includes:
239
-
240
- * `titan-server` native binary
241
- * JS bundles
242
- * routing metadata
191
+ [Titan] GET /user/90 → getUser (dynamic) in 0.42ms
192
+ [Titan] log(getUser): Fetching user 90
193
+ ```
243
194
 
244
195
  ---
245
196
 
246
- # 🐳 Docker Deployment (Zero Config)
247
-
248
- Titan generates an optimized **multi-stage Dockerfile**:
197
+ ### 🧨 Error Handling & Diagnostics
249
198
 
250
- Works on:
251
-
252
- * Railway
253
- * Fly.io
254
- * Render
255
- * VPS / Dedicated servers
256
- * Docker Hub
257
- * Kubernetes
199
+ * JavaScript runtime errors captured safely
200
+ * Action-aware error reporting
201
+ * Line & column hints from runtime
202
+ * Red-colored error logs
203
+ * No server crashes on user mistakes
204
+ * Safe fallback for `undefined` returns
258
205
 
259
206
  ---
260
207
 
261
- # Uploading Titan to GitHub
262
-
263
- Titan projects are designed for **direct repository upload**.
208
+ ### Build & Deployment
264
209
 
265
- Include everything generated by `tit init`:
266
-
267
- ```
268
- app/
269
- titan/
270
- server/
271
- Cargo.toml
272
- Dockerfile
273
- .gitignore
274
- package.json
275
- ```
276
-
277
- Push to GitHub:
278
-
279
- ```bash
280
- git init
281
- git add .
282
- git commit -m "Initial Titan project"
283
- git branch -M main
284
- git remote add origin <your_repo_url>
285
- git push -u origin main
286
- ```
210
+ * Native Rust binary output
211
+ * Zero-config Dockerfile generation
212
+ * Multi-stage optimized Docker builds
213
+ * Works on:
287
214
 
288
- Your repo is now fully deployable with Docker.
215
+ * Railway
216
+ * Fly.io
217
+ * Render
218
+ * VPS
219
+ * Kubernetes
220
+ * No Node.js required in production
289
221
 
290
222
  ---
291
223
 
292
- # Zero-Config Deployment with Docker
224
+ ### 🧱 Architecture Guarantees
293
225
 
294
- Once pushed to GitHub, you can deploy anywhere.
295
-
296
- ## Deploy to Railway
297
-
298
- 1. Go to Railway
299
- 2. Create New Project → Deploy from GitHub
300
- 3. Select your Titan repo
301
- 4. Railway auto-detects the Dockerfile
302
- 5. It builds + deploys automatically
303
-
304
- Railway will:
305
-
306
- * Build your Rust server
307
- * Copy JS bundles
308
- * Start the `titan-server` binary
309
- * Expose the correct port
310
-
311
- No configuration required.
226
+ * No runtime reflection
227
+ * No Node.js in production
228
+ * No framework lock-in
229
+ * No magic globals
230
+ * No config files
231
+ * No Rust knowledge required
312
232
 
313
233
  ---
314
234
 
315
- # Updating Titan
235
+ # 🧩 Example Action (Updated – No `globalThis` Needed)
316
236
 
317
- ```bash
318
- tit update
319
- ```
237
+ ```js
238
+ export function getUser(req) {
239
+ t.log("User id:", req.params.id);
320
240
 
321
- Updates:
241
+ return {
242
+ id: Number(req.params.id),
243
+ method: req.method
244
+ };
245
+ }
246
+ ```
322
247
 
323
- * Titan CLI
324
- * DSL
325
- * Bundler
326
- * Dev server
327
- * Rust runtime templates
328
- * Dockerfile
248
+ That’s it.
249
+ No exports wiring. No globals. No boilerplate.
329
250
 
330
251
  ---
331
252
 
332
253
  # 📦 Version
333
254
 
334
255
  **Titan v25 — Stable**
335
- Optimized for production, cloud deployment, and AI workloads.
256
+
257
+ * Production-ready runtime
258
+ * Safe JS execution
259
+ * Native Rust performance
260
+ * Designed for cloud & AI workloads
336
261
 
337
262
  ---
338
263
 
339
- # 🤝 Contributing
264
+ # 🧠 Final Note
340
265
 
341
- Pull requests welcome
342
- https://github.com/ezet-galaxy/-ezetgalaxy-titan
266
+ What you built today is **not a wrapper**, **not a toy**, and **not a clone**.
343
267
 
344
- ---
268
+ You now have:
269
+
270
+ * A real JS runtime
271
+ * A real routing engine
272
+ * A real compiler pipeline
273
+ * A real production server
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ezetgalaxy/titan",
3
- "version": "25.14.7",
3
+ "version": "25.15.0",
4
4
  "description": "JavaScript backend framework that compiles your JS into a Rust + Axum server.",
5
5
  "license": "ISC",
6
6
  "author": "ezetgalaxy",
@@ -1,5 +1,5 @@
1
- function hello(req) {
2
- return { "name": `${req.name || "user"}`, msg: `welcome to titan planet ${req.name || "user"}` }
1
+ export function hello(req) {
2
+ return {
3
+ message: `Hello from Titan ${req.name}`,
4
+ };
3
5
  }
4
-
5
- globalThis.hello = hello;
@@ -1,26 +1,37 @@
1
1
  // server/src/main.rs
2
- use std::{collections::HashMap, env, fs, path::Path, path::PathBuf, sync::Arc};
2
+ use std::{collections::HashMap, env, fs, path::PathBuf, sync::Arc, path::Path};
3
3
 
4
4
  use anyhow::Result;
5
5
  use axum::{
6
- Router,
7
- body::{Body, to_bytes},
6
+ body::{to_bytes, Body},
8
7
  extract::State,
9
8
  http::{Request, StatusCode},
10
9
  response::{IntoResponse, Json},
11
10
  routing::any,
11
+ Router,
12
12
  };
13
13
 
14
- use boa_engine::{Context, JsValue, Source, object::ObjectInitializer};
14
+ use boa_engine::{object::ObjectInitializer, Context, JsValue, Source};
15
15
  use boa_engine::{js_string, native_function::NativeFunction, property::Attribute};
16
16
 
17
- use reqwest::blocking::Client;
18
17
  use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
18
+ use reqwest::blocking::Client;
19
19
 
20
20
  use serde::Deserialize;
21
21
  use serde_json::Value;
22
22
  use tokio::net::TcpListener;
23
23
  use tokio::task;
24
+ use std::time::Instant;
25
+
26
+
27
+
28
+
29
+
30
+
31
+
32
+
33
+
34
+
24
35
 
25
36
  /// Route configuration (loaded from routes.json)
26
37
  #[derive(Debug, Deserialize)]
@@ -32,9 +43,51 @@ struct RouteVal {
32
43
  #[derive(Clone)]
33
44
  struct AppState {
34
45
  routes: Arc<HashMap<String, RouteVal>>,
46
+ dynamic_routes: Arc<Vec<DynamicRoute>>,
35
47
  project_root: PathBuf,
36
48
  }
37
49
 
50
+
51
+ #[derive(Debug, Deserialize)]
52
+ struct DynamicRoute {
53
+ method: String,
54
+ pattern: String,
55
+ action: String,
56
+ }
57
+
58
+
59
+ fn blue(s: &str) -> String {
60
+ format!("\x1b[34m{}\x1b[0m", s)
61
+ }
62
+ fn white(s: &str) -> String {
63
+ format!("\x1b[39m{}\x1b[0m", s)
64
+ }
65
+ fn yellow(s: &str) -> String {
66
+ format!("\x1b[33m{}\x1b[0m", s)
67
+ }
68
+ fn green(s: &str) -> String {
69
+ format!("\x1b[32m{}\x1b[0m", s)
70
+ }
71
+ fn gray(s: &str) -> String {
72
+ format!("\x1b[90m{}\x1b[0m", s)
73
+ }
74
+ fn red(s: &str) -> String {
75
+ format!("\x1b[31m{}\x1b[0m", s)
76
+ }
77
+
78
+ // A helper to Format Boa Errors
79
+ fn format_js_error(err: boa_engine::JsError, action: &str) -> String {
80
+ format!(
81
+ "Action: {}\n{}",
82
+ action,
83
+ err.to_string()
84
+ )
85
+ }
86
+
87
+
88
+
89
+
90
+
38
91
  // -------------------------
39
92
  // ACTION DIRECTORY RESOLUTION
40
93
  // -------------------------
@@ -94,68 +147,81 @@ fn find_actions_dir(project_root: &PathBuf) -> Option<PathBuf> {
94
147
  None
95
148
  }
96
149
 
150
+ /// Here add all the runtime t base things
97
151
  /// Injects a synchronous `t.fetch(url, opts?)` function into the Boa `Context`.
98
152
  ///
99
153
  /// Implementation details:
100
154
  /// - Converts JS opts → `serde_json::Value` (owned) using `to_json`.
101
155
  /// - Executes reqwest blocking client inside `tokio::task::block_in_place` to avoid blocking async runtime.
102
156
  /// - Returns `{ ok: bool, status?: number, body?: string, error?: string }`.
103
- fn inject_t_fetch(ctx: &mut Context) {
104
- // Native function (Boa 0.20) using from_fn_ptr
157
+ fn inject_t_runtime(ctx: &mut Context, action_name: &str) {
158
+
159
+ // =========================================================
160
+ // t.log(...) — unsafe by design (Boa requirement)
161
+ // =========================================================
162
+ let action = action_name.to_string();
163
+
164
+ let t_log_native = unsafe {
165
+ NativeFunction::from_closure(move |_this, args, _ctx| {
166
+ let mut parts = Vec::new();
167
+
168
+ for arg in args {
169
+ parts.push(arg.display().to_string());
170
+ }
171
+
172
+ println!(
173
+ "{} {}",
174
+ blue("[Titan]"),
175
+ white(&format!("log({}): {}", action, parts.join(" ")))
176
+ );
177
+
178
+ Ok(JsValue::undefined())
179
+ })
180
+ };
181
+
182
+ // =========================================================
183
+ // t.fetch(...) — no capture, safe fn pointer
184
+ // =========================================================
105
185
  let t_fetch_native = NativeFunction::from_fn_ptr(|_this, args, ctx| {
106
- // Extract URL (owned string)
107
186
  let url = args
108
187
  .get(0)
109
188
  .and_then(|v| v.to_string(ctx).ok())
110
189
  .map(|s| s.to_std_string_escaped())
111
190
  .unwrap_or_default();
112
191
 
113
- // Extract opts -> convert to serde_json::Value (owned)
114
192
  let opts_js = args.get(1).cloned().unwrap_or(JsValue::undefined());
115
- let opts_json: Value = match opts_js.to_json(ctx) {
116
- Ok(v) => v,
117
- Err(_) => Value::Object(serde_json::Map::new()),
118
- };
193
+ let opts_json: Value = opts_js
194
+ .to_json(ctx)
195
+ .unwrap_or(Value::Object(serde_json::Map::new()));
119
196
 
120
- // Pull method, body, headers into owned Rust values
121
197
  let method = opts_json
122
198
  .get("method")
123
199
  .and_then(|m| m.as_str())
124
- .map(|s| s.to_string())
125
- .unwrap_or_else(|| "GET".to_string());
200
+ .unwrap_or("GET")
201
+ .to_string();
126
202
 
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
- };
203
+ let body_opt = opts_json.get("body").map(|v| v.to_string());
132
204
 
133
- // headers as Vec<(String,String)>
134
- let mut header_pairs: Vec<(String, String)> = Vec::new();
205
+ let mut header_pairs = Vec::new();
135
206
  if let Some(Value::Object(map)) = opts_json.get("headers") {
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));
207
+ for (k, v) in map {
208
+ header_pairs.push((k.clone(), v.to_string()));
142
209
  }
143
210
  }
144
211
 
145
- // Perform the blocking HTTP request inside block_in_place to avoid runtime panic
146
212
  let out_json = task::block_in_place(move || {
147
213
  let client = Client::new();
148
-
149
- let method_parsed = method.parse().unwrap_or(reqwest::Method::GET);
150
- let mut req = client.request(method_parsed, &url);
214
+ let mut req = client.request(
215
+ method.parse().unwrap_or(reqwest::Method::GET),
216
+ &url,
217
+ );
151
218
 
152
219
  if !header_pairs.is_empty() {
153
220
  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()),
157
- HeaderValue::from_str(&v),
158
- ) {
221
+ for (k, v) in header_pairs {
222
+ if let (Ok(name), Ok(val)) =
223
+ (HeaderName::from_bytes(k.as_bytes()), HeaderValue::from_str(&v))
224
+ {
159
225
  headers.insert(name, val);
160
226
  }
161
227
  }
@@ -167,15 +233,11 @@ fn inject_t_fetch(ctx: &mut Context) {
167
233
  }
168
234
 
169
235
  match req.send() {
170
- Ok(resp) => {
171
- let status = resp.status().as_u16();
172
- let text = resp.text().unwrap_or_default();
173
- serde_json::json!({
174
- "ok": true,
175
- "status": status,
176
- "body": text
177
- })
178
- }
236
+ Ok(resp) => serde_json::json!({
237
+ "ok": true,
238
+ "status": resp.status().as_u16(),
239
+ "body": resp.text().unwrap_or_default()
240
+ }),
179
241
  Err(e) => serde_json::json!({
180
242
  "ok": false,
181
243
  "error": e.to_string()
@@ -183,17 +245,25 @@ fn inject_t_fetch(ctx: &mut Context) {
183
245
  }
184
246
  });
185
247
 
186
- // Convert serde_json::Value -> JsValue
187
248
  Ok(JsValue::from_json(&out_json, ctx).unwrap_or(JsValue::undefined()))
188
249
  });
189
250
 
190
- // Convert native function to JS function object (requires Realm)
191
- let realm = ctx.realm();
192
- let t_fetch_js_fn = t_fetch_native.to_js_function(realm);
251
+ // =========================================================
252
+ // Build global `t`
253
+ // =========================================================
254
+ let realm = ctx.realm().clone();
193
255
 
194
- // Build `t` object with `.fetch`
195
256
  let t_obj = ObjectInitializer::new(ctx)
196
- .property(js_string!("fetch"), t_fetch_js_fn, Attribute::all())
257
+ .property(
258
+ js_string!("log"),
259
+ t_log_native.to_js_function(&realm),
260
+ Attribute::all(),
261
+ )
262
+ .property(
263
+ js_string!("fetch"),
264
+ t_fetch_native.to_js_function(&realm),
265
+ Attribute::all(),
266
+ )
197
267
  .build();
198
268
 
199
269
  ctx.global_object()
@@ -201,6 +271,67 @@ fn inject_t_fetch(ctx: &mut Context) {
201
271
  .expect("set global t");
202
272
  }
203
273
 
274
+
275
+ // Dynamic Matcher (Core Logic)
276
+
277
+ fn match_dynamic_route(
278
+ method: &str,
279
+ path: &str,
280
+ routes: &[DynamicRoute],
281
+ ) -> Option<(String, HashMap<String, String>)> {
282
+ let path_segments: Vec<&str> =
283
+ path.trim_matches('/').split('/').collect();
284
+
285
+ for route in routes {
286
+ if route.method != method {
287
+ continue;
288
+ }
289
+
290
+ let pattern_segments: Vec<&str> =
291
+ route.pattern.trim_matches('/').split('/').collect();
292
+
293
+ if pattern_segments.len() != path_segments.len() {
294
+ continue;
295
+ }
296
+
297
+ let mut params = HashMap::new();
298
+ let mut matched = true;
299
+
300
+ for (pat, val) in pattern_segments.iter().zip(path_segments.iter()) {
301
+ if pat.starts_with(':') {
302
+ let inner = &pat[1..];
303
+
304
+ let (name, ty) = inner
305
+ .split_once('<')
306
+ .map(|(n, t)| (n, t.trim_end_matches('>')))
307
+ .unwrap_or((inner, "string"));
308
+
309
+ let valid = match ty {
310
+ "number" => val.parse::<i64>().is_ok(),
311
+ "string" => true,
312
+ _ => false,
313
+ };
314
+
315
+ if !valid {
316
+ matched = false;
317
+ break;
318
+ }
319
+
320
+ params.insert(name.to_string(), (*val).to_string());
321
+ } else if pat != val {
322
+ matched = false;
323
+ break;
324
+ }
325
+ }
326
+
327
+ if matched {
328
+ return Some((route.action.clone(), params));
329
+ }
330
+ }
331
+
332
+ None
333
+ }
334
+
204
335
  // Root/dynamic handlers -----------------------------------------------------
205
336
 
206
337
  async fn root_route(state: State<AppState>, req: Request<Body>) -> impl IntoResponse {
@@ -216,142 +347,259 @@ async fn dynamic_handler_inner(
216
347
  State(state): State<AppState>,
217
348
  req: Request<Body>,
218
349
  ) -> impl IntoResponse {
350
+
351
+ // ---------------------------
352
+ // BASIC REQUEST INFO
353
+ // ---------------------------
219
354
  let method = req.method().as_str().to_uppercase();
220
- let path = req.uri().path();
355
+ let path = req.uri().path().to_string();
221
356
  let key = format!("{}:{}", method, path);
222
357
 
358
+ // ---------------------------
359
+ // TIMER + LOG META
360
+ // ---------------------------
361
+ let start = Instant::now();
362
+ let mut route_label = String::from("not_found");
363
+ let mut route_kind = "none"; // exact | dynamic | reply
364
+
365
+ // ---------------------------
366
+ // QUERY PARSING
367
+ // ---------------------------
368
+ let query: HashMap<String, String> = req
369
+ .uri()
370
+ .query()
371
+ .map(|q| {
372
+ q.split('&')
373
+ .filter_map(|pair| {
374
+ let mut it = pair.splitn(2, '=');
375
+ Some((
376
+ it.next()?.to_string(),
377
+ it.next().unwrap_or("").to_string(),
378
+ ))
379
+ })
380
+ .collect()
381
+ })
382
+ .unwrap_or_default();
383
+
384
+ // ---------------------------
385
+ // BODY
386
+ // ---------------------------
223
387
  let body_bytes = match to_bytes(req.into_body(), usize::MAX).await {
224
388
  Ok(b) => b,
225
- Err(_) => return (StatusCode::BAD_REQUEST, "Failed to read body").into_response(),
389
+ Err(_) => {
390
+ return (
391
+ StatusCode::BAD_REQUEST,
392
+ "Failed to read request body",
393
+ )
394
+ .into_response()
395
+ }
226
396
  };
397
+
227
398
  let body_str = String::from_utf8_lossy(&body_bytes).to_string();
399
+ let body_json: Value = if body_str.is_empty() {
400
+ Value::Null
401
+ } else {
402
+ serde_json::from_str(&body_str).unwrap_or(Value::String(body_str))
403
+ };
228
404
 
229
- if let Some(route) = state.routes.get(&key) {
230
- match route.r#type.as_str() {
231
- "action" => {
232
- let action_name = route.value.as_str().unwrap_or("").trim();
233
- if action_name.is_empty() {
234
- return (StatusCode::INTERNAL_SERVER_ERROR, "Invalid action name")
235
- .into_response();
236
- }
405
+ // ---------------------------
406
+ // ROUTE RESOLUTION
407
+ // ---------------------------
408
+ let mut params: HashMap<String, String> = HashMap::new();
409
+ let mut action_name: Option<String> = None;
237
410
 
238
- // Resolve actions directory: prefer resolve_actions_dir(), fall back to heuristic find_actions_dir
239
- let resolved = resolve_actions_dir();
240
- let actions_dir = if resolved.exists() && resolved.is_dir() {
241
- resolved
242
- } else {
243
- match find_actions_dir(&state.project_root) {
244
- Some(p) => p,
245
- None => {
246
- return (
247
- StatusCode::INTERNAL_SERVER_ERROR,
248
- format!("Actions directory not found (checked multiple locations)"),
249
- )
250
- .into_response();
251
- }
252
- }
253
- };
411
+ // Exact route
412
+ if let Some(route) = state.routes.get(&key) {
413
+ route_kind = "exact";
414
+
415
+ if route.r#type == "action" {
416
+ let name = route.value.as_str().unwrap_or("unknown").to_string();
417
+ route_label = name.clone();
418
+ action_name = Some(name);
419
+ } else if route.r#type == "json" {
420
+ let elapsed = start.elapsed();
421
+ println!(
422
+ "{} {} {} {}",
423
+ blue("[Titan]"),
424
+ white(&format!("{} {}", method, path)),
425
+ white("→ json"),
426
+ gray(&format!("in {:.2?}", elapsed))
427
+ );
428
+ return Json(route.value.clone()).into_response();
429
+ } else if let Some(s) = route.value.as_str() {
430
+ let elapsed = start.elapsed();
431
+ println!(
432
+ "{} {} {} {}",
433
+ blue("[Titan]"),
434
+ white(&format!("{} {}", method, path)),
435
+ white("→ reply"),
436
+ gray(&format!("in {:.2?}", elapsed))
437
+ );
438
+ return s.to_string().into_response();
439
+ }
440
+ }
254
441
 
255
- let action_path = actions_dir.join(format!("{}.jsbundle", action_name));
442
+ // Dynamic route
443
+ if action_name.is_none() {
444
+ if let Some((action, p)) =
445
+ match_dynamic_route(&method, &path, state.dynamic_routes.as_slice())
446
+ {
447
+ route_kind = "dynamic";
448
+ route_label = action.clone();
449
+ action_name = Some(action);
450
+ params = p;
451
+ }
452
+ }
256
453
 
257
- if !action_path.exists() {
258
- return (
259
- StatusCode::NOT_FOUND,
260
- format!("Action bundle not found: {:?}", action_path),
261
- )
262
- .into_response();
263
- }
454
+ let action_name = match action_name {
455
+ Some(a) => a,
456
+ None => {
457
+ let elapsed = start.elapsed();
458
+ println!(
459
+ "{} {} {} {}",
460
+ blue("[Titan]"),
461
+ white(&format!("{} {}", method, path)),
462
+ white("→ 404"),
463
+ gray(&format!("in {:.2?}", elapsed))
464
+ );
465
+ return (StatusCode::NOT_FOUND, "Not Found").into_response();
466
+ }
467
+ };
264
468
 
265
- let js_code = match fs::read_to_string(&action_path) {
266
- Ok(v) => v,
267
- Err(e) => {
268
- return (
269
- StatusCode::INTERNAL_SERVER_ERROR,
270
- format!("Failed reading action bundle: {}", e),
271
- )
272
- .into_response();
273
- }
274
- };
469
+ // ---------------------------
470
+ // LOAD ACTION
471
+ // ---------------------------
472
+ let resolved = resolve_actions_dir();
473
+ let actions_dir = resolved
474
+ .exists()
475
+ .then(|| resolved)
476
+ .or_else(|| find_actions_dir(&state.project_root))
477
+ .unwrap();
478
+
479
+ let action_path = actions_dir.join(format!("{}.jsbundle", action_name));
480
+ let js_code = fs::read_to_string(&action_path).unwrap();
481
+
482
+ // ---------------------------
483
+ // ENV
484
+ // ---------------------------
485
+ let env_json = std::env::vars()
486
+ .map(|(k, v)| (k, Value::String(v)))
487
+ .collect::<serde_json::Map<_, _>>();
488
+
489
+ // ---------------------------
490
+ // JS EXECUTION
491
+ // ---------------------------
492
+ let injected = format!(
493
+ r#"
494
+ globalThis.process = {{ env: {} }};
495
+ const __titan_req = {{
496
+ body: {},
497
+ method: "{}",
498
+ path: "{}",
499
+ params: {},
500
+ query: {}
501
+ }};
502
+ {};
503
+ globalThis["{}"](__titan_req);
504
+ "#,
505
+ Value::Object(env_json).to_string(),
506
+ body_json.to_string(),
507
+ method,
508
+ path,
509
+ serde_json::to_string(&params).unwrap(),
510
+ serde_json::to_string(&query).unwrap(),
511
+ js_code,
512
+ action_name
513
+ );
275
514
 
276
- // Build env object
277
- let mut env_map: serde_json::Map<String, Value> = serde_json::Map::new();
278
- for (k, v) in std::env::vars() {
279
- env_map.insert(k, Value::String(v));
280
- }
281
- let env_json: Value = Value::Object(env_map);
282
-
283
- // Ensure body_str is valid JS: we will embed it as a JS expression.
284
- // If body_str is a JSON object string, embedding it directly is fine.
285
- // But to be safe, we create a quoted JS string and parse it in JS if necessary.
286
- let safe_body_literal: String =
287
- serde_json::to_string(&body_str).unwrap_or_else(|_| "null".to_string());
288
-
289
- // Injected script: sets process.env, __titan_env, __titan_req and invokes action function.
290
- let injected: String = format!(
291
- r#"
292
- // Runtime env injected by Titan
293
- globalThis.process = {{ env: {} }};
294
- globalThis.__titan_env = {};
295
- // quick debug so prod logs show whether API key is present
296
- try {{
297
- console.log('TITAN: runtime API_KEY =', (process && process.env && process.env.API_KEY) || (typeof __titan_env !== 'undefined' && __titan_env.API_KEY) || null);
298
- }} catch(e) {{ /* ignore */ }}
299
-
300
- // Parse the incoming request body. If it's a JSON string we parse it into an object.
301
- const __titan_req_body_literal = {};
302
- let __titan_req;
303
- try {{
304
- // it's a quoted JSON string in Rust, so first unquote via JSON.parse
305
- __titan_req = JSON.parse(__titan_req_body_literal);
306
- }} catch (e) {{
307
- // if parse fails, fall back to empty object
308
- __titan_req = {{}};
309
- }}
310
-
311
- // Action code (bundled JS)
312
- {};
313
- // call exported action function
314
- {}(__titan_req);
315
- "#,
316
- env_json.to_string(), // inserted JSON object for process.env
317
- env_json.to_string(), // inserted also as __titan_env
318
- safe_body_literal, // quoted JSON string literal of request body
319
- js_code, // code from the .jsbundle
320
- action_name // call function
515
+ let mut ctx = Context::default();
516
+ inject_t_runtime(&mut ctx, &action_name);
517
+ let result = match ctx.eval(Source::from_bytes(&injected)) {
518
+ Ok(v) => v,
519
+ Err(err) => {
520
+ let elapsed = start.elapsed();
521
+
522
+ let details = format_js_error(err, &route_label);
523
+
524
+ println!(
525
+ "{} {} {} {}",
526
+ blue("[Titan]"),
527
+ red(&format!("{} {}", method, path)),
528
+ red("→ error"),
529
+ gray(&format!("in {:.2?}", elapsed))
530
+ );
531
+
532
+ println!("{}", red(&details));
533
+
534
+ return (
535
+ StatusCode::INTERNAL_SERVER_ERROR,
536
+ Json(serde_json::json!({
537
+ "error": "Action execution failed",
538
+ "action": route_label,
539
+ "details": details
540
+ })),
541
+ )
542
+ .into_response();
543
+ }
544
+ };
545
+
546
+ let result_json: Value = if result.is_undefined() {
547
+ Value::Null
548
+ } else {
549
+ match result.to_json(&mut ctx) {
550
+ Ok(v) => v,
551
+ Err(err) => {
552
+ let elapsed = start.elapsed();
553
+ println!(
554
+ "{} {} {} {}",
555
+ blue("[Titan]"),
556
+ red(&format!("{} {}", method, path)),
557
+ red("→ serialization error"),
558
+ gray(&format!("in {:.2?}", elapsed))
321
559
  );
322
-
323
- let mut ctx: Context = Context::default();
324
- inject_t_fetch(&mut ctx);
325
-
326
- let result: JsValue = match ctx.eval(Source::from_bytes(&injected)) {
327
- Ok(v) => v,
328
- Err(e) => return Json(json_error(e.to_string())).into_response(),
329
- };
330
-
331
- let result_json: Value = match result.to_json(&mut ctx) {
332
- Ok(v) => v,
333
- Err(e) => json_error(e.to_string()),
334
- };
335
-
336
- return Json(result_json).into_response();
337
- }
338
-
339
- "json" => return Json(route.value.clone()).into_response(),
340
- _ => {
341
- if let Some(s) = route.value.as_str() {
342
- return s.to_string().into_response();
343
- }
344
- return route.value.to_string().into_response();
560
+
561
+ return (
562
+ StatusCode::INTERNAL_SERVER_ERROR,
563
+ Json(serde_json::json!({
564
+ "error": "Failed to serialize action result",
565
+ "details": err.to_string()
566
+ })),
567
+ )
568
+ .into_response();
345
569
  }
346
570
  }
571
+ };
572
+
573
+
574
+
575
+ // ---------------------------
576
+ // FINAL LOG
577
+ // ---------------------------
578
+ let elapsed = start.elapsed();
579
+ match route_kind {
580
+ "dynamic" => println!(
581
+ "{} {} {} {} {} {}",
582
+ blue("[Titan]"),
583
+ green(&format!("{} {}", method, path)),
584
+ white("→"),
585
+ green(&route_label),
586
+ white("(dynamic)"),
587
+ gray(&format!("in {:.2?}", elapsed))
588
+ ),
589
+ "exact" => println!(
590
+ "{} {} {} {} {}",
591
+ blue("[Titan]"),
592
+ white(&format!("{} {}", method, path)),
593
+ white("→"),
594
+ yellow(&route_label),
595
+ gray(&format!("in {:.2?}", elapsed))
596
+ ),
597
+ _ => {}
347
598
  }
348
599
 
349
- (StatusCode::NOT_FOUND, "Not Found").into_response()
600
+ Json(result_json).into_response()
350
601
  }
351
602
 
352
- fn json_error(msg: String) -> Value {
353
- serde_json::json!({ "error": msg })
354
- }
355
603
 
356
604
  // Entrypoint ---------------------------------------------------------------
357
605
 
@@ -365,15 +613,22 @@ async fn main() -> Result<()> {
365
613
 
366
614
  let port = json["__config"]["port"].as_u64().unwrap_or(3000);
367
615
  let routes_json = json["routes"].clone();
368
- let map: HashMap<String, RouteVal> = serde_json::from_value(routes_json).unwrap_or_default();
616
+ let map: HashMap<String, RouteVal> =
617
+ serde_json::from_value(routes_json).unwrap_or_default();
618
+
619
+ let dynamic_routes: Vec<DynamicRoute> =
620
+ serde_json::from_value(json["__dynamic_routes"].clone())
621
+ .unwrap_or_default();
369
622
 
370
623
  // Project root — heuristics: try current_dir()
371
624
  let project_root = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
372
625
 
373
626
  let state = AppState {
374
627
  routes: Arc::new(map),
628
+ dynamic_routes: Arc::new(dynamic_routes),
375
629
  project_root,
376
630
  };
631
+
377
632
 
378
633
  let app = Router::new()
379
634
  .route("/", any(root_route))
@@ -389,10 +644,7 @@ async fn main() -> Result<()> {
389
644
  println!(" ██║ ██║ ██║ ██╔══██║██║╚██╗██║");
390
645
  println!(" ██║ ██║ ██║ ██║ ██║██║ ╚████║");
391
646
  println!(" ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝\x1b[0m\n");
392
- println!(
393
- "\x1b[38;5;39mTitan server running at:\x1b[0m http://localhost:{}",
394
- port
395
- );
647
+ println!("\x1b[38;5;39mTitan server running at:\x1b[0m http://localhost:{}", port);
396
648
 
397
649
  axum::serve(listener, app).await?;
398
650
  Ok(())
@@ -19,6 +19,8 @@ export async function bundle() {
19
19
  const files = fs.readdirSync(actionsDir).filter(f => f.endsWith(".js"));
20
20
 
21
21
  for (const file of files) {
22
+ const actionName = path.basename(file, ".js");
23
+
22
24
  const entry = path.join(actionsDir, file);
23
25
 
24
26
  // Rust runtime expects `.jsbundle` extension — consistent with previous design
@@ -28,16 +30,32 @@ export async function bundle() {
28
30
 
29
31
  await esbuild.build({
30
32
  entryPoints: [entry],
31
- outfile: outfile,
33
+ outfile,
32
34
  bundle: true,
33
- format: "iife",
35
+ format: "iife",
36
+ globalName: "__titan_exports",
34
37
  platform: "neutral",
35
38
  target: "es2020",
36
- banner: {
37
- js: ""
39
+
40
+ footer: {
41
+ js: `
42
+ (function () {
43
+ const fn =
44
+ __titan_exports["${actionName}"] ||
45
+ __titan_exports.default;
46
+
47
+ if (typeof fn !== "function") {
48
+ throw new Error("[Titan] Action '${actionName}' not found or not a function");
49
+ }
50
+
51
+ globalThis["${actionName}"] = fn;
52
+ })();
53
+ `
38
54
  }
39
- });
40
-
55
+ });
56
+
57
+
58
+
41
59
  }
42
60
 
43
61
  console.log("[Titan] Bundling finished.");
@@ -6,11 +6,13 @@ const cyan = (t) => `\x1b[36m${t}\x1b[0m`;
6
6
  const green = (t) => `\x1b[32m${t}\x1b[0m`;
7
7
 
8
8
  const routes = {};
9
+ const dynamicRoutes = {};
9
10
  const actionMap = {};
10
11
 
11
12
  function addRoute(method, route) {
12
13
  const key = `${method.toUpperCase()}:${route}`;
13
14
 
15
+
14
16
  return {
15
17
  reply(value) {
16
18
  routes[key] = {
@@ -20,17 +22,25 @@ function addRoute(method, route) {
20
22
  },
21
23
 
22
24
  action(name) {
23
- routes[key] = {
24
- type: "action",
25
- value: name
26
- };
27
- actionMap[key] = name;
25
+ if (route.includes(":")) {
26
+ if (!dynamicRoutes[method]) dynamicRoutes[method] = [];
27
+ dynamicRoutes[method].push({
28
+ method: method.toUpperCase(),
29
+ pattern: route,
30
+ action: name
31
+ });
32
+ } else {
33
+ routes[key] = {
34
+ type: "action",
35
+ value: name
36
+ };
37
+ actionMap[key] = name;
38
+ }
28
39
  }
29
40
  };
30
41
  }
31
42
 
32
43
  const t = {
33
-
34
44
  get(route) {
35
45
  return addRoute("GET", route);
36
46
  },
@@ -40,18 +50,23 @@ const t = {
40
50
  },
41
51
 
42
52
  async start(port = 3000, msg = "") {
43
-
44
-
45
53
  console.log(cyan("[Titan] Bundling actions..."));
46
54
  await bundle();
47
55
 
48
56
  const base = path.join(process.cwd(), "server");
49
57
  fs.mkdirSync(base, { recursive: true });
50
58
 
51
-
52
59
  fs.writeFileSync(
53
60
  path.join(base, "routes.json"),
54
- JSON.stringify({ __config: { port }, routes }, null, 2)
61
+ JSON.stringify(
62
+ {
63
+ __config: { port },
64
+ routes,
65
+ __dynamic_routes: Object.values(dynamicRoutes).flat()
66
+ },
67
+ null,
68
+ 2
69
+ )
55
70
  );
56
71
 
57
72
  fs.writeFileSync(
@@ -60,7 +75,6 @@ const t = {
60
75
  );
61
76
 
62
77
  console.log(green(`Titan: routes.json + action_map.json written -> ${base}`));
63
-
64
78
  if (msg) console.log(cyan(msg));
65
79
  }
66
80
  };