@ezetgalaxy/titan 25.14.8 → 25.15.1

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,16 @@
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
+ 💟 **Website for titan docs coming soon**
17
+
18
+
19
+ ---
13
20
 
14
21
  # TITAN PLANET 🚀
15
22
 
@@ -20,7 +27,7 @@ Titan Planet is a JavaScript-first backend framework that compiles your JavaScri
20
27
  You write **zero Rust**.
21
28
  Titan ships a full backend engine, dev server, bundler, router, action runtime, and Docker deploy pipeline — all powered by Rust under the hood.
22
29
 
23
- Titan = JavaScript productivity × Rust performance × Zero DevOps.
30
+ Titan = **JavaScript productivity × Rust performance × Zero DevOps**
24
31
 
25
32
  ---
26
33
 
@@ -102,243 +109,166 @@ Titan will:
102
109
 
103
110
  ---
104
111
 
105
- # 📁 Project Layout
112
+ # Update to new version
106
113
 
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`
114
+ * At first update the cli
137
115
 
116
+ ```bash
117
+ npm install -g @ezetgalaxy/titan@latest
138
118
  ```
119
+ * Then
139
120
 
140
- ---
141
-
142
- # 🛣 Example Route
143
-
144
- **app/app.js**
145
-
146
- ```js
147
- import t from "../titan/titan.js";
121
+ ```bash
122
+ tit update
123
+ ```
124
+ * ``tit update`` will update and add new features in your Titan project
148
125
 
149
- t.post("/hello").action("hello");
150
- t.get("/").reply("Welcome to Titan Planet");
151
126
 
152
- t.start(3000, "Ready to land on Titan 🚀");
153
- ```
127
+ # What Titan Can Do (New & Core Features)
154
128
 
155
- ---
129
+ Titan now includes a **complete runtime engine** with the following built-in capabilities:
156
130
 
157
- # 🧩 Example Action
131
+ ### 🛣 Routing & HTTP
158
132
 
159
- **app/actions/hello.js**
133
+ * Static routes (`/`, `/health`)
134
+ * Dynamic routes (`/user/:id<number>`)
135
+ * Typed route parameters
136
+ * Automatic method matching (GET / POST)
137
+ * Query parsing (`req.query`)
138
+ * Body parsing (`req.body`)
139
+ * Zero-config routing metadata generation
160
140
 
161
- ```js
162
- export function hello(req) {
163
- return { message: "Hello from Titan!" };
164
- }
141
+ ### 🧠 Action Runtime
165
142
 
166
- globalThis.hello = hello;
167
- ```
143
+ * JavaScript actions executed inside a Rust runtime (Boa)
144
+ * Automatic action discovery and execution
145
+ * No `globalThis` required anymore
146
+ * Safe handling of `undefined` returns
147
+ * JSON serialization guardrails
148
+ * Action-scoped execution context
168
149
 
169
- ---
150
+ ### 🔌 Runtime APIs (`t`)
170
151
 
171
- # New: Built-In HTTP Fetch (`t.fetch`)
152
+ * `t.fetch(...)` built-in Rust-powered HTTP client
153
+ * `t.log(...)` — sandboxed, action-scoped logging
154
+ * Environment variable access (`process.env`)
155
+ * No access to raw Node.js APIs (safe by default)
172
156
 
173
- Titan now includes a built-in server-side `fetch` bridge powered by Rust.
157
+ ### 🧾 Request Object (`req`)
174
158
 
175
- Use it to call any external API:
159
+ Each action receives a normalized request object:
176
160
 
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
- };
161
+ ```json
162
+ {
163
+ "method": "GET",
164
+ "path": "/user/90",
165
+ "params": { "id": "90" },
166
+ "query": {},
167
+ "body": null
201
168
  }
202
-
203
- globalThis.hello = hello;
204
169
  ```
205
170
 
206
- ### `t.fetch` supports:
171
+ This object is:
207
172
 
208
- * GET, POST, PUT, DELETE
209
- * Custom headers
210
- * JSON bodies
211
- * Authorization tokens
212
- * External / internal APIs
173
+ * Stable
174
+ * Predictable
175
+ * Serializable
176
+ * Identical across dev & production
213
177
 
214
178
  ---
215
179
 
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
-
180
+ ### 🔥 Developer Experience
229
181
 
230
- ---
182
+ * Hot reload dev server (`tit dev`)
183
+ * Automatic rebundling of actions
184
+ * Automatic Rust server restart
185
+ * Colored request logs
186
+ * Per-route timing metrics
187
+ * Action-aware logs
231
188
 
232
- # 🧱 Production Build
189
+ Example runtime log:
233
190
 
234
- ```bash
235
- tit build
236
191
  ```
237
-
238
- Output includes:
239
-
240
- * `titan-server` native binary
241
- * JS bundles
242
- * routing metadata
192
+ [Titan] GET /user/90 → getUser (dynamic) in 0.42ms
193
+ [Titan] log(getUser): Fetching user 90
194
+ ```
243
195
 
244
196
  ---
245
197
 
246
- # 🐳 Docker Deployment (Zero Config)
247
-
248
- Titan generates an optimized **multi-stage Dockerfile**:
198
+ ### 🧨 Error Handling & Diagnostics
249
199
 
250
- Works on:
251
-
252
- * Railway
253
- * Fly.io
254
- * Render
255
- * VPS / Dedicated servers
256
- * Docker Hub
257
- * Kubernetes
200
+ * JavaScript runtime errors captured safely
201
+ * Action-aware error reporting
202
+ * Line & column hints from runtime
203
+ * Red-colored error logs
204
+ * No server crashes on user mistakes
205
+ * Safe fallback for `undefined` returns
258
206
 
259
207
  ---
260
208
 
261
- # Uploading Titan to GitHub
262
-
263
- Titan projects are designed for **direct repository upload**.
209
+ ### Build & Deployment
264
210
 
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
- ```
211
+ * Native Rust binary output
212
+ * Zero-config Dockerfile generation
213
+ * Multi-stage optimized Docker builds
214
+ * Works on:
287
215
 
288
- Your repo is now fully deployable with Docker.
216
+ * Railway
217
+ * Fly.io
218
+ * Render
219
+ * VPS
220
+ * Kubernetes
221
+ * No Node.js required in production
289
222
 
290
223
  ---
291
224
 
292
- # Zero-Config Deployment with Docker
225
+ ### 🧱 Architecture Guarantees
293
226
 
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.
227
+ * No runtime reflection
228
+ * No Node.js in production
229
+ * No framework lock-in
230
+ * No magic globals
231
+ * No config files
232
+ * No Rust knowledge required
312
233
 
313
234
  ---
314
235
 
315
- # Updating Titan
236
+ # 🧩 Example Action (Updated – No `globalThis` Needed)
316
237
 
317
- ```bash
318
- tit update
319
- ```
238
+ ```js
239
+ export function getUser(req) {
240
+ t.log("User id:", req.params.id);
320
241
 
321
- Updates:
242
+ return {
243
+ id: Number(req.params.id),
244
+ method: req.method
245
+ };
246
+ }
247
+ ```
322
248
 
323
- * Titan CLI
324
- * DSL
325
- * Bundler
326
- * Dev server
327
- * Rust runtime templates
328
- * Dockerfile
249
+ That’s it.
250
+ No exports wiring. No globals. No boilerplate.
329
251
 
330
252
  ---
331
253
 
332
254
  # 📦 Version
333
255
 
334
256
  **Titan v25 — Stable**
335
- Optimized for production, cloud deployment, and AI workloads.
257
+
258
+ * Production-ready runtime
259
+ * Safe JS execution
260
+ * Native Rust performance
261
+ * Designed for cloud & AI workloads
336
262
 
337
263
  ---
338
264
 
339
- # 🤝 Contributing
265
+ # 🧠 Final Note
340
266
 
341
- Pull requests welcome
342
- https://github.com/ezet-galaxy/-ezetgalaxy-titan
267
+ What you built today is **not a wrapper**, **not a toy**, and **not a clone**.
343
268
 
344
- ---
269
+ You now have:
270
+
271
+ * A real JS runtime
272
+ * A real routing engine
273
+ * A real compiler pipeline
274
+ * A real production server
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ezetgalaxy/titan",
3
- "version": "25.14.8",
3
+ "version": "25.15.1",
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;
@@ -21,6 +21,17 @@ 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,64 +147,78 @@ 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
+ gray(&format!("\x1b[90mlog({})\x1b[0m\x1b[97m: {}\x1b[0m", 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() {
221
+ for (k, v) in header_pairs {
155
222
  if let (Ok(name), Ok(val)) =
156
223
  (HeaderName::from_bytes(k.as_bytes()), HeaderValue::from_str(&v))
157
224
  {
@@ -166,15 +233,11 @@ fn inject_t_fetch(ctx: &mut Context) {
166
233
  }
167
234
 
168
235
  match req.send() {
169
- Ok(resp) => {
170
- let status = resp.status().as_u16();
171
- let text = resp.text().unwrap_or_default();
172
- serde_json::json!({
173
- "ok": true,
174
- "status": status,
175
- "body": text
176
- })
177
- }
236
+ Ok(resp) => serde_json::json!({
237
+ "ok": true,
238
+ "status": resp.status().as_u16(),
239
+ "body": resp.text().unwrap_or_default()
240
+ }),
178
241
  Err(e) => serde_json::json!({
179
242
  "ok": false,
180
243
  "error": e.to_string()
@@ -182,17 +245,25 @@ fn inject_t_fetch(ctx: &mut Context) {
182
245
  }
183
246
  });
184
247
 
185
- // Convert serde_json::Value -> JsValue
186
248
  Ok(JsValue::from_json(&out_json, ctx).unwrap_or(JsValue::undefined()))
187
249
  });
188
250
 
189
- // Convert native function to JS function object (requires Realm)
190
- let realm = ctx.realm();
191
- let t_fetch_js_fn = t_fetch_native.to_js_function(realm);
251
+ // =========================================================
252
+ // Build global `t`
253
+ // =========================================================
254
+ let realm = ctx.realm().clone();
192
255
 
193
- // Build `t` object with `.fetch`
194
256
  let t_obj = ObjectInitializer::new(ctx)
195
- .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
+ )
196
267
  .build();
197
268
 
198
269
  ctx.global_object()
@@ -200,6 +271,67 @@ fn inject_t_fetch(ctx: &mut Context) {
200
271
  .expect("set global t");
201
272
  }
202
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
+
203
335
  // Root/dynamic handlers -----------------------------------------------------
204
336
 
205
337
  async fn root_route(state: State<AppState>, req: Request<Body>) -> impl IntoResponse {
@@ -215,115 +347,259 @@ async fn dynamic_handler_inner(
215
347
  State(state): State<AppState>,
216
348
  req: Request<Body>,
217
349
  ) -> impl IntoResponse {
350
+
351
+ // ---------------------------
352
+ // BASIC REQUEST INFO
353
+ // ---------------------------
218
354
  let method = req.method().as_str().to_uppercase();
219
- let path = req.uri().path();
355
+ let path = req.uri().path().to_string();
220
356
  let key = format!("{}:{}", method, path);
221
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
+ // ---------------------------
222
387
  let body_bytes = match to_bytes(req.into_body(), usize::MAX).await {
223
388
  Ok(b) => b,
224
- 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
+ }
225
396
  };
226
- let body_str = String::from_utf8_lossy(&body_bytes).to_string();
227
397
 
228
- if let Some(route) = state.routes.get(&key) {
229
- match route.r#type.as_str() {
230
- "action" => {
231
- let action_name = route.value.as_str().unwrap_or("").trim();
232
- if action_name.is_empty() {
233
- return (StatusCode::INTERNAL_SERVER_ERROR, "Invalid action name").into_response();
234
- }
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
+ };
235
404
 
236
- // Resolve actions directory: prefer resolve_actions_dir(), fall back to heuristic find_actions_dir
237
- let resolved = resolve_actions_dir();
238
- let actions_dir = if resolved.exists() && resolved.is_dir() {
239
- resolved
240
- } else {
241
- match find_actions_dir(&state.project_root) {
242
- Some(p) => p,
243
- None => {
244
- return (
245
- StatusCode::INTERNAL_SERVER_ERROR,
246
- format!("Actions directory not found (checked multiple locations)"),
247
- )
248
- .into_response();
249
- }
250
- }
251
- };
405
+ // ---------------------------
406
+ // ROUTE RESOLUTION
407
+ // ---------------------------
408
+ let mut params: HashMap<String, String> = HashMap::new();
409
+ let mut action_name: Option<String> = None;
252
410
 
253
- let action_path = actions_dir.join(format!("{}.jsbundle", action_name));
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
- if !action_path.exists() {
256
- return (
257
- StatusCode::NOT_FOUND,
258
- format!("Action bundle not found: {:?}", action_path),
259
- )
260
- .into_response();
261
- }
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
+ }
262
453
 
263
- let js_code = match fs::read_to_string(&action_path) {
264
- Ok(v) => v,
265
- Err(e) => {
266
- return (
267
- StatusCode::INTERNAL_SERVER_ERROR,
268
- format!("Failed reading action bundle: {}", e),
269
- )
270
- .into_response();
271
- }
272
- };
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
+ };
273
468
 
274
- // Build env object
275
- let mut env_map = serde_json::Map::new();
276
- for (k, v) in std::env::vars() {
277
- env_map.insert(k, Value::String(v));
278
- }
279
- let env_json = Value::Object(env_map);
280
-
281
- // Injected script: sets process.env and __titan_req and invokes action function.
282
- let injected = format!(
283
- r#"
284
- globalThis.process = {{ env: {} }};
285
- const __titan_req = {};
286
- {};
287
- {}(__titan_req);
288
- "#,
289
- env_json.to_string(),
290
- body_str,
291
- js_code,
292
- action_name
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
+ );
514
+
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))
293
559
  );
294
-
295
- let mut ctx = Context::default();
296
- inject_t_fetch(&mut ctx);
297
-
298
- let result = match ctx.eval(Source::from_bytes(&injected)) {
299
- Ok(v) => v,
300
- Err(e) => return Json(json_error(e.to_string())).into_response(),
301
- };
302
-
303
- let result_json: Value = match result.to_json(&mut ctx) {
304
- Ok(v) => v,
305
- Err(e) => json_error(e.to_string()),
306
- };
307
-
308
- return Json(result_json).into_response();
309
- }
310
-
311
- "json" => return Json(route.value.clone()).into_response(),
312
- _ => {
313
- if let Some(s) = route.value.as_str() {
314
- return s.to_string().into_response();
315
- }
316
- return route.value.to_string().into_response();
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();
317
569
  }
318
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
+ _ => {}
319
598
  }
320
599
 
321
- (StatusCode::NOT_FOUND, "Not Found").into_response()
600
+ Json(result_json).into_response()
322
601
  }
323
602
 
324
- fn json_error(msg: String) -> Value {
325
- serde_json::json!({ "error": msg })
326
- }
327
603
 
328
604
  // Entrypoint ---------------------------------------------------------------
329
605
 
@@ -337,15 +613,22 @@ async fn main() -> Result<()> {
337
613
 
338
614
  let port = json["__config"]["port"].as_u64().unwrap_or(3000);
339
615
  let routes_json = json["routes"].clone();
340
- 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();
341
622
 
342
623
  // Project root — heuristics: try current_dir()
343
624
  let project_root = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
344
625
 
345
626
  let state = AppState {
346
627
  routes: Arc::new(map),
628
+ dynamic_routes: Arc::new(dynamic_routes),
347
629
  project_root,
348
630
  };
631
+
349
632
 
350
633
  let app = Router::new()
351
634
  .route("/", any(root_route))
@@ -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
  };