@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 +15 -0
- package/README.md +116 -187
- package/package.json +1 -1
- package/templates/app/actions/hello.js +4 -4
- package/templates/server/src/main.rs +426 -174
- package/templates/titan/bundle.js +24 -6
- package/templates/titan/titan.js +25 -11
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
130
|
+
### 🛣 Routing & HTTP
|
|
158
131
|
|
|
159
|
-
|
|
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
|
-
|
|
162
|
-
export function hello(req) {
|
|
163
|
-
return { message: "Hello from Titan!" };
|
|
164
|
-
}
|
|
140
|
+
### 🧠 Action Runtime
|
|
165
141
|
|
|
166
|
-
|
|
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
|
-
|
|
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
|
-
|
|
156
|
+
### 🧾 Request Object (`req`)
|
|
174
157
|
|
|
175
|
-
|
|
158
|
+
Each action receives a normalized request object:
|
|
176
159
|
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
170
|
+
This object is:
|
|
207
171
|
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
* External / internal APIs
|
|
172
|
+
* Stable
|
|
173
|
+
* Predictable
|
|
174
|
+
* Serializable
|
|
175
|
+
* Identical across dev & production
|
|
213
176
|
|
|
214
177
|
---
|
|
215
178
|
|
|
216
|
-
|
|
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
|
-
|
|
188
|
+
Example runtime log:
|
|
233
189
|
|
|
234
|
-
```bash
|
|
235
|
-
tit build
|
|
236
190
|
```
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
Titan generates an optimized **multi-stage Dockerfile**:
|
|
197
|
+
### 🧨 Error Handling & Diagnostics
|
|
249
198
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
Titan projects are designed for **direct repository upload**.
|
|
208
|
+
### ⚙ Build & Deployment
|
|
264
209
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
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
|
-
|
|
224
|
+
### 🧱 Architecture Guarantees
|
|
293
225
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
#
|
|
235
|
+
# 🧩 Example Action (Updated – No `globalThis` Needed)
|
|
316
236
|
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
|
|
237
|
+
```js
|
|
238
|
+
export function getUser(req) {
|
|
239
|
+
t.log("User id:", req.params.id);
|
|
320
240
|
|
|
321
|
-
|
|
241
|
+
return {
|
|
242
|
+
id: Number(req.params.id),
|
|
243
|
+
method: req.method
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
```
|
|
322
247
|
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
264
|
+
# 🧠 Final Note
|
|
340
265
|
|
|
341
|
-
|
|
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,26 +1,37 @@
|
|
|
1
1
|
// server/src/main.rs
|
|
2
|
-
use std::{collections::HashMap, env, fs, path::
|
|
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
|
-
|
|
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
|
|
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
|
|
104
|
-
|
|
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 =
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
.
|
|
125
|
-
.
|
|
200
|
+
.unwrap_or("GET")
|
|
201
|
+
.to_string();
|
|
126
202
|
|
|
127
|
-
let body_opt =
|
|
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
|
-
|
|
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
|
|
137
|
-
|
|
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
|
-
|
|
150
|
-
|
|
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
|
|
155
|
-
if let (Ok(name), Ok(val)) =
|
|
156
|
-
HeaderName::from_bytes(k.as_bytes()),
|
|
157
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
//
|
|
191
|
-
|
|
192
|
-
|
|
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(
|
|
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(_) =>
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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(¶ms).unwrap(),
|
|
510
|
+
serde_json::to_string(&query).unwrap(),
|
|
511
|
+
js_code,
|
|
512
|
+
action_name
|
|
513
|
+
);
|
|
275
514
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
(
|
|
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> =
|
|
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
|
|
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
|
-
|
|
37
|
-
|
|
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.");
|
package/templates/titan/titan.js
CHANGED
|
@@ -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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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(
|
|
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
|
};
|