@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 +15 -0
- package/README.md +117 -187
- package/package.json +1 -1
- package/templates/app/actions/hello.js +4 -4
- package/templates/server/src/main.rs +418 -135
- 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,16 @@
|
|
|
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
|
+
💟 **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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
131
|
+
### 🛣 Routing & HTTP
|
|
158
132
|
|
|
159
|
-
|
|
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
|
-
|
|
162
|
-
export function hello(req) {
|
|
163
|
-
return { message: "Hello from Titan!" };
|
|
164
|
-
}
|
|
141
|
+
### 🧠 Action Runtime
|
|
165
142
|
|
|
166
|
-
|
|
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
|
-
|
|
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
|
-
|
|
157
|
+
### 🧾 Request Object (`req`)
|
|
174
158
|
|
|
175
|
-
|
|
159
|
+
Each action receives a normalized request object:
|
|
176
160
|
|
|
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
|
-
};
|
|
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
|
-
|
|
171
|
+
This object is:
|
|
207
172
|
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
* External / internal APIs
|
|
173
|
+
* Stable
|
|
174
|
+
* Predictable
|
|
175
|
+
* Serializable
|
|
176
|
+
* Identical across dev & production
|
|
213
177
|
|
|
214
178
|
---
|
|
215
179
|
|
|
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
|
-
|
|
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
|
-
|
|
189
|
+
Example runtime log:
|
|
233
190
|
|
|
234
|
-
```bash
|
|
235
|
-
tit build
|
|
236
191
|
```
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
Titan generates an optimized **multi-stage Dockerfile**:
|
|
198
|
+
### 🧨 Error Handling & Diagnostics
|
|
249
199
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
Titan projects are designed for **direct repository upload**.
|
|
209
|
+
### ⚙ Build & Deployment
|
|
264
210
|
|
|
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
|
-
```
|
|
211
|
+
* Native Rust binary output
|
|
212
|
+
* Zero-config Dockerfile generation
|
|
213
|
+
* Multi-stage optimized Docker builds
|
|
214
|
+
* Works on:
|
|
287
215
|
|
|
288
|
-
|
|
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
|
-
|
|
225
|
+
### 🧱 Architecture Guarantees
|
|
293
226
|
|
|
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.
|
|
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
|
-
#
|
|
236
|
+
# 🧩 Example Action (Updated – No `globalThis` Needed)
|
|
316
237
|
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
|
|
238
|
+
```js
|
|
239
|
+
export function getUser(req) {
|
|
240
|
+
t.log("User id:", req.params.id);
|
|
320
241
|
|
|
321
|
-
|
|
242
|
+
return {
|
|
243
|
+
id: Number(req.params.id),
|
|
244
|
+
method: req.method
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
```
|
|
322
248
|
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
265
|
+
# 🧠 Final Note
|
|
340
266
|
|
|
341
|
-
|
|
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
|
@@ -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
|
|
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
|
+
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 =
|
|
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
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
//
|
|
190
|
-
|
|
191
|
-
|
|
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(
|
|
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(_) =>
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
+
);
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
(
|
|
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> =
|
|
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
|
|
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
|
};
|