@ezetgalaxy/titan 25.14.5 → 25.14.7

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/README.md CHANGED
@@ -1,339 +1,344 @@
1
-
2
- ```
3
- ████████╗██╗████████╗ █████╗ ███╗ ██╗
4
- ╚══██╔══╝██║╚══██╔══╝██╔══██╗████╗ ██║
5
- ██║ ██║ ██║ ███████║██╔██╗ ██║
6
- ██║ ██║ ██║ ██╔══██║██║╚██╗██║
7
- ██║ ██║ ██║ ██║ ██║██║ ╚████║
8
- ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝
9
- ```
10
-
11
- # TITAN PLANET 🚀
12
-
13
- **JavaScript Simplicity. Rust Power. Zero Configuration.**
14
-
15
- Titan Planet is a JavaScript-first backend framework that compiles your JavaScript routes and actions into a **native Rust + Axum server**.
16
-
17
- You write **zero Rust**.
18
- Titan ships a full backend engine, dev server, bundler, router, action runtime, and Docker deploy pipeline all powered by Rust under the hood.
19
-
20
- Titan = JavaScript productivity × Rust performance × Zero DevOps.
21
-
22
- ---
23
-
24
- # 🌌 Why Titan?
25
-
26
- | Feature | Titan | Express/Nest | FastAPI | Bun |
27
- | ------------------------------------ | ----- | ------------ | ------- | --------- |
28
- | Native binary output | ✅ Yes | ❌ No | ❌ No | ❌ No |
29
- | Rust-level performance | Yes | No | No | ❌ No |
30
- | Pure JavaScript developer experience | Yes | Yes | No | Partial |
31
- | Zero-config Docker deploy | ✅ Yes | ❌ No | ❌ No | ❌ No |
32
- | Action-based architecture | ✅ Yes | ❌ No | ❌ No | ❌ No |
33
- | Hot reload dev server | ✅ Yes | No | ❌ No | ❌ No |
34
-
35
- Titan gives you:
36
-
37
- * Native speed
38
- * JS comfort
39
- * Cloud-first deployment
40
- * Full environment variable support
41
- * Built-in HTTP client (`t.fetch`)
42
- * Lightweight serverless-like actions
43
- * Instant hot reload
44
- * Zero configuration
45
- * Single deployable binary
46
-
47
- ---
48
-
49
- # 🚀 Quick Start
50
-
51
-
52
- # Requirements
53
-
54
- Install before using Titan:
55
-
56
- ### 1. Rust (latest stable)
57
-
58
- [https://rust-lang.org/tools/install/](https://rust-lang.org/tools/install/)
59
-
60
- ### 2. Node.js (v18+)
61
-
62
- Required for:
63
-
64
- * Titan CLI
65
- * esbuild
66
- * JS → Rust compilation pipeline
67
-
68
- Verify:
69
-
70
- ```bash
71
- node -v
72
- npm -v
73
- rustc -V
74
- ```
75
-
76
- ---
77
-
78
- ### Install Titan CLI
79
-
80
- ```bash
81
- npm install -g @ezetgalaxy/titan
82
- ```
83
-
84
- ### Create a new project
85
-
86
- ```bash
87
- tit init my-app
88
- cd my-app
89
- tit dev
90
- ```
91
-
92
- Titan will:
93
-
94
- * Build routes
95
- * Bundle actions
96
- * Start Rust dev server
97
- * Watch file changes
98
- * Trigger instant reload
99
-
100
- ---
101
-
102
- # 📁 Project Layout
103
-
104
- ```
105
- my-app/
106
- ├── app/ # You develop ONLY this folder
107
- │ ├── app.js # Titan routes (DSL)
108
- │ └── actions/ # Your custom JS actions
109
- │ └── hello.js # Example Titan action
110
-
111
- ───────────────────────────────────────────────────────────────
112
- Everything below is auto-generated by `tit init`
113
- You never modify these folders manually
114
- ───────────────────────────────────────────────────────────────
115
-
116
- ├── titan/ # Titan's internal JS engine
117
- │ ├── titan.js # Titan DSL runtime
118
- │ ├── bundle.js # JS → .jsbundle bundler
119
- │ └── dev.js # Hot Reload system
120
-
121
- ├── server/ # Auto-generated Rust backend
122
- ├── Cargo.toml # Rust project config
123
- ├── src/ # Rust source code
124
- ├── actions/ # Compiled .jsbundle actions
125
- │ ├── titan/ # Internal Rust runtime files
126
- │ ├── routes.json # Generated route metadata
127
- │ ├── action_map.json # Maps actions to bundles
128
- └── titan-server # Final production Rust binary
129
-
130
- ├── Dockerfile # Auto-generated production Dockerfile
131
- ├── .dockerignore # Auto-generated Docker ignore rules
132
- ├── package.json # JS project config (auto)
133
- └── .gitignore # Auto-generated by `tit init`
134
-
135
- ```
136
-
137
- ---
138
-
139
- # 🛣 Example Route
140
-
141
- **app/app.js**
142
-
143
- ```js
144
- import t from "../titan/titan.js";
145
-
146
- t.post("/hello").action("hello");
147
- t.get("/").reply("Welcome to Titan Planet");
148
-
149
- t.start(3000, "Ready to land on Titan 🚀");
150
- ```
151
-
152
- ---
153
-
154
- # 🧩 Example Action
155
-
156
- **app/actions/hello.js**
157
-
158
- ```js
159
- export function hello(req) {
160
- return { message: "Hello from Titan!" };
161
- }
162
-
163
- globalThis.hello = hello;
164
- ```
165
-
166
- ---
167
-
168
- # ⚡ New: Built-In HTTP Fetch (`t.fetch`)
169
-
170
- Titan now includes a built-in server-side `fetch` bridge powered by Rust.
171
-
172
- Use it to call any external API:
173
-
174
- ```js
175
- export function hello(req) {
176
- const body = JSON.stringify({
177
- model: "gpt-4.1-mini",
178
- messages: [{ role: "user", content: "hii" }]
179
- });
180
-
181
- const r = t.fetch("https://api.openai.com/v1/chat/completions", {
182
- method: "POST",
183
- headers: {
184
- "Content-Type": "application/json",
185
- "Authorization": `Bearer ${process.env.OPENAI_API_KEY}`
186
- },
187
- body
188
- });
189
-
190
- const json = JSON.parse(r.body);
191
-
192
- return {
193
- ok: true,
194
- message: json.choices[0].message.content
195
- };
196
- }
197
-
198
- globalThis.hello = hello;
199
- ```
200
-
201
- ### `t.fetch` supports:
202
-
203
- * GET, POST, PUT, DELETE
204
- * Custom headers
205
- * JSON bodies
206
- * Authorization tokens
207
- * External / internal APIs
208
-
209
- ---
210
-
211
- # 🔥 Hot Reload Dev Server
212
-
213
- ```bash
214
- tit dev
215
- ```
216
-
217
- Titan’s dev engine:
218
-
219
- * Rebuilds routes
220
- * Rebundil actions
221
- * Restarts Rust server
222
- * Updates instantly
223
-
224
-
225
- ---
226
-
227
- # 🧱 Production Build
228
-
229
- ```bash
230
- tit build
231
- ```
232
-
233
- Output includes:
234
-
235
- * `titan-server` native binary
236
- * JS bundles
237
- * routing metadata
238
-
239
- ---
240
-
241
- # 🐳 Docker Deployment (Zero Config)
242
-
243
- Titan generates an optimized **multi-stage Dockerfile**:
244
-
245
- Works on:
246
-
247
- * Railway
248
- * Fly.io
249
- * Render
250
- * VPS / Dedicated servers
251
- * Docker Hub
252
- * Kubernetes
253
-
254
- ---
255
-
256
- # Uploading Titan to GitHub
257
-
258
- Titan projects are designed for **direct repository upload**.
259
-
260
- Include everything generated by `tit init`:
261
-
262
- ```
263
- app/
264
- titan/
265
- server/
266
- Cargo.toml
267
- Dockerfile
268
- .gitignore
269
- package.json
270
- ```
271
-
272
- Push to GitHub:
273
-
274
- ```bash
275
- git init
276
- git add .
277
- git commit -m "Initial Titan project"
278
- git branch -M main
279
- git remote add origin <your_repo_url>
280
- git push -u origin main
281
- ```
282
-
283
- Your repo is now fully deployable with Docker.
284
-
285
- ---
286
-
287
- # ☁ Zero-Config Deployment with Docker
288
-
289
- Once pushed to GitHub, you can deploy anywhere.
290
-
291
- ## Deploy to Railway
292
-
293
- 1. Go to Railway
294
- 2. Create New Project Deploy from GitHub
295
- 3. Select your Titan repo
296
- 4. Railway auto-detects the Dockerfile
297
- 5. It builds + deploys automatically
298
-
299
- Railway will:
300
-
301
- * Build your Rust server
302
- * Copy JS bundles
303
- * Start the `titan-server` binary
304
- * Expose the correct port
305
-
306
- No configuration required.
307
-
308
- ---
309
-
310
- # ✨ Updating Titan
311
-
312
- ```bash
313
- tit update
314
- ```
315
-
316
- Updates:
317
-
318
- * Titan CLI
319
- * DSL
320
- * Bundler
321
- * Dev server
322
- * Rust runtime templates
323
- * Dockerfile
324
-
325
- ---
326
-
327
- # 📦 Version
328
-
329
- **Titan v25 — Stable**
330
- Optimized for production, cloud deployment, and AI workloads.
331
-
332
- ---
333
-
334
- # 🤝 Contributing
335
-
336
- Pull requests welcome
337
- https://github.com/ezet-galaxy/-ezetgalaxy-titan
338
-
339
- ---
1
+
2
+ ```
3
+ ████████╗██╗████████╗ █████╗ ███╗ ██╗
4
+ ╚══██╔══╝██║╚══██╔══╝██╔══██╗████╗ ██║
5
+ ██║ ██║ ██║ ███████║██╔██╗ ██║
6
+ ██║ ██║ ██║ ██╔══██║██║╚██╗██║
7
+ ██║ ██║ ██║ ██║ ██║██║ ╚████║
8
+ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝
9
+ ```
10
+ # Notice
11
+ **Production mode is under development 😞**
12
+ **Enjoy development mode `tit dev` 💙**
13
+
14
+ # TITAN PLANET 🚀
15
+
16
+ **JavaScript Simplicity. Rust Power. Zero Configuration.**
17
+
18
+ Titan Planet is a JavaScript-first backend framework that compiles your JavaScript routes and actions into a **native Rust + Axum server**.
19
+
20
+ You write **zero Rust**.
21
+ Titan ships a full backend engine, dev server, bundler, router, action runtime, and Docker deploy pipeline — all powered by Rust under the hood.
22
+
23
+ Titan = JavaScript productivity × Rust performance × Zero DevOps.
24
+
25
+ ---
26
+
27
+ # 🌌 Why Titan?
28
+
29
+ | Feature | Titan | Express/Nest | FastAPI | Bun |
30
+ | ------------------------------------ | ----- | ------------ | ------- | --------- |
31
+ | Native binary output | ✅ Yes | ❌ No | ❌ No | ❌ No |
32
+ | Rust-level performance | ✅ Yes | ❌ No | ❌ No | ❌ No |
33
+ | Pure JavaScript developer experience | ✅ Yes | Yes | ❌ No | ❌ Partial |
34
+ | Zero-config Docker deploy | ✅ Yes | ❌ No | ❌ No | ❌ No |
35
+ | Action-based architecture | ✅ Yes | ❌ No | ❌ No | ❌ No |
36
+ | Hot reload dev server | ✅ Yes | ❌ No | ❌ No | ❌ No |
37
+
38
+ Titan gives you:
39
+
40
+ * Native speed
41
+ * JS comfort
42
+ * Cloud-first deployment
43
+ * Full environment variable support
44
+ * Built-in HTTP client (`t.fetch`)
45
+ * Lightweight serverless-like actions
46
+ * Instant hot reload
47
+ * Zero configuration
48
+ * Single deployable binary
49
+
50
+ ---
51
+
52
+ # 🚀 Quick Start
53
+
54
+
55
+ # ⚙ Requirements
56
+
57
+ Install before using Titan:
58
+
59
+ ### 1. Rust (latest stable)
60
+
61
+ [https://rust-lang.org/tools/install/](https://rust-lang.org/tools/install/)
62
+
63
+ ### 2. Node.js (v18+)
64
+
65
+ Required for:
66
+
67
+ * Titan CLI
68
+ * esbuild
69
+ * JS → Rust compilation pipeline
70
+
71
+ Verify:
72
+
73
+ ```bash
74
+ node -v
75
+ npm -v
76
+ rustc -V
77
+ ```
78
+
79
+ ---
80
+
81
+ ### Install Titan CLI
82
+
83
+ ```bash
84
+ npm install -g @ezetgalaxy/titan
85
+ ```
86
+
87
+ ### Create a new project
88
+
89
+ ```bash
90
+ tit init my-app
91
+ cd my-app
92
+ tit dev
93
+ ```
94
+
95
+ Titan will:
96
+
97
+ * Build routes
98
+ * Bundle actions
99
+ * Start Rust dev server
100
+ * Watch file changes
101
+ * Trigger instant reload
102
+
103
+ ---
104
+
105
+ # 📁 Project Layout
106
+
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`
137
+
138
+ ```
139
+
140
+ ---
141
+
142
+ # 🛣 Example Route
143
+
144
+ **app/app.js**
145
+
146
+ ```js
147
+ import t from "../titan/titan.js";
148
+
149
+ t.post("/hello").action("hello");
150
+ t.get("/").reply("Welcome to Titan Planet");
151
+
152
+ t.start(3000, "Ready to land on Titan 🚀");
153
+ ```
154
+
155
+ ---
156
+
157
+ # 🧩 Example Action
158
+
159
+ **app/actions/hello.js**
160
+
161
+ ```js
162
+ export function hello(req) {
163
+ return { message: "Hello from Titan!" };
164
+ }
165
+
166
+ globalThis.hello = hello;
167
+ ```
168
+
169
+ ---
170
+
171
+ # ⚡ New: Built-In HTTP Fetch (`t.fetch`)
172
+
173
+ Titan now includes a built-in server-side `fetch` bridge powered by Rust.
174
+
175
+ Use it to call any external API:
176
+
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
+ };
201
+ }
202
+
203
+ globalThis.hello = hello;
204
+ ```
205
+
206
+ ### `t.fetch` supports:
207
+
208
+ * GET, POST, PUT, DELETE
209
+ * Custom headers
210
+ * JSON bodies
211
+ * Authorization tokens
212
+ * External / internal APIs
213
+
214
+ ---
215
+
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
+
229
+
230
+ ---
231
+
232
+ # 🧱 Production Build
233
+
234
+ ```bash
235
+ tit build
236
+ ```
237
+
238
+ Output includes:
239
+
240
+ * `titan-server` native binary
241
+ * JS bundles
242
+ * routing metadata
243
+
244
+ ---
245
+
246
+ # 🐳 Docker Deployment (Zero Config)
247
+
248
+ Titan generates an optimized **multi-stage Dockerfile**:
249
+
250
+ Works on:
251
+
252
+ * Railway
253
+ * Fly.io
254
+ * Render
255
+ * VPS / Dedicated servers
256
+ * Docker Hub
257
+ * Kubernetes
258
+
259
+ ---
260
+
261
+ # ☁ Uploading Titan to GitHub
262
+
263
+ Titan projects are designed for **direct repository upload**.
264
+
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
+ ```
287
+
288
+ Your repo is now fully deployable with Docker.
289
+
290
+ ---
291
+
292
+ # ☁ Zero-Config Deployment with Docker
293
+
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.
312
+
313
+ ---
314
+
315
+ # ✨ Updating Titan
316
+
317
+ ```bash
318
+ tit update
319
+ ```
320
+
321
+ Updates:
322
+
323
+ * Titan CLI
324
+ * DSL
325
+ * Bundler
326
+ * Dev server
327
+ * Rust runtime templates
328
+ * Dockerfile
329
+
330
+ ---
331
+
332
+ # 📦 Version
333
+
334
+ **Titan v25 — Stable**
335
+ Optimized for production, cloud deployment, and AI workloads.
336
+
337
+ ---
338
+
339
+ # 🤝 Contributing
340
+
341
+ Pull requests welcome
342
+ https://github.com/ezet-galaxy/-ezetgalaxy-titan
343
+
344
+ ---
package/index.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  import fs from "fs";
3
3
  import path from "path";
4
4
  import { execSync, spawn } from "child_process";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ezetgalaxy/titan",
3
- "version": "25.14.5",
3
+ "version": "25.14.7",
4
4
  "description": "JavaScript backend framework that compiles your JS into a Rust + Axum server.",
5
5
  "license": "ISC",
6
6
  "author": "ezetgalaxy",
@@ -1,6 +1,5 @@
1
1
  function hello(req) {
2
- const name = req.name;
3
- return { name: name, msg: `Hello ${name}` }
2
+ return { "name": `${req.name || "user"}`, msg: `welcome to titan planet ${req.name || "user"}` }
4
3
  }
5
4
 
6
5
  globalThis.hello = hello;
@@ -1,34 +1,28 @@
1
- use std::{collections::HashMap, fs, path::PathBuf, sync::Arc};
1
+ // server/src/main.rs
2
+ use std::{collections::HashMap, env, fs, path::Path, path::PathBuf, sync::Arc};
2
3
 
3
4
  use anyhow::Result;
4
5
  use axum::{
5
- body::{to_bytes, Body},
6
+ Router,
7
+ body::{Body, to_bytes},
6
8
  extract::State,
7
9
  http::{Request, StatusCode},
8
10
  response::{IntoResponse, Json},
9
11
  routing::any,
10
- Router,
11
12
  };
12
13
 
13
- use std::path::Path;
14
-
15
- use boa_engine::{
16
- js_string,
17
- native_function::NativeFunction,
18
- object::ObjectInitializer,
19
- property::Attribute,
20
- Context, JsValue, Source,
21
- };
14
+ use boa_engine::{Context, JsValue, Source, object::ObjectInitializer};
15
+ use boa_engine::{js_string, native_function::NativeFunction, property::Attribute};
22
16
 
23
17
  use reqwest::blocking::Client;
24
18
  use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
25
19
 
26
20
  use serde::Deserialize;
27
21
  use serde_json::Value;
28
-
29
22
  use tokio::net::TcpListener;
30
23
  use tokio::task;
31
24
 
25
+ /// Route configuration (loaded from routes.json)
32
26
  #[derive(Debug, Deserialize)]
33
27
  struct RouteVal {
34
28
  r#type: String,
@@ -38,245 +32,367 @@ struct RouteVal {
38
32
  #[derive(Clone)]
39
33
  struct AppState {
40
34
  routes: Arc<HashMap<String, RouteVal>>,
35
+ project_root: PathBuf,
36
+ }
37
+
38
+ // -------------------------
39
+ // ACTION DIRECTORY RESOLUTION
40
+ // -------------------------
41
+
42
+ fn resolve_actions_dir() -> PathBuf {
43
+ // Respect explicit override first
44
+ if let Ok(override_dir) = env::var("TITAN_ACTIONS_DIR") {
45
+ return PathBuf::from(override_dir);
46
+ }
47
+
48
+ // Production container layout
49
+ if Path::new("/app/actions").exists() {
50
+ return PathBuf::from("/app/actions");
51
+ }
52
+
53
+ // Try to walk up from the executing binary to discover `<...>/server/actions`
54
+ if let Ok(exe) = std::env::current_exe() {
55
+ if let Some(parent) = exe.parent() {
56
+ if let Some(target_dir) = parent.parent() {
57
+ if let Some(server_dir) = target_dir.parent() {
58
+ let candidate = server_dir.join("actions");
59
+ if candidate.exists() {
60
+ return candidate;
61
+ }
62
+ }
63
+ }
64
+ }
65
+ }
66
+
67
+ // Fall back to local ./actions
68
+ PathBuf::from("./actions")
41
69
  }
42
70
 
71
+ /// Try to find the directory that contains compiled action bundles.
72
+ ///
73
+ /// Checks multiple likely paths to support both dev and production container layouts:
74
+ /// - <project_root>/server/actions
75
+ /// - <project_root>/actions
76
+ /// - <project_root>/../server/actions
77
+ /// - /app/actions
78
+ /// - ./actions
79
+ fn find_actions_dir(project_root: &PathBuf) -> Option<PathBuf> {
80
+ let candidates = [
81
+ project_root.join("server").join("actions"),
82
+ project_root.join("actions"),
83
+ project_root.join("..").join("server").join("actions"),
84
+ PathBuf::from("/app").join("actions"),
85
+ PathBuf::from("actions"),
86
+ ];
87
+
88
+ for p in &candidates {
89
+ if p.exists() && p.is_dir() {
90
+ return Some(p.clone());
91
+ }
92
+ }
93
+
94
+ None
95
+ }
96
+
97
+ /// Injects a synchronous `t.fetch(url, opts?)` function into the Boa `Context`.
98
+ ///
99
+ /// Implementation details:
100
+ /// - Converts JS opts → `serde_json::Value` (owned) using `to_json`.
101
+ /// - Executes reqwest blocking client inside `tokio::task::block_in_place` to avoid blocking async runtime.
102
+ /// - Returns `{ ok: bool, status?: number, body?: string, error?: string }`.
43
103
  fn inject_t_fetch(ctx: &mut Context) {
104
+ // Native function (Boa 0.20) using from_fn_ptr
44
105
  let t_fetch_native = NativeFunction::from_fn_ptr(|_this, args, ctx| {
106
+ // Extract URL (owned string)
45
107
  let url = args
46
108
  .get(0)
47
109
  .and_then(|v| v.to_string(ctx).ok())
48
110
  .map(|s| s.to_std_string_escaped())
49
111
  .unwrap_or_default();
50
112
 
113
+ // Extract opts -> convert to serde_json::Value (owned)
51
114
  let opts_js = args.get(1).cloned().unwrap_or(JsValue::undefined());
52
- let opts_json: Value = opts_js.to_json(ctx).unwrap_or(Value::Null);
115
+ let opts_json: Value = match opts_js.to_json(ctx) {
116
+ Ok(v) => v,
117
+ Err(_) => Value::Object(serde_json::Map::new()),
118
+ };
53
119
 
120
+ // Pull method, body, headers into owned Rust values
54
121
  let method = opts_json
55
122
  .get("method")
56
123
  .and_then(|m| m.as_str())
57
- .unwrap_or("GET")
58
- .to_string();
124
+ .map(|s| s.to_string())
125
+ .unwrap_or_else(|| "GET".to_string());
59
126
 
60
- let body_opt = opts_json.get("body").cloned();
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
+ };
61
132
 
62
- let mut headers = HeaderMap::new();
133
+ // headers as Vec<(String,String)>
134
+ let mut header_pairs: Vec<(String, String)> = Vec::new();
63
135
  if let Some(Value::Object(map)) = opts_json.get("headers") {
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
- }
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));
70
142
  }
71
143
  }
72
144
 
73
- let out = task::block_in_place(move || {
145
+ // Perform the blocking HTTP request inside block_in_place to avoid runtime panic
146
+ let out_json = task::block_in_place(move || {
74
147
  let client = Client::new();
75
- let mut req = client.request(method.parse().unwrap(), &url);
76
148
 
77
- req = req.headers(headers);
149
+ let method_parsed = method.parse().unwrap_or(reqwest::Method::GET);
150
+ let mut req = client.request(method_parsed, &url);
151
+
152
+ if !header_pairs.is_empty() {
153
+ 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
+ ) {
159
+ headers.insert(name, val);
160
+ }
161
+ }
162
+ req = req.headers(headers);
163
+ }
78
164
 
79
165
  if let Some(body) = body_opt {
80
- req = req.body(body.to_string());
166
+ req = req.body(body);
81
167
  }
82
168
 
83
169
  match req.send() {
84
170
  Ok(resp) => {
85
171
  let status = resp.status().as_u16();
86
172
  let text = resp.text().unwrap_or_default();
87
- serde_json::json!({ "ok": true, "status": status, "body": text })
173
+ serde_json::json!({
174
+ "ok": true,
175
+ "status": status,
176
+ "body": text
177
+ })
88
178
  }
89
- Err(e) => serde_json::json!({ "ok": false, "error": e.to_string() }),
179
+ Err(e) => serde_json::json!({
180
+ "ok": false,
181
+ "error": e.to_string()
182
+ }),
90
183
  }
91
184
  });
92
185
 
93
- Ok(JsValue::from_json(&out, ctx).unwrap())
186
+ // Convert serde_json::Value -> JsValue
187
+ Ok(JsValue::from_json(&out_json, ctx).unwrap_or(JsValue::undefined()))
94
188
  });
95
189
 
190
+ // Convert native function to JS function object (requires Realm)
96
191
  let realm = ctx.realm();
97
192
  let t_fetch_js_fn = t_fetch_native.to_js_function(realm);
98
193
 
194
+ // Build `t` object with `.fetch`
99
195
  let t_obj = ObjectInitializer::new(ctx)
100
196
  .property(js_string!("fetch"), t_fetch_js_fn, Attribute::all())
101
197
  .build();
102
198
 
103
199
  ctx.global_object()
104
200
  .set(js_string!("t"), JsValue::from(t_obj), false, ctx)
105
- .unwrap();
106
- }
107
-
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
- }
201
+ .expect("set global t");
130
202
  }
131
203
 
204
+ // Root/dynamic handlers -----------------------------------------------------
132
205
 
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")
206
+ async fn root_route(state: State<AppState>, req: Request<Body>) -> impl IntoResponse {
207
+ dynamic_handler_inner(state, req).await
153
208
  }
154
209
 
210
+ async fn dynamic_route(state: State<AppState>, req: Request<Body>) -> impl IntoResponse {
211
+ dynamic_handler_inner(state, req).await
212
+ }
155
213
 
156
-
157
- async fn dynamic_handler(
214
+ /// Main handler: looks up routes.json and executes action bundles using Boa.
215
+ async fn dynamic_handler_inner(
158
216
  State(state): State<AppState>,
159
217
  req: Request<Body>,
160
218
  ) -> impl IntoResponse {
161
219
  let method = req.method().as_str().to_uppercase();
162
- let path = req.uri().path().to_string();
220
+ let path = req.uri().path();
163
221
  let key = format!("{}:{}", method, path);
164
222
 
165
223
  let body_bytes = match to_bytes(req.into_body(), usize::MAX).await {
166
- Ok(v) => v,
224
+ Ok(b) => b,
167
225
  Err(_) => return (StatusCode::BAD_REQUEST, "Failed to read body").into_response(),
168
226
  };
169
227
  let body_str = String::from_utf8_lossy(&body_bytes).to_string();
170
228
 
171
- let route = match state.routes.get(&key) {
172
- Some(r) => r,
173
- None => return (StatusCode::NOT_FOUND, "Not Found").into_response(),
174
- };
175
-
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
- }
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
+ }
182
237
 
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
- }
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
+ };
191
254
 
192
- let actions_dir = resolve_actions_dir();
193
- let action_path = actions_dir.join(format!("{}.jsbundle", action_name));
255
+ let action_path = actions_dir.join(format!("{}.jsbundle", action_name));
194
256
 
195
- if !action_path.exists() {
196
- return (
197
- StatusCode::NOT_FOUND,
198
- format!("Action bundle not found: {:?}", action_path),
199
- )
200
- .into_response();
201
- }
257
+ if !action_path.exists() {
258
+ return (
259
+ StatusCode::NOT_FOUND,
260
+ format!("Action bundle not found: {:?}", action_path),
261
+ )
262
+ .into_response();
263
+ }
202
264
 
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();
211
- }
212
- };
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
+ };
213
275
 
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));
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);
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);
226
315
  "#,
227
- env_json.to_string(),
228
- body_str,
229
- js_code,
230
- action_name
231
- );
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
321
+ );
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
+ }
232
338
 
233
- let mut ctx = Context::default();
234
- inject_t_fetch(&mut ctx);
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();
345
+ }
346
+ }
347
+ }
235
348
 
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
- };
349
+ (StatusCode::NOT_FOUND, "Not Found").into_response()
350
+ }
240
351
 
241
- let result_json = result.to_json(&mut ctx).unwrap_or(Value::Null);
242
- Json(result_json).into_response()
352
+ fn json_error(msg: String) -> Value {
353
+ serde_json::json!({ "error": msg })
243
354
  }
244
355
 
356
+ // Entrypoint ---------------------------------------------------------------
357
+
245
358
  #[tokio::main]
246
359
  async fn main() -> Result<()> {
247
360
  dotenvy::dotenv().ok();
248
361
 
249
- let raw = fs::read_to_string("./routes.json").unwrap_or("{}".into());
362
+ // Load routes.json (expected at runtime root)
363
+ let raw = fs::read_to_string("./routes.json").unwrap_or_else(|_| "{}".to_string());
250
364
  let json: Value = serde_json::from_str(&raw).unwrap_or_default();
251
365
 
252
366
  let port = json["__config"]["port"].as_u64().unwrap_or(3000);
253
- let routes_map: HashMap<String, RouteVal> =
254
- serde_json::from_value(json["routes"].clone()).unwrap_or_default();
367
+ let routes_json = json["routes"].clone();
368
+ let map: HashMap<String, RouteVal> = serde_json::from_value(routes_json).unwrap_or_default();
255
369
 
256
- let _project_root = detect_project_root();
370
+ // Project root — heuristics: try current_dir()
371
+ let project_root = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
257
372
 
258
- let state = AppState {
259
- routes: Arc::new(routes_map),
260
- };
261
-
262
-
373
+ let state = AppState {
374
+ routes: Arc::new(map),
375
+ project_root,
376
+ };
263
377
 
264
378
  let app = Router::new()
265
- .route("/", any(dynamic_handler))
266
- .fallback(any(dynamic_handler))
379
+ .route("/", any(root_route))
380
+ .fallback(any(dynamic_route))
267
381
  .with_state(state);
268
382
 
269
383
  let listener = TcpListener::bind(format!("0.0.0.0:{}", port)).await?;
270
- // TITAN BANNER
271
- println!("\n\x1b[38;5;208m████████╗██╗████████╗ █████╗ ███╗ ██╗");
272
- println!("╚══██╔══╝██║╚══██╔══╝██╔══██╗████╗ ██║");
273
- println!(" ██║ ██║ ██║ ███████║██╔██╗ ██║");
274
- println!(" ██║ ██║ ██║ ██╔══██║██║╚██╗██║");
275
- println!(" ██║ ██║ ██║ ██║ ██║██║ ╚████║");
276
- println!(" ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝\x1b[0m\n");
277
-
278
-
279
- println!("\x1b[38;5;39mTitan server running at:\x1b[0m http://localhost:{}", port);
384
+
385
+ // Banner (yellow-orange) and server info
386
+ println!("\n\x1b[38;5;208m████████╗██╗████████╗ █████╗ ███╗ ██╗");
387
+ println!("╚══██╔══╝██║╚══██╔══╝██╔══██╗████╗ ██║");
388
+ println!(" ██║ ██║ ██║ ███████║██╔██╗ ██║");
389
+ println!(" ██║ ██║ ██║ ██╔══██║██║╚██╗██║");
390
+ println!(" ██║ ██║ ██║ ██║ ██║██║ ╚████║");
391
+ println!(" ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝\x1b[0m\n");
392
+ println!(
393
+ "\x1b[38;5;39mTitan server running at:\x1b[0m http://localhost:{}",
394
+ port
395
+ );
280
396
 
281
397
  axum::serve(listener, app).await?;
282
398
  Ok(())
@@ -28,12 +28,16 @@ export async function bundle() {
28
28
 
29
29
  await esbuild.build({
30
30
  entryPoints: [entry],
31
+ outfile: outfile,
31
32
  bundle: true,
32
- format: "cjs",
33
+ format: "iife",
33
34
  platform: "neutral",
34
- outfile,
35
- minify: false,
36
- });
35
+ target: "es2020",
36
+ banner: {
37
+ js: ""
38
+ }
39
+ });
40
+
37
41
  }
38
42
 
39
43
  console.log("[Titan] Bundling finished.");